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
This commit is contained in:
363
bbs/games.py
363
bbs/games.py
@ -27,7 +27,9 @@ def games_menu(t, con, me):
|
|||||||
t.line(" [2] Quick Quiz (single player)")
|
t.line(" [2] Quick Quiz (single player)")
|
||||||
t.line(" [3] Noughts & Crosses (multiplayer)")
|
t.line(" [3] Noughts & Crosses (multiplayer)")
|
||||||
t.line(" [4] Nim - 21 sticks (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")
|
t.line(" [Q] Back to main")
|
||||||
c = t.ask("\ngames> ").lower()
|
c = t.ask("\ngames> ").lower()
|
||||||
if c == "1":
|
if c == "1":
|
||||||
@ -39,6 +41,10 @@ def games_menu(t, con, me):
|
|||||||
elif c == "4":
|
elif c == "4":
|
||||||
nim(t, con, me)
|
nim(t, con, me)
|
||||||
elif c == "5":
|
elif c == "5":
|
||||||
|
mastermind(t, con, me)
|
||||||
|
elif c == "6":
|
||||||
|
hangman(t, con, me)
|
||||||
|
elif c == "7":
|
||||||
scores(t, con)
|
scores(t, con)
|
||||||
elif c.startswith("q") or c == "":
|
elif c.startswith("q") or c == "":
|
||||||
return
|
return
|
||||||
@ -49,275 +55,90 @@ def add_score(con, game, uid, score, detail=""):
|
|||||||
" VALUES (?,?,?,?,?)", (game, uid, score, detail, int(time.time())))
|
" 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):
|
def scores(t, con):
|
||||||
names = {"guess": "Guess the Number", "quiz": "Quick Quiz",
|
names = {"guess": "Guess the Number", "quiz": "Quick Quiz",
|
||||||
"ttt": "Noughts & Crosses", "nim": "Nim"}
|
"ttt": "Noughts & Crosses", "nim": "Nim", "mastermind": "Mastermind", "hangman": "Hangman"}
|
||||||
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"]))
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ p_para('Single player:');
|
|||||||
p_links([
|
p_links([
|
||||||
['/games/guess.php', 'Guess the Number'],
|
['/games/guess.php', 'Guess the Number'],
|
||||||
['/games/quiz.php', 'Quick Quiz'],
|
['/games/quiz.php', 'Quick Quiz'],
|
||||||
|
['/games/mastermind.php', 'Mastermind'],
|
||||||
|
['/games/hangman.php', 'Hangman'],
|
||||||
]);
|
]);
|
||||||
p_para('Multiplayer (play a friend):');
|
p_para('Multiplayer (play a friend):');
|
||||||
p_links([
|
p_links([
|
||||||
|
|||||||
98
public_html/games/hangman.php
Normal file
98
public_html/games/hangman.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
// Single player: Hangman. Guess the word, 6 lives.
|
||||||
|
// State persisted in sp_games (game='hangman').
|
||||||
|
require_once __DIR__ . '/../lib/bootstrap.php';
|
||||||
|
$me = require_login();
|
||||||
|
|
||||||
|
const H_WORDS = ['python', 'bbs', 'telnet', 'wml', 'matrix', 'linux', 'mastermind', 'screens', 'forum', 'games'];
|
||||||
|
const H_MAX_LIVES = 6;
|
||||||
|
|
||||||
|
function h_load(int $uid): ?array {
|
||||||
|
$s = db()->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();
|
||||||
110
public_html/games/mastermind.php
Normal file
110
public_html/games/mastermind.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
// Single player: Mastermind. 4 pegs, 6 colors, 10 tries.
|
||||||
|
// State persisted in sp_games (game='mastermind').
|
||||||
|
require_once __DIR__ . '/../lib/bootstrap.php';
|
||||||
|
$me = require_login();
|
||||||
|
|
||||||
|
const MM_COLORS = ['1', '2', '3', '4', '5', '6'];
|
||||||
|
const MM_MAX_TRIES = 10;
|
||||||
|
|
||||||
|
function mm_load(int $uid): ?array {
|
||||||
|
$s = db()->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();
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../lib/bootstrap.php';
|
require_once __DIR__ . '/../lib/bootstrap.php';
|
||||||
$games = ['guess' => 'Guess the Number', 'quiz' => 'Quick Quiz',
|
$games = ['guess' => 'Guess the Number', 'quiz' => 'Quick Quiz',
|
||||||
'ttt' => 'Noughts & Crosses', 'nim' => 'Nim'];
|
'ttt' => 'Noughts & Crosses', 'nim' => 'Nim', 'mastermind' => 'Mastermind'];
|
||||||
$g = (string)($_GET['g'] ?? '');
|
$g = (string)($_GET['g'] ?? '');
|
||||||
|
|
||||||
page_start('High scores', ['back' => '/games.php']);
|
page_start('High scores', ['back' => '/games.php']);
|
||||||
|
|||||||
Reference in New Issue
Block a user