324 lines
12 KiB
Python
324 lines
12 KiB
Python
"""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 <id> = join #<id> = 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"]))
|