"""Games for the BBS. Shares `scores`, `sp_games` and `matches` with the web site, so a telnet user and a WAP user can play each other in the same match.""" import json import random import time from bbsdb import q, q1, ex, ago QUESTIONS = [ ("What does WAP stand for?", ["Wireless Application Protocol", "Web Access Point", "Wide Area Paging", "Wireless Audio Player"], 0), ("WML is based on which language?", ["HTML", "XML", "SGML", "JSON"], 1), ("Which company made the Nokia 7110?", ["Ericsson", "Motorola", "Nokia", "Siemens"], 2), ("A WML file is made up of one or more...", ["Pages", "Cards", "Frames", "Slides"], 1), ("What year was WAP 1.1 published?", ["1996", "1999", "2002", "2005"], 1), ("GPRS stands for General Packet Radio...?", ["System", "Service", "Standard", "Stream"], 1), ] def games_menu(t, con, me): while True: t.header("GAMES") t.line(" [1] Guess the Number (single player)") t.line(" [2] Quick Quiz (single player)") t.line(" [3] Noughts & Crosses (multiplayer)") t.line(" [4] Nim - 21 sticks (multiplayer)") t.line(" [5] High scores") t.line(" [Q] Back to main") c = t.ask("\ngames> ").lower() if c == "1": guess(t, con, me) elif c == "2": quiz(t, con, me) elif c == "3": ttt(t, con, me) elif c == "4": nim(t, con, me) elif c == "5": scores(t, con) elif c.startswith("q") or c == "": return def add_score(con, game, uid, score, detail=""): ex(con, "INSERT INTO scores (game,user_id,score,detail,created_at)" " VALUES (?,?,?,?,?)", (game, uid, score, detail, int(time.time()))) def scores(t, con): names = {"guess": "Guess the Number", "quiz": "Quick Quiz", "ttt": "Noughts & Crosses", "nim": "Nim"} t.header("HIGH SCORES") for g, label in names.items(): rows = q(con, "SELECT u.username, MAX(s.score) sc FROM scores s " "JOIN users u ON u.id=s.user_id WHERE s.game=? " "GROUP BY s.user_id ORDER BY sc DESC LIMIT 5", (g,)) t.line("\n " + label + ":") if not rows: t.line(" (none yet)") for i, r in enumerate(rows, 1): t.line(" %d. %-14s %d" % (i, r["username"][:14], r["sc"])) t.pause() # ------------------------------------------------------------------ guess def guess(t, con, me): n = random.randint(1, 100) tries = 0 t.header("GUESS THE NUMBER") t.line(" I picked a number from 1 to 100. Fewer guesses = more points.") t.line(" Type Q to give up.") while True: s = t.ask("\n guess> ", maxlen=4) if s.lower().startswith("q"): t.line(" The number was %d." % n) return if not s.isdigit(): t.line(" Enter a number 1-100.") continue g = int(s) if not 1 <= g <= 100: t.line(" Enter a number 1-100.") continue tries += 1 if g == n: score = max(1, 110 - 10 * tries) add_score(con, "guess", me["id"], score, "%d tries" % tries) t.line(" Correct! %d in %d tries. Score %d." % (n, tries, score)) return t.line(" Too %s. (tries: %d)" % ("LOW" if g < n else "HIGH", tries)) # ------------------------------------------------------------------ quiz def quiz(t, con, me): qs = random.sample(QUESTIONS, 5) right = 0 t.header("QUICK QUIZ") for i, (question, opts, ans) in enumerate(qs, 1): t.line("\n Q%d/5: %s" % (i, question)) for j, o in enumerate(opts): t.line(" %d) %s" % (j + 1, o)) s = t.ask(" answer> ", maxlen=2) pick = int(s) - 1 if s.isdigit() else -1 if pick == ans: right += 1 t.line(" Correct!") else: t.line(" Wrong - it was: %s" % opts[ans]) score = right * 20 add_score(con, "quiz", me["id"], score, "%d/5" % right) t.line("\n Final: %d/5 correct, score %d." % (right, score)) t.pause() # ------------------------------------------------- multiplayer plumbing def seat_of(m, uid): if m["p1"] == uid: return 1 if m["p2"] == uid: return 2 return 0 def lobby(t, con, me, game, label, new_state): """Shared lobby: returns a match id to play, or None to go back.""" while True: mine = q(con, "SELECT m.*, a.username n1, b.username n2 FROM matches m " "LEFT JOIN users a ON a.id=m.p1 LEFT JOIN users b ON b.id=m.p2 " "WHERE m.game=? AND (m.p1=? OR m.p2=?) " "AND m.status IN ('open','playing') " "ORDER BY m.updated_at DESC LIMIT 10", (game, me["id"], me["id"])) open_ = q(con, "SELECT m.*, a.username n1 FROM matches m " "LEFT JOIN users a ON a.id=m.p1 " "WHERE m.game=? AND m.status='open' AND m.p1<>? " "ORDER BY m.id DESC LIMIT 10", (game, me["id"])) t.header(label + " - LOBBY") t.line(" Your games:") if not mine: t.line(" (none)") for r in mine: opp = (r["n2"] or "waiting...") if r["p1"] == me["id"] else (r["n1"] or "?") yours = (r["status"] == "playing" and seat_of(r, me["id"]) == r["turn"]) t.line(" #%d vs %-12s %s" % (r["id"], opp[:12], "<< YOUR TURN" if yours else "")) t.line(" Open games to join:") if not open_: t.line(" (none)") for r in open_: t.line(" #%d by %s" % (r["id"], r["n1"] or "?")) t.line("\n [C]reate J = join # = play [Q]back") c = t.ask("\nlobby> ").lower().strip() if c.startswith("c"): now = int(time.time()) mid = ex(con, "INSERT INTO matches (game,p1,turn,state,status," "created_at,updated_at) VALUES (?,?,1,?,'open',?,?)", (game, me["id"], new_state, now, now)) t.line(" Created game #%d - waiting for an opponent." % mid) elif c.startswith("j"): digits = "".join(ch for ch in c if ch.isdigit()) if digits: mid = int(digits) m = q1(con, "SELECT * FROM matches WHERE id=? AND game=?", (mid, game)) if not m or m["status"] != "open" or m["p1"] == me["id"]: t.line(" Cannot join that game.") else: ex(con, "UPDATE matches SET p2=?, status='playing', updated_at=?" " WHERE id=?", (me["id"], int(time.time()), mid)) t.line(" Joined #%d." % mid) return mid elif c.startswith("#") or c.isdigit(): digits = "".join(ch for ch in c if ch.isdigit()) if digits: return int(digits) elif c.startswith("q") or c == "": return None def get_match(con, game, mid): return q1(con, "SELECT m.*, a.username n1, b.username n2 FROM matches m " "LEFT JOIN users a ON a.id=m.p1 LEFT JOIN users b ON b.id=m.p2 " "WHERE m.id=? AND m.game=?", (mid, game)) def save_match(con, mid, state, turn, status="playing", winner=None): ex(con, "UPDATE matches SET state=?,turn=?,status=?,winner=?,updated_at=?" " WHERE id=?", (state, turn, status, winner, int(time.time()), mid)) # ------------------------------------------------------------------ ttt def ttt_winner(b): for a, c, d in [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]: if b[a] and b[a] == b[c] == b[d]: return b[a] return None def ttt(t, con, me): mid = lobby(t, con, me, "ttt", "NOUGHTS & CROSSES", json.dumps({"b": [""] * 9})) if mid is None: return while True: m = get_match(con, "ttt", mid) if not m: t.line(" No such game.") return st = json.loads(m["state"]) b = st["b"] seat = seat_of(m, me["id"]) t.header("#%d X=%s O=%s" % (mid, m["n1"] or "?", m["n2"] or "waiting")) for r in range(3): cells = [b[r*3+c] if b[r*3+c] else str(r*3+c+1) for c in range(3)] t.line(" %s | %s | %s" % tuple(cells)) if r < 2: t.line(" ---+---+---") if m["status"] == "open": t.line("\n Waiting for an opponent to join.") t.line(" [R]efresh [Q]back") elif m["status"] == "done": if m["winner"] is None: t.line("\n Result: draw.") else: t.line("\n Winner: %s" % (m["n1"] if m["winner"] == m["p1"] else m["n2"])) t.pause() return elif seat == 0: t.line("\n Spectating. Turn: player %d" % m["turn"]) t.line(" [R]efresh [Q]back") elif seat == m["turn"]: t.line("\n Your turn (%s). Enter a free square 1-9." % ("X" if seat == 1 else "O")) else: t.line("\n Opponent to move. [R]efresh [Q]back") c = t.ask("\nttt> ").lower().strip() if c.startswith("q"): return if c.startswith("r") or c == "": continue if m["status"] != "playing" or seat == 0 or seat != m["turn"]: t.line(" Not your move.") continue if not c.isdigit() or not 1 <= int(c) <= 9: t.line(" Pick 1-9.") continue i = int(c) - 1 if b[i]: t.line(" That square is taken.") continue b[i] = "X" if seat == 1 else "O" st["b"] = b w = ttt_winner(b) if w: save_match(con, mid, json.dumps(st), seat, "done", me["id"]) add_score(con, "ttt", me["id"], 50, "win") t.line(" You win!") elif all(b): save_match(con, mid, json.dumps(st), seat, "done", None) t.line(" Draw.") else: save_match(con, mid, json.dumps(st), 2 if seat == 1 else 1) t.line(" Move played.") # ------------------------------------------------------------------ nim def nim(t, con, me): mid = lobby(t, con, me, "nim", "NIM - 21 STICKS", json.dumps({"sticks": 21})) if mid is None: return while True: m = get_match(con, "nim", mid) if not m: t.line(" No such game.") return st = json.loads(m["state"]) left = st["sticks"] seat = seat_of(m, me["id"]) t.header("#%d %s vs %s" % (mid, m["n1"] or "?", m["n2"] or "waiting")) t.line(" " + "| " * left) t.line(" %d sticks left. Take the LAST stick and you LOSE." % left) if m["status"] == "open": t.line("\n Waiting for an opponent. [R]efresh [Q]back") elif m["status"] == "done": t.line("\n Winner: %s" % (m["n1"] if m["winner"] == m["p1"] else m["n2"])) t.pause() return elif seat == 0: t.line("\n Spectating. [R]efresh [Q]back") elif seat == m["turn"]: t.line("\n Your turn - take 1, 2 or 3.") else: t.line("\n Opponent to move. [R]efresh [Q]back") c = t.ask("\nnim> ").lower().strip() if c.startswith("q"): return if c.startswith("r") or c == "": continue if m["status"] != "playing" or seat == 0 or seat != m["turn"]: t.line(" Not your move.") continue if c not in ("1", "2", "3") or int(c) > left: t.line(" Take 1-3, and no more than remain.") continue n = int(c) st["sticks"] = left - n if st["sticks"] <= 0: winner = m["p2"] if seat == 1 else m["p1"] save_match(con, mid, json.dumps(st), seat, "done", winner) add_score(con, "nim", winner, 40, "win") t.line(" You took the last stick - you LOSE!") else: save_match(con, mid, json.dumps(st), 2 if seat == 1 else 1) t.line(" You took %d. %d left." % (n, st["sticks"]))