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:
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
|
||||
require_once __DIR__ . '/../lib/bootstrap.php';
|
||||
$games = ['guess' => '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']);
|
||||
|
||||
Reference in New Issue
Block a user