"""Telnet MUD realm for the txt3 BBS. Implements the same rules as the WAP/XHTML front-end (lib/mud.php) against the same SQLite tables, so a character created on the web can be played live here and vice-versa. Combat/levelling formulas deliberately match the PHP engine so both front-ends behave identically. This module is imported by bbsd.py; it exposes mud_menu(t, con, me). """ import json import random import time from bbsdb import q, q1, ex, ago START_HP, START_ATK, START_DEF = 30, 4, 2 RESPAWN = 30 CLASSES = { "fighter": ("Fighter", 2, 0, 6), "mage": ("Mage", 4, 0, 0), "thief": ("Thief", 1, 2, 2), } def class_bonus(cls): return CLASSES.get(cls, CLASSES["fighter"]) def item(con, key): return q1(con, "SELECT * FROM mud_items WHERE key_name=?", (key,)) def char_for(con, uid): r = q1(con, "SELECT * FROM mud_chars WHERE user_id=?", (uid,)) return dict(r) if r else None def room(con, rid): r = q1(con, "SELECT * FROM mud_rooms WHERE id=?", (rid,)) if r: r = dict(r) r["exits"] = json.loads(r["exits"]) return r def ensure_char(con, me, cls="fighter"): c = char_for(con, me["id"]) if c: return dict(c) if cls not in CLASSES: cls = "fighter" nm, batk, bdef, bhp = class_bonus(cls) maxhp = START_HP + bhp ex(con, "INSERT INTO mud_chars (user_id,name,class,room_id,hp,max_hp,atk,def,xp,level,gold)" " VALUES (?,?,?,1,?,?,?,?,0,1,0)", (me["id"], me["username"], cls, maxhp, maxhp, START_ATK + batk, START_DEF + bdef)) echo(con, 1, "%s the %s arrives in the world." % (me["username"], nm)) return dict(char_for(con, me["id"])) def echo(con, rid, text): ex(con, "INSERT INTO mud_events (room_id,ts,text) VALUES (?,?,?)", (rid, int(time.time()), text)) def alive_spawns(con, rid): return q(con, "SELECT s.*, m.key_name AS key_name, m.name AS name, m.descr AS descr " "FROM mud_spawn s JOIN mud_mobs m ON m.id=s.mob_id " "WHERE s.room_id=? AND s.alive=1 ORDER BY s.id", (rid,)) def ground_items(con, rid): return q(con, "SELECT g.id, i.key_name AS key_name, i.name AS name, i.descr AS descr " "FROM mud_ground g JOIN mud_items i ON i.id=g.item_id " "WHERE g.room_id=? ORDER BY g.id", (rid,)) def respawn(con): now = int(time.time()) dead = q(con, "SELECT s.id, s.mob_id, m.hp AS mhp FROM mud_spawn s " "JOIN mud_mobs m ON m.id=s.mob_id WHERE s.alive=0 AND s.next_respawn<=?", (now,)) for d in dead: ex(con, "UPDATE mud_spawn SET alive=1, hp=? WHERE id=?", (d["mhp"], d["id"])) def level_check(con, c): need = c["level"] * 50 if c["xp"] >= need: c["xp"] -= need c["level"] += 1 c["max_hp"] += 8 c["atk"] += 2 c["def"] += 1 c["hp"] = c["max_hp"] ex(con, "UPDATE mud_chars SET level=?,max_hp=?,atk=?,def=?,xp=?,hp=? WHERE id=?", (c["level"], c["max_hp"], c["atk"], c["def"], c["xp"], c["hp"], c["id"])) return "You reached level %d! HP/ATK/DEF increased." % c["level"] return None def fight(con, spawn_id, c): sp = q1(con, "SELECT s.*, m.key_name AS key_name, m.name AS name, m.descr AS descr, " "m.atk AS atk, m.def AS def, m.xp AS xp, m.gold AS gold, m.loot AS loot, " "m.respawn AS respawn FROM mud_spawn s JOIN mud_mobs m ON m.id=s.mob_id " "WHERE s.id=?", (spawn_id,)) if not sp or not sp["alive"]: return ["There is nothing to fight here."] sp = dict(sp) msgs = [] wep = item(con, c["weapon"]) if c["weapon"] else None pDmg = max(1, c["atk"] + (wep["atk"] if wep else 0) - sp["def"] + random.randint(-1, 1)) sp["hp"] -= pDmg msgs.append("You hit %s for %d." % (sp["name"], pDmg)) if sp["hp"] <= 0: c["xp"] += sp["xp"] c["gold"] += sp["gold"] msgs.append("%s dies! +%d xp, +%d gold." % (sp["name"], sp["xp"], sp["gold"])) inv = json.loads(c["inv"] or "[]") for lk in json.loads(sp["loot"] or "[]"): it = item(con, lk) if it: inv.append(lk) msgs.append("You take %s." % it["name"]) c["inv"] = json.dumps(inv) ex(con, "UPDATE mud_spawn SET alive=0, next_respawn=? WHERE id=?", (int(time.time()) + sp["respawn"], sp["id"])) echo(con, c["room_id"], "%s slew %s." % (c["name"], sp["name"])) ex(con, "UPDATE mud_chars SET xp=?,gold=?,inv=? WHERE id=?", (c["xp"], c["gold"], c["inv"], c["id"])) lv = level_check(con, c) if lv: msgs.append(lv) return msgs arm = item(con, c["armor"]) if c["armor"] else None mDmg = max(1, sp["atk"] - (c["def"] + (arm["def"] if arm else 0)) + random.randint(-1, 1)) c["hp"] -= mDmg msgs.append("%s hits you for %d." % (sp["name"], mDmg)) if c["hp"] <= 0: c["hp"] = 0 msgs.append("You have fallen! You wake in the Village Square.") echo(con, c["room_id"], "%s was slain by %s and fades away." % (c["name"], sp["name"])) lost = int(c["gold"] * 0.2) c["gold"] -= lost c["room_id"] = 1 c["hp"] = c["max_hp"] ex(con, "UPDATE mud_chars SET room_id=1,hp=?,gold=?,last_cmd_at=? WHERE id=?", (c["hp"], c["gold"], int(time.time()), c["id"])) else: ex(con, "UPDATE mud_chars SET hp=? WHERE id=?", (c["hp"], c["id"])) return msgs # ------------------------------------------------------------------ UI DIRS = {"n": "North", "s": "South", "e": "East", "w": "West", "u": "Up", "d": "Down"} def show_room(t, con, c): rm = room(con, c["room_id"]) spawns = alive_spawns(con, c["room_id"]) ground = ground_items(con, c["room_id"]) others = players_in_room(con, c["room_id"]) events = q(con, "SELECT text FROM mud_events WHERE room_id=? ORDER BY id DESC LIMIT 5", (c["room_id"],)) t.header("MUD - " + c["name"]) t.line(" " + rm["name"]) t.line(" " + rm["descr"]) if spawns: t.line(" Here:") for s in spawns: t.line(" %s (%d hp)" % (s["descr"], s["hp"])) people = [p for p in others if p["id"] != c["id"]] if people: t.line(" Adventurers here:") for p in people: b = CLASSES.get(p["class"], CLASSES["fighter"])[0] bn = ", bounty %dg" % p["bounty"] if p["bounty"] > 0 else "" t.line(" %s (%s, L%d%s)" % (p["name"], b, p["level"], bn)) if ground: t.line(" On the ground:") for g in ground: t.line(" " + g["name"]) if events: t.line(" You see:") for ev in reversed(events): t.line(" " + ev["text"]) t.rule("-") t.line(" HP %d/%d Lvl %d XP %d Gold %d Bank %d" % ( c["hp"], c["max_hp"], c["level"], c["xp"], c["gold"], c["bank"])) ex(con, "UPDATE mud_chars SET last_cmd_at=? WHERE id=?", (int(time.time()), c["id"])) def mud_menu(t, con, me): c = ensure_char(con, me) show_room(t, con, c) while True: t.line("\n [n/s/e/w/u/d] move [l]ook [k]ill [t]ake ") t.line(" [w]ield [d]rink [i]nventory [sc]ore [Q]uit to main") line = t.ask("\nmud> ").lower().strip() if not line or line.startswith("q"): t.line("Leaving the realm...") return parts = line.split(" ", 1) cmd = parts[0] arg = parts[1].strip() if len(parts) > 1 else "" if cmd in DIRS: rm = room(con, c["room_id"]) if cmd in rm["exits"]: c["room_id"] = rm["exits"][cmd] ex(con, "UPDATE mud_chars SET room_id=? WHERE id=?", (c["room_id"], c["id"])) echo(con, c["room_id"], "%s heads %s." % (c["name"], DIRS[cmd])) else: t.line("You can't go that way.") show_room(t, con, c) continue if cmd in ("l", "look"): show_room(t, con, c) continue if cmd in ("k", "kill", "attack"): if not arg: t.line("Kill what?"); continue spawns = alive_spawns(con, c["room_id"]) hit = None for s in spawns: if s["key_name"].startswith(arg) or arg in s["name"]: hit = s; break if hit: for m in fight(con, hit["id"], c): t.line(" " + m) else: t.line("There is no '%s' here to fight." % arg) show_room(t, con, c) continue if cmd in ("t", "take", "get"): if not arg: t.line("Take what?"); continue ground = ground_items(con, c["room_id"]) got = None for g in ground: if g["key_name"].startswith(arg): got = g; break if got: ex(con, "DELETE FROM mud_ground WHERE id=?", (got["id"],)) inv = json.loads(c["inv"] or "[]") inv.append(got["key_name"]) c["inv"] = json.dumps(inv) ex(con, "UPDATE mud_chars SET inv=? WHERE id=?", (c["inv"], c["id"])) echo(con, c["room_id"], "%s takes %s." % (c["name"], got["name"])) t.line("You take %s." % got["name"]) else: t.line("There is no '%s' here." % arg) show_room(t, con, c) continue if cmd in ("w", "wield", "wear"): if not arg: t.line("Wield what?"); continue inv = json.loads(c["inv"] or "[]") it = None for k in inv: if k.startswith(arg): it = item(con, k); break if not it: t.line("You don't have that."); continue if it["slot"] == "weapon": c["weapon"] = it["key_name"] ex(con, "UPDATE mud_chars SET weapon=? WHERE id=?", (it["key_name"], c["id"])) t.line("You wield %s." % it["name"]) elif it["slot"] == "armor": c["armor"] = it["key_name"] ex(con, "UPDATE mud_chars SET armor=? WHERE id=?", (it["key_name"], c["id"])) t.line("You don %s." % it["name"]) else: t.line("You can't wield that.") show_room(t, con, c) continue if cmd in ("d", "drink", "quaff"): if not arg: t.line("Drink what?"); continue inv = json.loads(c["inv"] or "[]") it = None idx = None for i, k in enumerate(inv): if k.startswith(arg): it = item(con, k); idx = i; break if not it: t.line("You don't have that."); continue if it["slot"] != "potion": t.line("That's not a potion."); continue c["hp"] = min(c["max_hp"], c["hp"] + it["heal"]) inv.pop(idx) c["inv"] = json.dumps(inv) ex(con, "UPDATE mud_chars SET hp=?,inv=? WHERE id=?", (c["hp"], c["inv"], c["id"])) t.line("You drink %s and recover %d hp." % (it["name"], it["heal"])) show_room(t, con, c) continue if cmd in ("i", "inv", "inventory"): inv = json.loads(c["inv"] or "[]") if inv: t.line(" You carry: " + ", ".join(item(con, k)["name"] for k in inv)) else: t.line(" Your pack is empty.") continue if cmd in ("sc", "score", "stats"): t.line(" Level %d | HP %d/%d | ATK %d | DEF %d | XP %d | Gold %d | Bank %d | Kills %d | Deaths %d" % ( c["level"], c["hp"], c["max_hp"], c["atk"], c["def"], c["xp"], c["gold"], c["bank"], c["kills"], c["deaths"])) continue if cmd in ("a", "attack", "murder", "killp"): if not arg: t.line("Attack who?"); continue v = find_player_in_room(con, c["room_id"], arg) if v: if v["id"] == c["id"]: t.line("You can't attack yourself.") else: for m in pvp(con, c, v): t.line(" " + m) else: others = [p for p in players_in_room(con, c["room_id"]) if p["id"] != c["id"]] who = ", ".join(p["name"] for p in others) or "nobody" t.line("There is no '%s' here to fight. Adventurers present: %s." % (arg, who)) show_room(t, con, c) continue if cmd in ("b", "buy"): if not arg: t.line("Buy what? e.g. 'buy steel_sword'"); continue for m in buy(con, c, arg): t.line(" " + m) show_room(t, con, c) continue if cmd in ("bank",): if not arg: t.line("bank to deposit, bank - to withdraw"); continue if arg.startswith("-"): for m in withdraw(con, c, int(arg[1:] or 0)): t.line(" " + m) else: for m in deposit(con, c, int(arg or 0)): t.line(" " + m) show_room(t, con, c) continue if cmd in ("tax",): for m in tax(con, c): t.line(" " + m) show_room(t, con, c) continue if cmd in ("bounty", "hit"): parts = arg.split(" ", 1) if len(parts) < 2 or not parts[1].isdigit(): t.line("bounty "); continue v = find_player_in_room(con, c["room_id"], parts[0]) if v: if v["id"] == c["id"]: t.line("You can't bounty yourself.") else: for m in set_bounty(con, c, v, int(parts[1])): t.line(" " + m) else: t.line("There is no '%s' here to bounty." % parts[0]) show_room(t, con, c) continue if cmd in ("board", "top", "leaderboard"): lb = leaderboard(con, 10) t.line(" Adventurers of renown:") for i, r in enumerate(lb, 1): cn = CLASSES.get(r["class"], CLASSES["fighter"])[0] t.line(" %d. %s (%s) L%d G%d K%d/D%d" % (i, r["name"], cn, r["level"], r["gold"], r["kills"], r["deaths"])) continue t.line(" Unknown command. Try: n/s/e/w/u/d, look, kill , attack ,") t.line(" buy , bank , tax, bounty , board, inv, score.") # ---- RPGBBS-style economy & PvP (mirror of lib/mud.php) ---- def players_in_room(con, rid): return q(con, "SELECT c.*, u.username AS username FROM mud_chars c " "JOIN users u ON u.id=c.user_id WHERE c.room_id=? ORDER BY c.level DESC", (rid,)) def find_player_in_room(con, rid, frag): frag = frag.lower() for p in players_in_room(con, rid): if p["id"] == 0: continue if frag in (p["name"] or "").lower() or frag in (p["username"] or "").lower(): return p return None def shop_list(con): return q(con, "SELECT * FROM mud_items WHERE price>0 ORDER BY price") def buy(con, c, key): it = item(con, key) if not it or it["price"] <= 0: return ["The trader doesn't sell that."] if c["gold"] < it["price"]: return ["You can't afford the %s (%dg)." % (it["name"], it["price"])] c["gold"] -= it["price"] inv = json.loads(c["inv"] or "[]") inv.append(it["key_name"]) c["inv"] = json.dumps(inv) ex(con, "UPDATE mud_chars SET gold=?,inv=? WHERE id=?", (c["gold"], c["inv"], c["id"])) return ["You buy %s for %dg." % (it["name"], it["price"])] def deposit(con, c, amt): amt = max(0, min(amt, c["gold"])) if amt <= 0: return ["You have no gold to bank."] c["gold"] -= amt c["bank"] += amt ex(con, "UPDATE mud_chars SET gold=?,bank=? WHERE id=?", (c["gold"], c["bank"], c["id"])) return ["You deposit %dg. Bank balance: %dg." % (amt, c["bank"])] def withdraw(con, c, amt): amt = max(0, min(amt, c["bank"])) if amt <= 0: return ["Nothing to withdraw."] c["bank"] -= amt c["gold"] += amt ex(con, "UPDATE mud_chars SET gold=?,bank=? WHERE id=?", (c["gold"], c["bank"], c["id"])) return ["You withdraw %dg. You carry %dg." % (amt, c["gold"])] def tax(con, c): due = int(c["gold"] * 0.10) if due <= 0: return ["Sir Joe squints. \"Come back when ye've coins to tithe.\""] c["gold"] -= due ex(con, "UPDATE mud_chars SET gold=? WHERE id=?", (c["gold"], c["id"])) return ["Sir Joe pockets %dg. \"That's the price of civilisation, adventurer.\"" % due] def leaderboard(con, limit=10): return q(con, "SELECT name,class,level,gold,kills,deaths FROM mud_chars " "ORDER BY level DESC, gold DESC LIMIT %d" % limit) def pvp(con, att, dfn): msgs = [] if dfn["id"] == att["id"]: return ["You can't attack yourself."] if dfn["room_id"] != att["room_id"]: return ["They aren't here."] aWep = item(con, att["weapon"])["atk"] if att["weapon"] else 0 dWep = item(con, dfn["weapon"])["atk"] if dfn["weapon"] else 0 aArm = item(con, att["armor"])["def"] if att["armor"] else 0 dArm = item(con, dfn["armor"])["def"] if dfn["armor"] else 0 aDmg = max(1, att["atk"] + aWep - dfn["def"] - dArm + random.randint(-1, 1)) dDmg = max(1, dfn["atk"] + dWep - att["def"] - aArm + random.randint(-1, 1)) att["hp"] -= dDmg dfn["hp"] -= aDmg msgs.append("You strike %s for %d. %s strikes you for %d." % (dfn["name"], aDmg, dfn["name"], dDmg)) if dfn["hp"] <= 0 and att["hp"] > 0: loot = int(dfn["gold"] * 0.5) att["gold"] += loot att["kills"] += 1 dfn["gold"] -= loot dfn["deaths"] += 1 dfn["room_id"] = 1 dfn["hp"] = dfn["max_hp"] bounty = int(dfn["bounty"]) if bounty > 0: att["gold"] += bounty dfn["bounty"] = 0 msgs.append("You collect the %dg bounty on %s!" % (bounty, dfn["name"])) msgs.append("You slay %s! +%dg looted%s" % ( dfn["name"], loot, ", +%dg bounty." % bounty if bounty else ".")) echo(con, att["room_id"], "%s cut down %s in cold blood." % (att["name"], dfn["name"])) ex(con, "UPDATE mud_chars SET gold=?,kills=?,hp=?,room_id=? WHERE id=?", (att["gold"], att["kills"], att["hp"], att["room_id"], att["id"])) ex(con, "UPDATE mud_chars SET gold=?,deaths=?,bounty=?,hp=?,room_id=? WHERE id=?", (dfn["gold"], dfn["deaths"], dfn["bounty"], dfn["hp"], dfn["id"])) ex(con, "DELETE FROM mud_bounties WHERE target_id=?", (dfn["id"],)) return msgs if att["hp"] <= 0 and dfn["hp"] > 0: loot = int(att["gold"] * 0.5) dfn["gold"] += loot dfn["kills"] += 1 att["gold"] -= loot att["deaths"] += 1 att["room_id"] = 1 att["hp"] = att["max_hp"] msgs.append("%s bests you! You lose %dg and wake in the Village Square." % (dfn["name"], loot)) echo(con, att["room_id"], "%s cut down %s." % (dfn["name"], att["name"])) ex(con, "UPDATE mud_chars SET gold=?,deaths=?,hp=?,room_id=? WHERE id=?", (att["gold"], att["deaths"], att["hp"], att["room_id"], att["id"])) ex(con, "UPDATE mud_chars SET gold=?,kills=? WHERE id=?", (dfn["gold"], dfn["kills"], dfn["id"])) return msgs ex(con, "UPDATE mud_chars SET hp=? WHERE id=?", (att["hp"], att["id"])) ex(con, "UPDATE mud_chars SET hp=? WHERE id=?", (dfn["hp"], dfn["id"])) return msgs def set_bounty(con, c, target, amt): if target["id"] == c["id"]: return ["You can't bounty yourself."] if amt <= 0: return ["A bounty needs to be worth something."] if c["gold"] < amt: return ["You can't post a %dg bounty." % amt] c["gold"] -= amt ex(con, "UPDATE mud_chars SET bounty=bounty+?, gold=? WHERE id=?", (amt, c["gold"], target["id"])) ex(con, "INSERT INTO mud_bounties (target_id,by_id,amount,ts) VALUES (?,?,?,?)", (target["id"], c["id"], amt, int(time.time()))) ex(con, "UPDATE mud_chars SET gold=? WHERE id=?", (c["gold"], c["id"])) return ["You post a %dg bounty on %s. The realm will remember." % (amt, target["name"])]