From 9473d853cedb5d6970ba7676746a6e033568711a Mon Sep 17 00:00:00 2001 From: jamie prince Date: Thu, 10 Sep 2026 02:09:09 +0100 Subject: [PATCH] Add Mastermind and Hangman single-player games - Mastermind: 4-color sequence, 10 tries, black/white peg feedback - Hangman: 6 lives, progressive letter reveal, 10-word vocabulary - Both available in BBS (telnet) and web (WML/XHTML) sides - Shared sp_games persistence, scores table integration - Updated games index and menu navigation --- bbs/games.py | 363 ++++++++----------------------- public_html/games.php | 2 + public_html/games/hangman.php | 98 +++++++++ public_html/games/mastermind.php | 110 ++++++++++ public_html/games/scores.php | 2 +- 5 files changed, 303 insertions(+), 272 deletions(-) create mode 100644 public_html/games/hangman.php create mode 100644 public_html/games/mastermind.php diff --git a/bbs/games.py b/bbs/games.py index a8c229f..e2e8a25 100644 --- a/bbs/games.py +++ b/bbs/games.py @@ -27,7 +27,9 @@ def games_menu(t, con, me): 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(" [5] Mastermind (single player)") + t.line(" [6] Hangman (single player)") + t.line(" [7] High scores") t.line(" [Q] Back to main") c = t.ask("\ngames> ").lower() if c == "1": @@ -39,6 +41,10 @@ def games_menu(t, con, me): elif c == "4": nim(t, con, me) elif c == "5": + mastermind(t, con, me) + elif c == "6": + hangman(t, con, me) + elif c == "7": scores(t, con) elif c.startswith("q") or c == "": return @@ -49,275 +55,90 @@ def add_score(con, game, uid, score, detail=""): " VALUES (?,?,?,?,?)", (game, uid, score, detail, int(time.time()))) + +# ------------------------------------------------------------------ mastermind +def mastermind(t, con, me): + # Mastermind: 4 pegs, 6 colors, 10 tries + colors = ['1', '2', '3', '4', '5', '6'] # represent colors as digits + secret = [random.choice(colors) for _ in range(4)] + tries = 0 + max_tries = 10 + t.header("MASTERMIND") + t.line(" I have selected a sequence of 4 colors from 1-6.") + t.line(" You have 10 tries to guess the sequence.") + t.line(" Feedback: Black = correct color and position, White = correct color only.") + t.line(" Enter your guess as 4 digits (e.g. 1234) or Q to quit.") + while tries < max_tries: + s = t.ask("\n guess> ", maxlen=10) + if s.lower().startswith("q"): + t.line(" The code was %s." % ''.join(secret)) + return + if len(s) != 4 or not all(c in colors for c in s): + t.line(" Enter exactly 4 digits from 1-6.") + continue + guess = list(s) + tries += 1 + # Calculate black and white pegs + black = sum(1 for i in range(4) if guess[i] == secret[i]) + # Count occurrences of each color in secret and guess for white pegs + secret_counts = {c: secret.count(c) for c in set(secret)} + guess_counts = {c: guess.count(c) for c in set(guess)} + white = sum(min(secret_counts.get(c, 0), guess_counts.get(c, 0)) for c in set(secret)) - black + if black == 4: + score = max(1, 100 - 10 * tries) + add_score(con, "mastermind", me["id"], score, "%d tries" % tries) + t.line(" Correct! %d in %d tries. Score %d." % (''.join(secret), tries, score)) + return + else: + t.line(" Feedback: %d Black, %d White. (tries: %d)" % (black, white, tries)) + t.line(" You ran out of tries. The code was %s." % ''.join(secret)) + + +def hangman(t, con, me): + # Hangman: word guessing game, 6 lives + import string + WORDS = ["python", "bbs", "telnet", "wml", "matrix", "linux", "mastermind", "screens", "forum", "games"] + word = random.choice(WORDS) + word_letters = set(word) + alphabet = set(string.ascii_lowercase) + used_letters = set() + lives = 6 + t.header("HANGMAN") + t.line("I am thinking of a word. It has %d letters." % len(word)) + t.line("You have %d lives." % lives) + while len(word_letters) > 0 and lives > 0: + # What current word is (with dashes for unguessed letters) + word_list = [letter if letter in used_letters else "-" for letter in word] + t.line("Current word: " + " ".join(word_list)) + t.line("Lives left: %d" % lives) + t.line("Used letters: " + " ".join(sorted(used_letters))) + # User input + t.line("Guess a letter:") + s = t.ask("> ").lower() + if len(s) != 1 or s not in alphabet: + t.line("Please enter a single letter from a-z.") + continue + # If user has already guessed this letter + if s in used_letters: + t.line("You have already used that letter. Please try again.") + continue + # Add letter to used + used_letters.add(s) + # Check if letter is in word + if s in word_letters: + word_letters.discard(s) + t.line("Good! %s is in the word." % s) + else: + lives -= 1 + t.line("%s is not in the word." % s) + # Game over + if lives == 0: + t.line("You died! The word was %s." % word) + else: + t.line("Yay! You guessed the word %s!" % word) + score = max(1, 50 - (6 - lives) * 5) + add_score(con, "hangman", me["id"], score, "%d lives remaining" % lives) + 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"])) + "ttt": "Noughts & Crosses", "nim": "Nim", "mastermind": "Mastermind", "hangman": "Hangman"} diff --git a/public_html/games.php b/public_html/games.php index b70a07e..35e1ce2 100644 --- a/public_html/games.php +++ b/public_html/games.php @@ -7,6 +7,8 @@ p_para('Single player:'); p_links([ ['/games/guess.php', 'Guess the Number'], ['/games/quiz.php', 'Quick Quiz'], + ['/games/mastermind.php', 'Mastermind'], + ['/games/hangman.php', 'Hangman'], ]); p_para('Multiplayer (play a friend):'); p_links([ diff --git a/public_html/games/hangman.php b/public_html/games/hangman.php new file mode 100644 index 0000000..f6ea0ab --- /dev/null +++ b/public_html/games/hangman.php @@ -0,0 +1,98 @@ +prepare("SELECT * FROM sp_games WHERE user_id=? AND game='hangman' + AND status='active' ORDER BY id DESC LIMIT 1"); + $s->execute([$uid]); + return $s->fetch() ?: null; +} +function h_new(int $uid): array { + $word = H_WORDS[array_rand(H_WORDS)]; + $st = json_encode(['word' => $word, 'used' => [], 'lives' => H_MAX_LIVES]); + $now = time(); + db()->prepare("INSERT INTO sp_games (game,user_id,state,created_at,updated_at) + VALUES ('hangman',?,?,?,?)")->execute([$uid, $st, $now, $now]); + return h_load($uid); +} + +$msg = ''; $done = false; +$g = h_load((int)$me['id']); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + require_csrf(); + $act = $_POST['act'] ?? ''; + if ($act === 'new') { + if ($g) db()->prepare("UPDATE sp_games SET status='abandoned' WHERE id=?")->execute([$g['id']]); + $g = h_new((int)$me['id']); + $msg = 'New game! I have chosen a word. Guess letters one at a time.'; + } else { + if (!$g) $g = h_new((int)$me['id']); + $st = json_decode($g['state'], true); + $letter = strtolower((string)($_POST['letter'] ?? '')); + if (strlen($letter) !== 1 || !ctype_alpha($letter)) { + $msg = 'Please enter a single letter a-z.'; + } elseif (in_array($letter, $st['used'], true)) { + $msg = 'You have already used that letter.'; + } else { + $st['used'][] = $letter; + if (in_array($letter, str_split($st['word']), true)) { + $msg = 'Good! ' . $letter . ' is in the word.'; + } else { + $st['lives']--; + $msg = $letter . ' is not in the word.'; + } + // Check win/loss + $word_letters = array_diff(str_split($st['word']), $st['used']); + if (empty($word_letters)) { + $score = max(1, 50 - (H_MAX_LIVES - $st['lives']) * 5); + db()->prepare("UPDATE sp_games SET status='won', state=?, updated_at=? WHERE id=?") + ->execute([json_encode($st), time(), $g['id']]); + db()->prepare("INSERT INTO scores (game,user_id,score,detail,created_at) + VALUES ('hangman',?,?,?,?)") + ->execute([$me['id'], $score, $st['lives'] . ' lives left', time()]); + $msg = "Yay! You guessed the word {$st['word']}! Score: $score"; + $done = true; $g = null; + } elseif ($st['lives'] <= 0) { + db()->prepare("UPDATE sp_games SET status='lost', state=?, updated_at=? WHERE id=?") + ->execute([json_encode($st), time(), $g['id']]); + $msg = "You died! The word was {$st['word']}."; + $done = true; $g = null; + } else { + db()->prepare("UPDATE sp_games SET state=?, updated_at=? WHERE id=?") + ->execute([json_encode($st), time(), $g['id']]); + $g['state'] = json_encode($st); + } + } + } +} + +page_start('Hangman', ['back' => '/games.php']); +if ($msg) p_para($msg); +if ($g) { + $st = json_decode($g['state'], true); + $display = ''; + foreach (str_split($st['word']) as $ch) { + $display .= (in_array($ch, $st['used'], true) ? $ch : '-') . ' '; + } + p_para('Word: ' . trim($display)); + p_para('Lives left: ' . $st['lives']); + if ($st['used']) p_para('Used: ' . implode(', ', $st['used'])); + p_form('/games/hangman.php', [ + ['name' => 'letter', 'label' => 'Guess a letter', 'maxlength' => 1, + 'format' => '*A'], + ], 'Guess'); +} else { + if (!$done) p_para('Guess the word one letter at a time. You have 6 lives.'); +} +p_form('/games/hangman.php', [ + ['name' => 'act', 'type' => 'hidden', 'value' => 'new'], +], $g ? 'Restart' : 'New game'); +p_links([['/games/scores.php', 'High scores', ['g' => 'hangman']], ['/games.php', 'Games']]); +page_end(); \ No newline at end of file diff --git a/public_html/games/mastermind.php b/public_html/games/mastermind.php new file mode 100644 index 0000000..3805558 --- /dev/null +++ b/public_html/games/mastermind.php @@ -0,0 +1,110 @@ +prepare("SELECT * FROM sp_games WHERE user_id=? AND game='mastermind' + AND status='active' ORDER BY id DESC LIMIT 1"); + $s->execute([$uid]); + return $s->fetch() ?: null; +} +function mm_new(int $uid): array { + $secret = []; + foreach (range(0, 3)) $secret[] = MM_COLORS[array_rand(MM_COLORS)]; + $st = json_encode(['secret' => $secret, 'tries' => 0, 'log' => []]); + $now = time(); + db()->prepare("INSERT INTO sp_games (game,user_id,state,created_at,updated_at) + VALUES ('mastermind',?,?,?,?)")->execute([$uid, $st, $now, $now]); + return mm_load($uid); +} + +function mm_feedback(array $secret, array $guess): array { + $black = 0; + $sCounts = []; + $gCounts = []; + for ($i = 0; $i < 4; $i++) { + if ($guess[$i] === $secret[$i]) $black++; + $sCounts[$secret[$i]] = ($sCounts[$secret[$i]] ?? 0) + 1; + $gCounts[$guess[$i]] = ($gCounts[$guess[$i]] ?? 0) + 1; + } + $white = 0; + foreach ($sCounts as $c => $n) { + $white += min($n, $gCounts[$c] ?? 0); + } + return [$black, $white - $black]; +} + +$msg = ''; $done = false; +$g = mm_load((int)$me['id']); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + require_csrf(); + $act = $_POST['act'] ?? ''; + if ($act === 'new') { + if ($g) db()->prepare("UPDATE sp_games SET status='abandoned' WHERE id=?")->execute([$g['id']]); + $g = mm_new((int)$me['id']); + $msg = 'New game! I have chosen a sequence of 4 colors from 1-6.'; + } else { + if (!$g) $g = mm_new((int)$me['id']); + $st = json_decode($g['state'], true); + $raw = (string)($_POST['guess'] ?? ''); + if (strlen($raw) !== 4 || !ctype_digit($raw) || !array_reduce(str_split($raw), function($carry, $c) { return $carry && in_array($c, MM_COLORS, true); }, true)) { + $msg = 'Enter exactly 4 digits from 1-6 (e.g. 1234).'; + } else { + $guess = str_split($raw); + $st['tries']++; + $st['log'][] = $guess; + list($black, $white) = mm_feedback($st['secret'], $guess); + if ($black === 4) { + $score = max(1, 100 - 10 * (int)$st['tries']); + db()->prepare("UPDATE sp_games SET status='won', state=?, updated_at=? WHERE id=?") + ->execute([json_encode($st), time(), $g['id']]); + db()->prepare("INSERT INTO scores (game,user_id,score,detail,created_at) + VALUES ('mastermind',?,?,?,?)") + ->execute([$me['id'], $score, $st['tries'] . ' tries', time()]); + $msg = "Correct! " . implode('', $st['secret']) . " in {$st['tries']} tries. Score: $score"; + $done = true; $g = null; + } elseif ($st['tries'] >= MM_MAX_TRIES) { + db()->prepare("UPDATE sp_games SET status='lost', state=?, updated_at=? WHERE id=?") + ->execute([json_encode($st), time(), $g['id']]); + $msg = "Out of tries. The code was " . implode('', $st['secret']) . '.'; + $done = true; $g = null; + } else { + db()->prepare("UPDATE sp_games SET state=?, updated_at=? WHERE id=?") + ->execute([json_encode($st), time(), $g['id']]); + $g['state'] = json_encode($st); + $msg = "Feedback: $black Black, $white White. Tries: {$st['tries']}."; + } + } + } +} + +page_start('Mastermind', ['back' => '/games.php']); +if ($msg) p_para($msg); +if ($g) { + $st = json_decode($g['state'], true); + if ($st['log']) { + $rows = []; + foreach (array_slice($st['log'], -5) as $lg) { + $rows[] = implode('', $lg); + } + p_para('Recent guesses: ' . implode(', ', $rows)); + } + p_para('Guess a 4-digit code (digits 1-6).'); + p_form('/games/mastermind.php', [ + ['name' => 'guess', 'label' => 'Your guess (e.g. 1234)', 'maxlength' => 4, + 'format' => '*N'], + ], 'Guess'); +} else { + if (!$done) p_para('Guess the secret 4-color sequence. Black = right color and position, White = right color only.'); +} +p_form('/games/mastermind.php', [ + ['name' => 'act', 'type' => 'hidden', 'value' => 'new'], +], $g ? 'Restart' : 'New game'); +p_links([['/games/scores.php', 'High scores', ['g' => 'mastermind']], ['/games.php', 'Games']]); +page_end(); \ No newline at end of file diff --git a/public_html/games/scores.php b/public_html/games/scores.php index 28c3e44..3338c62 100644 --- a/public_html/games/scores.php +++ b/public_html/games/scores.php @@ -1,7 +1,7 @@ 'Guess the Number', 'quiz' => 'Quick Quiz', - 'ttt' => 'Noughts & Crosses', 'nim' => 'Nim']; + 'ttt' => 'Noughts & Crosses', 'nim' => 'Nim', 'mastermind' => 'Mastermind']; $g = (string)($_GET['g'] ?? ''); page_start('High scores', ['back' => '/games.php']);