clone

a takuzu puzzle field for agents to explore tile by tile, with a referee and a replayable spectator view

site: daily game ids in MCP, public origin in tool text, day in list_games, engine/data paths from the site dir when bundled

dami committed 2fa72a7
commit
2fa72a7682ee82b3af201a728afd344427fdfb0e
parent
024b417261ee85aaee24e50a63186472e2c5b3b1
author
dami <gazey55@gmail.com>
4 files changed, +22 -7
Msite/lib/engine.ts5++---
Msite/lib/games.ts3+++
Msite/lib/log.ts13++++++++++++-
Msite/lib/mcp.ts8+++++---
M site/lib/engine.ts +2 -3
M site/lib/games.ts +3 -0
M site/lib/log.ts +12 -1
M site/lib/mcp.ts +5 -3
@@ -4,6 +4,7 @@ import { McpServer } from "@modelcontextprotocol/server";
44 import { z } from "zod";55 import { getEntry, getGame, listGames, startGame } from "./games.ts";66 import { type JoinOptions, RefereeError } from "./referee.ts";7+import { publicOrigin } from "./prompt.ts";78 89 /**910 * The client's identity as the transport knows it. On a 2026-07-28-era
@@ -68,7 +69,8 @@ function findImplementation(o: unknown): string | undefined {
6869 const RULES_TEXT =6970 `Rules of a mosaic. The board is a G x G field of binary cells partly covered by tiles. A tile is a shape: the union of rectangles {rects: [lx, ly, w, h]} inside its bounding box at (x0, y0); tiles come in many sizes and shapes (rectangles, L, T, plus). Neighbouring tiles overlap: a shared cell belongs to every tile that covers it and has one value. Cells no tile covers are holes: never playable. Inside every tile, independently, the rules apply per LINE, where a line is a maximal horizontal or vertical run of the tile's cells: each line has as many 0s as 1s; no three equal values are adjacent along a line; no two lines of the same orientation and the same length within one tile are equal. Nothing is enforced across a tile boundary except that shared cells agree. Every tile ALONE (its givens only) has 2..m solutions, m depending on its cell count (2 up to 64 cells, 3 up to 196, 4 above), and its alternatives differ on cells it shares with SOME neighbour. PINNING IS NOT GUARANTEED BY ANY ONE NEIGHBOUR: a solved neighbour fixes the cells it shares, which pins the tile only if the alternatives differ there; a tile can keep 2 or more solutions with one solved neighbour and need another, or a guess. AGENTS: several agents play one field. Call join to get your agent id and your seed tile, a hidden tile at a random spot away from everyone; every reveal, commit and retract carries your agent id. Your frontier is your seed tile while unsolved, plus every unsolved tile sharing a cell with a tile your group solved. When a tile you solve touches a tile solved by another group, you meet: the two territories merge and from then on share one frontier. Tiles are referred to by id; observe {agent: your id} lists your territory, everything within 24 cells of it and your frontier: by design that includes HIDDEN tiles' shapes, cell counts and m before they are revealed (the map is public; only givens and values are earned), each tile with id, x0, y0, w, h, rects, cells, m, status, agent (who solved it) and neighbours; observe {rect} lists a rectangle; full listings exist only on small fields. Givens are hidden until you reveal a tile, and reveal is only accepted on your frontier. Your seed tile has no solved neighbour, so at least one guess is unavoidable there; everywhere else, prefer to expand from a solved tile and use the shared cells. A tile is solved when all of its cells are known (givens, cells fixed by solved neighbours, and correct commits); solving a tile earns its cell count as reward and attributes the tile to you. COMMITS are checked per cell against the true solution: a wrong value is rejected and reported as {id, reason: "wrong", v}, and since cells are binary a rejection is full information: the other value is correct, commit it next. Correct cells are accepted and classified forced or guess. FORCED means the referee's own propagation reaches the value from the cells known in revealed tiles (givens, shared cells, accepted commits): repeatedly, for every empty cell of every revealed tile and each candidate value, the value is excluded on any line through the cell when (run) it would make three equal in a row along that line, in any of the three positions: the two cells before it, the two after it, or one on each side; (balance) the line already holds len/2 cells of that value; (unique) the line is complete except this cell, or is complete except this cell and one more blank, and with this value it would equal another complete line of the same orientation and length in the same tile (with two blanks the balance rule fills the last one identically, so both forms count). A cell with exactly one admissible value is fixed, every fixed cell is re-evaluated, until nothing changes; bifurcation (assume a value, propagate, contradiction excludes it) only to the game's configured depth, default 0. That is single-cell propagation of the three rules; distinct-line counting, pair/gap arguments beyond the three positions above, and cross-line reasoning are NOT applied, so a cell that holds in every solution but needs them is recorded as guess. Commit propagation-forced cells first if you care about that split. Some shared cells may be givens; cells the referee already knows, and holes, are ignored, not rejected. Every write carries expected_revision; on conflict (other agents are writing too), observe again and retry. TRANSPORT: this endpoint serves the 2025-era streamable HTTP protocol statelessly: there is no Mcp-Session-Id (do not expect one), initialize answers as one SSE frame, notifications/initialized returns 202 at once, and long tool calls (start_mosaic no longer blocks) send SSE keepalive comments. GROWING FIELDS AND FOG: a field started with a grow policy is allocated on a large canvas (G up to 1000) with the initial chunks in the middle, and it GROWS: on a timer, when most current tiles are revealed or solved, and whenever every current tile is solved, the referee generates a ring of new chunks around the cluster (a grow event; tile ids continue, existing cells never change). Observe again after each batch and expect new tile ids; the game ends only when a growth finds no room. Under FOG (on by default for growing fields) observe lists a hidden tile only once your group reaches it: a tile you revealed or solved overlaps it, or it is your seed tile; everything else is unlisted until then, so expanding toward open space is how you discover the map. Writing code to do the deduction is allowed; where to expand (toward open space, or toward another agent to merge frontiers) and how much to commit is your strategy.`;7071 71-const gameIdSchema = z.string().uuid();72+// a uuid for games agents start, `day-<date>` for the daily73+const gameIdSchema = z.string().regex(/^[a-zA-Z0-9_-]{1,80}$/);7274 7375 function textResult(text: string, structuredContent: Record<string, unknown>) {7476 return { content: [{ type: "text" as const, text }], structuredContent };
@@ -109,7 +111,7 @@ export function createServer(): McpServer {
109111 {110112 title: "Start a mosaic",111113 description:112- `Create a new field and open it in the spectator at http://127.0.0.1:8000/ . Returns at once with status "generating": the field is generated in the background (a G=60 scatter takes ~1 min, G=100 ~3 min); poll observe {game_id} or list_games every 10-20 s until the status is playing, then join. Do not start another field while one is generating; games nobody joins expire after 30 min. Defaults: kind=scatter with G=100, a field of tiles of random shapes and sizes scattered with overlaps and holes; kind=field with chunks=3 (chunk_g=60) is a larger field generated chunk by chunk; kind=grid with n=14 and G=27 is the classic 2x2 of 14x14 squares (G-1 must be a multiple of n-1). Starting does not seat you: call join next. Then play autonomously with observe, reveal, commit, retract until status is solved. ${RULES_TEXT}`,114+ `Create a new field and open it in the spectator at ${publicOrigin()}/ . Returns at once with status "generating": the field is generated in the background (a G=60 scatter takes ~1 min, G=100 ~3 min); poll observe {game_id} or list_games every 10-20 s until the status is playing, then join. Do not start another field while one is generating; games nobody joins expire after 30 min. Defaults: kind=scatter with G=100, a field of tiles of random shapes and sizes scattered with overlaps and holes; kind=field with chunks=3 (chunk_g=60) is a larger field generated chunk by chunk; kind=grid with n=14 and G=27 is the classic 2x2 of 14x14 squares (G-1 must be a multiple of n-1). Starting does not seat you: call join next. Then play autonomously with observe, reveal, commit, retract until status is solved. ${RULES_TEXT}`,113115 inputSchema: z.object({114116 G: z.number().int().min(6).max(1000).default(100).describe(115117 "Cells per side of the field. For kind=grid, G-1 must be a multiple of n-1 (27 for a 2x2 of 14x14).",
@@ -181,7 +183,7 @@ export function createServer(): McpServer {
181183 status: "generating",182184 kind,183185 G,184- spectator_url: "http://127.0.0.1:8000/",186+ spectator_url: publicOrigin() + "/",185187 },186188 );187189 } catch (error) {