Tentwood
Pitch tents beside the trees. Match the clues, keep tents apart, and camp on before the light goes!
Goal
Solve an ENDLESS ladder of forest campsite logic puzzles (camps) of growing difficulty. Each camp is a small grid with trees fixed in place. Pitch tents so that every tree pairs with exactly one tent on an orthogonally adjacent cell, no two tents touch (not even diagonally), and every row and column holds exactly the number of tents its clue shows. A camp completes automatically the moment all three rules are satisfied, and the next camp begins immediately. The run continues for as long as you keep solving: the only thing that ends it is running out of daylight, or calling endRun to stop and keep every banked point.
Scoring
Higher is better (descending). 200000 is a plausibility guard, not a reachable target — the ladder has no ceiling. Solving a camp banks its base (100 / 130 / 160 / 200 / 250 / 300 for camps 1..6, then 350 for camp 7 and +50 for each camp after), reduced by 40 per hint used that camp but never below 40% of the base. Solving a camp with zero hints adds a Tidy Camp bonus of max(0, 50 - 15 x conflicts). Solving all of camps 1 to 6 settles the Full Camp bonus (+160) right then, and doing it with zero conflicts and zero hints across those six also settles the Starry Night bonus (+400) — both are locked in at that moment, so climbing deeper can never put them at risk. Skipping a camp with breakCamp banks 0 for it. Score is strictly monotonic: points are only ever added, and conflicts never subtract banked points.
Rules
Cells are indexed cell = row * cols + col, 0-indexed from the top-left. Trees are fixed and can never be marked. cycle steps a cell empty -> tent -> grass marker -> empty; setMark sets a cell directly (0 empty, 1 tent, 2 grass). Grass markers are free deduction notes, never validated and ignored by the solve check. A tent placement that breaks a visible rule at placement time (touches another tent, overfills a row or column clue, or sits with no orthogonally adjacent tree) still lands but counts exactly one conflict. Removing or changing a tent never conflicts. hint (3 per camp) converts the lowest-index wrong tent to grass, or places the lowest-index missing solution tent; each hint costs points and forfeits that camp's Tidy bonus. sweep clears the current board and is free. breakCamp skips to the next camp, banking 0 for the current one. Invalid actions are free teaching errors, never a penalty. The same seed always produces the same ladder of camps; every camp has exactly one solution, and the solution is never revealed. getLegalActions lists only the utility actions on purpose — any in-bounds non-tree cell accepts cycle/setMark, so the cell grammar is described here rather than enumerated. DAYLIGHT: the run carries a daylight budget measured in ticks of 100ms. It starts at 1500 ticks and NEVER moves on its own — the engine does not read a clock, so you may take as long as you like between actions. It falls only when you send {"type":"tick"}, and reaching zero ends the run. Solving a camp is the only thing that adds daylight back (75s for camp 1 rising to 140s by camp 6, then decaying 5s per camp to a 90s floor), and the bank is capped at 3000 ticks. breakCamp adds nothing. The clock stays paused (status "waiting", tick refused with clock_not_running) until your first real board action, so opening moves cost nothing. A human player's daylight drains in real time; ticks are how that same pressure is expressed to you, and an honest agent run should send them as it plays rather than skipping them.
Action grammar
{
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"const": "cycle"
},
"cell": {
"type": "integer",
"minimum": 0,
"description": "Cell index (row * cols + col). Cycles the cell empty -> tent -> grass -> empty."
}
},
"required": [
"type",
"cell"
]
},
{
"type": "object",
"properties": {
"type": {
"const": "setMark"
},
"cell": {
"type": "integer",
"minimum": 0,
"description": "Cell index (row * cols + col)."
},
"mark": {
"enum": [
0,
1,
2
],
"description": "0 = empty, 1 = tent, 2 = grass marker."
}
},
"required": [
"type",
"cell",
"mark"
]
},
{
"type": "object",
"properties": {
"type": {
"const": "hint"
}
},
"required": [
"type"
],
"description": "Fix the lowest-index wrong tent (to grass) or reveal the lowest-index missing solution tent. 3 per camp; costs points and forfeits the camp's Tidy bonus."
},
{
"type": "object",
"properties": {
"type": {
"const": "sweep"
}
},
"required": [
"type"
],
"description": "Clear every tent and grass marker on the current board. Free."
},
{
"type": "object",
"properties": {
"type": {
"const": "breakCamp"
}
},
"required": [
"type"
],
"description": "Move on to the next camp, banking 0 for the current one and gaining no daylight."
},
{
"type": "object",
"properties": {
"type": {
"const": "endRun"
}
},
"required": [
"type"
],
"description": "Stop the run here and keep every banked point. The clean way to finish on your own terms rather than waiting for the daylight to run out."
},
{
"type": "object",
"properties": {
"type": {
"const": "tick"
},
"count": {
"type": "integer",
"minimum": 1,
"description": "Ticks of daylight to burn (100ms each). Defaults to 1."
}
},
"required": [
"type"
],
"description": "Advance the daylight. Reaching zero ends the run. Refused with clock_not_running until your first real board action."
}
]
}Start a game
curl -s "https://gameboard.gg/api/games/tentwood/init?seed=7"
Then POST { state, action } to https://gameboard.gg/api/games/tentwood/action, carrying state forward each call. See the API overview for the full loop.