First git commit

This commit is contained in:
root
2026-09-08 18:21:10 +01:00
commit 66911dc450
65 changed files with 7012 additions and 0 deletions

View File

@ -0,0 +1,74 @@
<?php
// Single player: guess the number 1-100, state persisted in sp_games.
require_once __DIR__ . '/../lib/bootstrap.php';
$me = require_login();
function guess_load(int $uid): ?array {
$s = db()->prepare("SELECT * FROM sp_games WHERE user_id=? AND game='guess'
AND status='active' ORDER BY id DESC LIMIT 1");
$s->execute([$uid]);
return $s->fetch() ?: null;
}
function guess_new(int $uid): array {
$st = json_encode(['n' => random_int(1, 100), 'tries' => 0, 'log' => []]);
$now = time();
db()->prepare("INSERT INTO sp_games (game,user_id,state,created_at,updated_at)
VALUES ('guess',?,?,?,?)")->execute([$uid, $st, $now, $now]);
return guess_load($uid);
}
$msg = ''; $done = false;
$g = guess_load((int)$me['id']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf();
if (($_POST['act'] ?? '') === 'new') {
if ($g) db()->prepare("UPDATE sp_games SET status='abandoned' WHERE id=?")->execute([$g['id']]);
$g = guess_new((int)$me['id']);
$msg = 'New game! Guess a number between 1 and 100.';
} else {
if (!$g) $g = guess_new((int)$me['id']);
$st = json_decode($g['state'], true);
$gv = (int)($_POST['guess'] ?? 0);
if ($gv < 1 || $gv > 100) {
$msg = 'Enter a number from 1 to 100.';
} else {
$st['tries']++;
$st['log'][] = $gv;
if ($gv === (int)$st['n']) {
$score = max(1, 110 - 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 ('guess',?,?,?,?)")
->execute([$me['id'], $score, $st['tries'] . ' tries', time()]);
$msg = "Correct! $gv was the number, in {$st['tries']} tries. Score: $score";
$done = true; $g = null;
} else {
$msg = $gv . ' is too ' . ($gv < (int)$st['n'] ? 'LOW' : 'HIGH')
. '. Tries: ' . $st['tries'];
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('Guess the Number', ['back' => '/games.php']);
if ($msg) p_para($msg);
if ($g) {
$st = json_decode($g['state'], true);
if ($st['log']) p_para('Previous: ' . implode(', ', array_slice($st['log'], -8)));
p_form('/games/guess.php', [
['name' => 'guess', 'label' => 'Your guess (1-100)', 'maxlength' => 3,
'format' => '*N'],
], 'Guess');
} else {
if (!$done) p_para('Guess the secret number between 1 and 100. Fewer tries = more points.');
}
p_form('/games/guess.php', [
['name' => 'act', 'type' => 'hidden', 'value' => 'new'],
], $g ? 'Restart' : 'New game');
p_links([['/games/scores.php', 'High scores', ['g' => 'guess']], ['/games.php', 'Games']]);
page_end();

View File

@ -0,0 +1,82 @@
<?php
// Shared helpers for turn-based multiplayer games stored in `matches`.
require_once __DIR__ . '/../lib/bootstrap.php';
function mp_create(string $game, int $uid, string $state): int {
$now = time();
db()->prepare("INSERT INTO matches (game,p1,turn,state,status,created_at,updated_at)
VALUES (?,?,1,?, 'open',?,?)")
->execute([$game, $uid, $state, $now, $now]);
return (int)db()->lastInsertId();
}
function mp_join(string $game, int $mid, int $uid): bool {
$s = db()->prepare("SELECT * FROM matches WHERE id=? AND game=?");
$s->execute([$mid, $game]);
$m = $s->fetch();
if (!$m || $m['status'] !== 'open' || (int)$m['p1'] === $uid) return false;
db()->prepare("UPDATE matches SET p2=?, status='playing', updated_at=? WHERE id=?")
->execute([$uid, time(), $mid]);
return true;
}
function mp_get(string $game, int $mid): ?array {
$s = db()->prepare("SELECT m.*, a.username AS n1, b.username AS 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=?");
$s->execute([$mid, $game]);
return $s->fetch() ?: null;
}
function mp_save(int $mid, string $state, int $turn, string $status = 'playing', ?int $winner = null): void {
db()->prepare("UPDATE matches SET state=?, turn=?, status=?, winner=?, updated_at=? WHERE id=?")
->execute([$state, $turn, $status, $winner, time(), $mid]);
}
// Which seat is this user in? 1, 2, or 0 for spectator.
function mp_seat(array $m, int $uid): int {
if ((int)$m['p1'] === $uid) return 1;
if ((int)$m['p2'] === $uid) return 2;
return 0;
}
// Render the lobby: open games to join, your active games, and a create button.
function mp_lobby(string $game, string $self, int $uid, string $newState): void {
$d = db();
$s = $d->prepare("SELECT m.*, a.username AS 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");
$s->execute([$game, $uid]);
$open = $s->fetchAll();
$s = $d->prepare("SELECT m.*, a.username AS n1, b.username AS 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");
$s->execute([$game, $uid, $uid]);
$mine = $s->fetchAll();
if ($mine) {
p_para('Your games:');
foreach ($mine as $m) {
$opp = ((int)$m['p1'] === $uid) ? ($m['n2'] ?? 'waiting...') : ($m['n1'] ?? '?');
$yourTurn = ($m['status'] === 'playing' && mp_seat($m, $uid) === (int)$m['turn']);
p_link($self, '#' . $m['id'] . ' vs ' . $opp . ($yourTurn ? ' - YOUR TURN' : ''),
['g' => $m['id']]);
}
}
if ($open) {
p_para('Open games to join:');
foreach ($open as $m) {
p_link($self, 'Join #' . $m['id'] . ' by ' . ($m['n1'] ?? '?'), ['g' => $m['id'], 'join' => 1]);
}
}
if (!$mine && !$open) p_para('No games yet - create one and wait for an opponent.');
p_form($self, [
['name' => 'act', 'type' => 'hidden', 'value' => 'create'],
], 'Create new game');
}

95
public_html/games/nim.php Normal file
View File

@ -0,0 +1,95 @@
<?php
// Multiplayer Nim: 21 sticks, take 1-3, whoever takes the last stick loses.
require_once __DIR__ . '/mplib.php';
$me = require_login();
$uid = (int)$me['id'];
$SELF = '/games/nim.php';
$msg = '';
$mid = (int)($_GET['g'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf();
$act = $_POST['act'] ?? '';
if ($act === 'create') {
$mid = mp_create('nim', $uid, json_encode(['sticks' => 21]));
redirect($SELF, ['g' => $mid]);
}
if ($act === 'take') {
$mid = (int)($_POST['mid'] ?? 0);
$m = mp_get('nim', $mid);
$seat = $m ? mp_seat($m, $uid) : 0;
if (!$m || $m['status'] !== 'playing') $msg = 'Game is not in play.';
elseif ($seat === 0) $msg = 'You are not in this game.';
elseif ($seat !== (int)$m['turn']) $msg = 'Not your turn.';
else {
$st = json_decode($m['state'], true);
$n = (int)($_POST['n'] ?? 0);
if ($n < 1 || $n > 3 || $n > (int)$st['sticks']) {
$msg = 'Take 1 to 3 sticks (and no more than remain).';
} else {
$st['sticks'] -= $n;
if ($st['sticks'] <= 0) {
// taker of the last stick loses -> the other seat wins
$winnerId = ($seat === 1) ? (int)$m['p2'] : (int)$m['p1'];
mp_save($mid, json_encode($st), $seat, 'done', $winnerId);
db()->prepare("INSERT INTO scores (game,user_id,score,detail,created_at)
VALUES ('nim',?,?,?,?)")
->execute([$winnerId, 40, 'win', time()]);
$msg = 'You took the last stick - you lose!';
} else {
mp_save($mid, json_encode($st), $seat === 1 ? 2 : 1);
$msg = 'You took ' . $n . '. ' . $st['sticks'] . ' left.';
}
}
}
}
}
if ($mid && !empty($_GET['join'])) {
$msg = mp_join('nim', $mid, $uid) ? 'You joined the game.' : 'Could not join that game.';
}
page_start('Nim - 21 sticks', ['back' => '/games.php']);
if ($msg) p_para($msg);
if (!$mid) {
p_para('21 sticks. Players alternate taking 1, 2 or 3. Take the LAST stick and you lose.');
mp_lobby('nim', $SELF, $uid, json_encode(['sticks' => 21]));
p_links([['/games.php', 'Games']]);
page_end();
exit;
}
$m = mp_get('nim', $mid);
if (!$m) bail('Game', 'No such game.', $SELF);
$st = json_decode($m['state'], true);
$seat = mp_seat($m, $uid);
$left = (int)$st['sticks'];
p_para('#' . $m['id'] . ': ' . ($m['n1'] ?? '?') . ' vs ' . ($m['n2'] ?? 'waiting'));
p_para('Sticks left: ' . $left);
if ($left > 0) p_para(str_repeat('|', min($left, 21)));
p_rule();
if ($m['status'] === 'open') {
p_para('Waiting for an opponent to join.');
} elseif ($m['status'] === 'done') {
$wn = ((int)$m['winner'] === (int)$m['p1']) ? $m['n1'] : $m['n2'];
p_para('Winner: ' . ($wn ?? '?'));
} elseif ($seat === 0) {
p_para('Spectating. Turn: player ' . $m['turn']);
} elseif ($seat === (int)$m['turn']) {
p_para('Your turn. Take how many?');
p_form($SELF . '?g=' . $mid, [
['name' => 'act', 'type' => 'hidden', 'value' => 'take'],
['name' => 'mid', 'type' => 'hidden', 'value' => (string)$mid],
['name' => 'n', 'label' => 'Take', 'type' => 'select',
'options' => ['1' => '1 stick', '2' => '2 sticks', '3' => '3 sticks']],
], 'Take');
} else {
p_para('Opponent to move.');
}
p_links([[$SELF, 'Refresh', ['g' => $mid]], [$SELF, 'Lobby'], ['/games.php', 'Games']]);
page_end();

View File

@ -0,0 +1,82 @@
<?php
// Single player: 5-question multiple choice quiz, one question per deck.
require_once __DIR__ . '/../lib/bootstrap.php';
$me = require_login();
$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, the first big WAP phone?', ['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],
['Default MIME type for WML is text/vnd.wap...?', ['wap','wml','xml','wsp'], 1],
['GPRS stands for General Packet Radio...?', ['System','Service','Standard','Stream'], 1],
];
function quiz_load(int $uid): ?array {
$s = db()->prepare("SELECT * FROM sp_games WHERE user_id=? AND game='quiz'
AND status='active' ORDER BY id DESC LIMIT 1");
$s->execute([$uid]);
return $s->fetch() ?: null;
}
$msg = '';
$g = quiz_load((int)$me['id']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf();
$act = $_POST['act'] ?? '';
if ($act === 'new' || !$g) {
if ($g) db()->prepare("UPDATE sp_games SET status='abandoned' WHERE id=?")->execute([$g['id']]);
$keys = array_keys($QUESTIONS);
shuffle($keys);
$keys = array_slice($keys, 0, 5);
$st = json_encode(['q' => $keys, 'i' => 0, 'right' => 0]);
$now = time();
db()->prepare("INSERT INTO sp_games (game,user_id,state,created_at,updated_at)
VALUES ('quiz',?,?,?,?)")->execute([$me['id'], $st, $now, $now]);
$g = quiz_load((int)$me['id']);
$msg = 'New quiz: 5 questions.';
} elseif ($act === 'ans') {
$st = json_decode($g['state'], true);
$qi = $QUESTIONS[$st['q'][$st['i']]];
$pick = (int)($_POST['a'] ?? -1);
if ($pick === (int)$qi[2]) { $st['right']++; $msg = 'Correct!'; }
else $msg = 'Wrong - it was: ' . $qi[1][$qi[2]];
$st['i']++;
if ($st['i'] >= count($st['q'])) {
$score = $st['right'] * 20;
db()->prepare("UPDATE sp_games SET status='done', 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 ('quiz',?,?,?,?)")
->execute([$me['id'], $score, $st['right'] . '/5', time()]);
$msg .= ' Quiz over: ' . $st['right'] . '/5 correct, score ' . $score . '.';
$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('Quick Quiz', ['back' => '/games.php']);
if ($msg) p_para($msg);
if ($g) {
$st = json_decode($g['state'], true);
$qi = $QUESTIONS[$st['q'][$st['i']]];
p_para('Q' . ($st['i'] + 1) . '/' . count($st['q']) . ': ' . $qi[0]);
$opts = [];
foreach ($qi[1] as $k => $lab) $opts[(string)$k] = $lab;
p_form('/games/quiz.php', [
['name' => 'act', 'type' => 'hidden', 'value' => 'ans'],
['name' => 'a', 'label' => 'Answer', 'type' => 'select', 'options' => $opts],
], 'Answer');
} else {
p_para('A short quiz about WAP and mobile history. 20 points per correct answer.');
p_form('/games/quiz.php', [['name' => 'act', 'type' => 'hidden', 'value' => 'new']], 'Start quiz');
}
p_links([['/games/scores.php', 'High scores', ['g' => 'quiz']], ['/games.php', 'Games']]);
page_end();

View File

@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../lib/bootstrap.php';
$games = ['guess' => 'Guess the Number', 'quiz' => 'Quick Quiz',
'ttt' => 'Noughts & Crosses', 'nim' => 'Nim'];
$g = (string)($_GET['g'] ?? '');
page_start('High scores', ['back' => '/games.php']);
if (!isset($games[$g])) {
p_para('Pick a game:');
$links = [];
foreach ($games as $k => $n) $links[] = ['/games/scores.php', $n, ['g' => $k]];
p_links($links);
} else {
p_para($games[$g] . ' - top 10:');
$s = db()->prepare("SELECT u.username, MAX(s.score) sc, COUNT(*) n
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 10");
$s->execute([$g]);
$rows = $s->fetchAll();
if (!$rows) p_para('No scores yet.');
$i = 1;
foreach ($rows as $r) {
p_para($i++ . '. ' . $r['username'] . ' - ' . $r['sc'] . ' (' . $r['n'] . ' plays)');
}
p_links([['/games/scores.php', 'Other games']]);
}
p_links([['/games.php', 'Games'], ['/index.php', 'Home']]);
page_end();

116
public_html/games/ttt.php Normal file
View File

@ -0,0 +1,116 @@
<?php
// Multiplayer noughts & crosses.
require_once __DIR__ . '/mplib.php';
$me = require_login();
$uid = (int)$me['id'];
$SELF = '/games/ttt.php';
function ttt_win(array $b): ?string {
$lines = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
foreach ($lines as $l) {
if ($b[$l[0]] !== '' && $b[$l[0]] === $b[$l[1]] && $b[$l[1]] === $b[$l[2]]) return $b[$l[0]];
}
return null;
}
$msg = '';
$mid = (int)($_GET['g'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf();
$act = $_POST['act'] ?? '';
if ($act === 'create') {
$mid = mp_create('ttt', $uid, json_encode(['b' => array_fill(0, 9, '')]));
redirect($SELF, ['g' => $mid]);
}
if ($act === 'move') {
$mid = (int)($_POST['mid'] ?? 0);
$m = mp_get('ttt', $mid);
$seat = $m ? mp_seat($m, $uid) : 0;
if (!$m || $m['status'] !== 'playing') $msg = 'Game is not in play.';
elseif ($seat === 0) $msg = 'You are not in this game.';
elseif ($seat !== (int)$m['turn']) $msg = 'Not your turn.';
else {
$st = json_decode($m['state'], true);
$cell = (int)($_POST['cell'] ?? 0) - 1; // players type 1-9
if ($cell < 0 || $cell > 8 || $st['b'][$cell] !== '') {
$msg = 'That square is not free (pick 1-9).';
} else {
$mark = $seat === 1 ? 'X' : 'O';
$st['b'][$cell] = $mark;
$w = ttt_win($st['b']);
$full = !in_array('', $st['b'], true);
if ($w !== null) {
mp_save($mid, json_encode($st), $seat, 'done', $uid);
db()->prepare("INSERT INTO scores (game,user_id,score,detail,created_at)
VALUES ('ttt',?,?,?,?)")->execute([$uid, 50, 'win', time()]);
$msg = 'You win!';
} elseif ($full) {
mp_save($mid, json_encode($st), $seat, 'done', null);
$msg = 'Draw!';
} else {
mp_save($mid, json_encode($st), $seat === 1 ? 2 : 1);
$msg = 'Move played. Waiting for your opponent.';
}
}
}
}
}
if ($mid && !empty($_GET['join'])) {
if (mp_join('ttt', $mid, $uid)) $msg = 'You joined the game. Player 2 is O.';
else $msg = 'Could not join that game.';
}
page_start('Noughts & Crosses', ['back' => '/games.php']);
if ($msg) p_para($msg);
if (!$mid) {
mp_lobby('ttt', $SELF, $uid, json_encode(['b' => array_fill(0, 9, '')]));
p_links([['/games.php', 'Games']]);
page_end();
exit;
}
$m = mp_get('ttt', $mid);
if (!$m) bail('Game', 'No such game.', $SELF);
$st = json_decode($m['state'], true);
$b = $st['b'];
$seat = mp_seat($m, $uid);
p_para('#' . $m['id'] . ': X=' . ($m['n1'] ?? '?') . ' vs O=' . ($m['n2'] ?? 'waiting'));
// board: 3 rows, free cells show their number so WAP users know what to type
for ($r = 0; $r < 3; $r++) {
$cells = [];
for ($c = 0; $c < 3; $c++) {
$i = $r * 3 + $c;
$cells[] = $b[$i] === '' ? (string)($i + 1) : $b[$i];
}
p_para(implode(' | ', $cells));
}
p_rule();
if ($m['status'] === 'open') {
p_para('Waiting for an opponent to join. Tell a friend to open Games > Noughts & Crosses.');
} elseif ($m['status'] === 'done') {
if ($m['winner'] === null) p_para('Result: draw.');
else {
$wn = ((int)$m['winner'] === (int)$m['p1']) ? $m['n1'] : $m['n2'];
p_para('Winner: ' . $wn);
}
} elseif ($seat === 0) {
p_para('You are watching. Turn: player ' . $m['turn']);
} elseif ($seat === (int)$m['turn']) {
p_para('Your turn (' . ($seat === 1 ? 'X' : 'O') . '). Enter a free square number.');
p_form($SELF . '?g=' . $mid, [
['name' => 'act', 'type' => 'hidden', 'value' => 'move'],
['name' => 'mid', 'type' => 'hidden', 'value' => (string)$mid],
['name' => 'cell', 'label' => 'Square (1-9)', 'maxlength' => 1, 'format' => 'N'],
], 'Play');
} else {
p_para('Opponent to move. Check back shortly.');
}
p_links([[$SELF, 'Refresh', ['g' => $mid]], [$SELF, 'Lobby'], ['/games.php', 'Games']]);
page_end();