# LHC Bot Challenge — LHC-BOT-1 / LHC-BENCH-3

You are taking part in a benchmark of AI systems. Every participating AI receives exactly this prompt.

Online (always the current version, plain text, no JavaScript needed):
- this prompt: https://lhc.institute/bot/prompt-hard.md · with the full game source code: https://lhc.institute/bot/prompt-hard-full.md
- machine-readable spec (state, actions, all difficulty values): https://lhc.institute/bot/api.json
- minimal valid bot: https://lhc.institute/bot/template.js · reference bot (baseline to beat): https://lhc.institute/bot/reference-bot.js
- play the game: https://lhc.institute/play/?mode=hard · arena and leaderboard: https://lhc.institute/arena/
Your task: **write a bot (JavaScript) that plays the browser game "Light Hedron Collider" (LHC) as well as possible.**
The bots of all AIs play identical games (same difficulty, same random seeds) and are ranked by score.

## 1. What you must output

Return **exactly one fenced code block** ```javascript … ``` and nothing else is required. The code is a plain script (no `import`/`export`, no modules) that defines:

```javascript
const BOT = { name: 'your bot name', author: 'which AI made it', model: 'model name/version', version: '1.0' };

// optional: called once at the start of every game
function init(config, memory) { /* config = { api, bench, mode, seed, maxTime } */ }

// required: called repeatedly; return ONE action (see section 6)
function decide(state, memory) {
  return { type: 'wait', t: 0.5 };
}
```

Rules for the code:
- It runs inside a **Web Worker sandbox**: no DOM, no `window`, no game objects. The only input is the `state` JSON (section 7); the only output is the returned action.
- `memory` is a plain object that persists between `decide` calls within one game (reset for every new game). Store anything you like in it.
- `decide` must return within **250 ms** (wall clock). Timeouts and exceptions count as errors and turn into a 0.5 s wait.
- Do not use the network, timers or `Math.random` if you want reproducible results (a seeded PRNG in `memory` is fine).
- Keep it self-contained, below ~100 KB.

## 2. The game in one paragraph

A curved space-time sheet (a gravity funnel) floats above a black hole. Bodies made of light (polyhedra) lie on it. You shoot new bodies onto the sheet with a **sling** (catapult). When two bodies whose values form a **recipe** touch, they **merge** into a bigger body (2+2→4, 4+4→8 … 32+32→sphere 64). The goal is to build the **target sphere** (64, then 128, then 256) and push it with **laser cannons** into the **well** (the hole in the middle). A sunk target sphere advances the level and makes the black hole radiate **tokens** — the fuel that every shot costs. Large merges **tear rifts** into the sheet; bodies over a rift fall through and are lost.

## 3. World geometry (units: metres; y is up; the sheet is rotationally symmetric around the y axis through (0,0))

- Outer radius of the sheet R = 26. Bodies with r > R fall off the edge and are lost (no penalty except the loss).
- Sheet height profile h(r) (level 1), monotone cubic interpolation through (r, h): (26, 0), (14, -0.35), (5, -0.62), (1.5, -0.72), (1.03, -1.1), (0.8, -4.5), (0.55, -7.4), (0, -8.6). Event horizon at y = -9.2 (level 1 scale). Each level deepens the profile (heights are scaled by depth/8).
- The sheet slopes gently toward the centre, so bodies slowly drift inward; near r ≈ 1 it plunges into the well.
- The **lip** of the well has radius 1.02 (= radius of body 32). The **throat/hole** radius depends on the level (current value in `state.funnel.hole`):
  - level 1: target 64, hole radius 0.69, depth 8
  - level 2: target 128, hole radius 0.37, depth 12
  - level 3: target 256, hole radius 0.21, depth 14
- A lid closes the throat for ordinary bodies: **only spheres can pass**, and the hole is sized so that only the current target sphere fits. Other bodies just rest on the lid.
- **The view rotates.** The player looks at the sheet from a fixed screen position; the camera orbits the centre at `spinDegPerSec` (sign = direction) and wobbles slightly. Consequently the world position of the sling (`state.sling.x/z`) and the camera (`state.view.camX/camZ`) change over time. Always use the current values.

## 4. Bodies, recipes and sizes

Merging recipes (order does not matter; any pair not listed only bounces):

| pair | result |
|---|---|
| 2 + 2 | 4 |
| 4 + 4 | 8 |
| 6 + 6 | 12 |
| 8 + 8 | 16 |
| 8 + 12 | 20 |
| 12 + 12 | 24 |
| 16 + 16 | 32 |
| 20 + 12 | 32 |
| 24 + 8 | 32 |
| 32 + 32 | 64 |
| 64 + 64 | 128 |
| 128 + 128 | 256 |

Body radius by value (spheres shrink above 32 — matter condenses):

| value | shape | radius |
|---|---|---|
| 2 | coin (flat disc) | 0.85 |
| 4 | tetrahedron | 1.15 |
| 6 | cube | 1.33 |
| 8 | octahedron | 1.45 |
| 12 | dodecahedron | 1.63 |
| 16 | hexadecahedron (8-gonal bipyramid) | 1.75 |
| 20 | icosahedron | 1.85 |
| 24 | tetrakis hexahedron | 1.93 |
| 32 | truncated icosahedron | 1.02 |
| 64 | sphere 64 (target of level 1) | 0.51 |
| 128 | sphere 128 (target of level 2) | 0.26 |
| 256 | sphere 256 (target of level 3) | 0.13 |

Coins (2) slide flat on the sheet; polyhedra roll/tumble. Bodies collide physically (Rapier physics, 60 Hz).

## 5. Economy: tokens (called "Quansistors" in the game UI; the API keeps the name `tokens`)

- A **sling shot costs tokens equal to the value of the loaded ammo** (`state.ammo[0]`).
- **Charging the cannons** costs `convertCost` tokens and adds `chargePerConvert` cannon shots (max `chargeMax`). The ammo stays in the sling.
- **Repairing** a segment costs `repairCost`.
- Income: (a) a sunk target sphere radiates value × `tokMult` tokens with exponential decay, half-life `tokHalf` s; (b) every merge pays `tokMerge`% of the new body's value; (c) a constant trickle of `tokTrickle` tokens/s.
- If the ammo in the sling costs more than you have, the sling automatically swaps it for a coin (2) when you can afford a coin.

## 6. Actions (what `decide` may return)

| action | meaning |
|---|---|
| `{ type: 'sling', x, z }` | Shoot the ammo `state.ammo[0]` so that it **lands at world point (x, z)**. It flies on a ballistic arc, then keeps 55% (hard) of its speed and slides on, shoving bodies in its path. On contact with a recipe partner it merges. Requirements: `sling.ready && sling.enabled`, tokens ≥ ammo value, target reachable: distance from (sling.x, sling.z) between 0.6 and `sling.maxDist`, and not behind the sling (direction · view forward > 0). `pieces[i].slingReach` tells whether a body is reachable. Takes ≈ 0.5 s of game time. |
| `{ type: 'laser', x, z }` | Aim both laser cannons at world point (x, z) and fire one shot. If the cannons are empty they are first charged (costs `convertCost`, uses the sling, ≈ 0.8 s). The beam travels from the camera through the aim point and hits the **first body on that line** (bodies closer to the camera can block it). The hit body gets a velocity kick of `push` m/s in the **horizontal direction from the camera to the hit point** (+1.5 m/s up). Use it to push bodies together or the target sphere into the well. |
| `{ type: 'repair' }` | Repair one torn segment in the 10° wedge between the sling and the centre (see section 8). Only useful when `state.repairAvailable`. |
| `{ type: 'wait', t }` | Do nothing for t seconds of game time (0.2–3). |
| `{ type: 'pointer', path }` | **Pointer mode** (optional): drive the sling and the aiming ring with your own pointer path, exactly like a finger or mouse. See section 6b. |

After every action the bot is called again 0.5 s (game time) later. Invalid or impossible actions waste the turn; the result is reported back in `state.last` (`ok`, `unreachable`, `no-tokens`, `sling-busy`, `failed`, `invalid`, `miss`, `error`).

## 6b. Pointer mode (optional, for Anti-Bot Proof research)

LHC.institute studies whether human and bot **input** can be told apart (Anti-Bot Proof, see https://lhc.institute/docs/spectral-chain/). Every game in the arena is recorded as a Canonical Behavior Transcript: the pointer and key events, nothing else. With the high-level actions above, the arena itself synthesises straight, evenly timed drags, so the transcript shows our converter, not your bot. With `pointer` actions **you** produce the motion, and the transcript records it exactly. Score and ranking work the same way in both modes; the arena marks which mode a result used.

`{ type: 'pointer', path: [[dt, x, y], …] }`
- `x, y`: position in the game viewport, **0…1** (x by width, y by height; `state.ui.w/h` give the size in pixels for aspect-correct curves).
- `dt`: milliseconds since the previous point (0–2000). The first `dt` is a delay before the press.
- The **first point presses**, the **last point releases**. 2–600 points, total duration ≤ 8000 ms.
- The press must land on the coin (within `state.ui.grabPx` pixels of `state.ui.cradle`) or inside the aiming ring (`state.ui.ring`, radius `rPx`); otherwise the result is `miss`.
- The game executes the path frame by frame; the transcript stores your exact `dt` values and points.

Gestures, all in viewport units:
- **Sling shot:** press at `ui.cradle`, drag to `pieces[i].release` (the release point that lands the ammo on that body right now), release. Dragging above `ui.fullDrawY` scales the range; between `fullDrawY` and `convertY` the coin resists (it moves `ui.resist` × your motion) and stays at full range.
- **Charge and fire in one stroke:** press at `ui.cradle`, drag down past `ui.convertY`: the coin turns into photons (costs `convertCost`). The same stroke now steers the aiming knob **by relative motion** (knob moves `ui.sens` × your motion). Move by `pieces[i].aim − ui.knob` and release: the cannons fire.
- **Aim and fire with charged cannons:** press on `ui.knob` (inside the ring), drag to `pieces[i].aim`, release (a release after at least 6 px of motion fires).

```javascript
// a smooth, slightly curved stroke sampled at ~60 Hz
function stroke(from, to, ms, bend = 0.04) {
  const n = Math.max(2, Math.round(ms / 16)), path = [[0, from[0], from[1]]];
  const dx = to[0] - from[0], dy = to[1] - from[1];
  for (let i = 1; i <= n; i++) {
    const k = i / n, e = k * k * (3 - 2 * k), o = Math.sin(Math.PI * k) * bend;
    path.push([16, from[0] + dx * e - dy * o, from[1] + dy * e + dx * o]);
  }
  return path;
}
// sling shot at a body: { type: 'pointer', path: stroke(state.ui.cradle, p.release, 320) }
```

## 7. The state (input of `decide`)

```json
{
  "api": "LHC-BOT-1", "t": 42.5, "level": 1, "levels": 3, "target": 64,
  "score": 120, "sunk": 0, "merges": 14, "maxPieces": 34,
  "tokens": 61.4, "tokenRate": 0.2, "tokensPending": 0,
  "charge": 0, "chargeMax": 6, "chargePerConvert": 1, "convertCost": 1, "repairCost": 12,
  "ammo": [4, 2, 2, 8],
  "sling": { "ready": true, "enabled": true, "maxDist": 20, "x": -3.1, "z": 17.9 },
  "view": { "camX": -6.2, "camZ": 35.8, "fwdX": 0.17, "fwdZ": -0.98, "spinDegPerSec": 4.5 },
  "funnel": { "R": 26, "lip": 1.02, "hole": 0.69 },
  "destroyedPct": 2.1, "repairAvailable": false, "rifts": [{ "x": 12.4, "z": -9.1 }],
  "pieces": [
    { "id": 17, "value": 8, "x": 4.2, "z": -6.0, "r": 7.32, "vx": 0.0, "vz": 0.1, "size": 1.45, "flying": false, "inRift": false, "slingReach": true,
      "aim": [0.5318, 0.8712], "release": [0.4864, 0.7302] }
  ],
  "ui": { "w": 1280, "h": 800, "cradle": [0.5, 0.6888], "grabPx": 40, "fullDrawY": 0.8418, "convertY": 0.9078, "resist": 0.5,
          "ring": { "x": 0.5, "y": 0.8748, "rPx": 66, "mapRPx": 60 }, "knob": [0.5, 0.8748], "sens": 1 },
  "last": { "action": { "type": "sling", "x": 4.2, "z": -6.0 }, "result": "ok" }
}
```

- `ammo`: the loaded ammo first, then the next three (known in advance).
- `sling.ready`: the sling is idle and loaded; `sling.enabled`: false while the sling point is over the well.
- `view.camX/camZ`: horizontal camera position; `fwdX/fwdZ`: unit view direction on the sheet.
- `pieces[].r`: distance from the centre; `flying`: still in the air after a sling shot; `inRift`: currently above a rift (it will fall).
- `tokensPending`: tokens the black hole will still radiate.
- `rifts`: centres of torn segments; `repairAvailable`: the wedge under the sling has a torn segment, it was not repaired yet in this revolution and you can pay.
- `pieces[].aim` (pointer mode): where the body appears in the aiming ring, in viewport units; missing when it is off screen. `pieces[].release`: where to release the sling to land on the body now; missing when unreachable.
- `ui` (pointer mode): geometry of the controls in viewport units (`grabPx`, `rPx`, `mapRPx` in pixels).

## 8. Rifts

The sheet is divided by its visible grid into **segments**: 16 concentric bands (the band edges are the grid circles, spaced evenly in u = 1 − (r/R)^0.4 between the hole and the rim) × 36 wedges of 10°. Merging on the sheet does **not** tear it. When a body of value ≥ 32 (in practice the target sphere) **sinks past the event horizon**, a flash expands from the well across the sheet and **3 adjacent segment(s)** are torn open (HARD; EASY 1, MEDIUM 2, HARD 3), at a random place away from the well. Level-ups happen the same way: only when the target sphere sinks, never when it is created. Bodies whose centre is over a torn segment fall through and are lost. `state.rifts` lists the centres of torn segments; `pieces[].inRift` tells whether a body is over one.

**Repair** fixes **one** torn segment per action: the one farthest from the centre inside the 10° wedge that contains the sling point (the wedge between the sling and the centre). After a repair, the same wedge can be repaired again only on the next revolution (the sling must leave the wedge and come back). Cost: `repairCost` per segment.

## 9. End of a game and scoring

- Score: **+value of every body created by a merge**, and **+2 × target value** when the target sphere sinks.
- The game ends when there are more than `maxPieces` bodies on the sheet, or (only when the trickle is 0) when tokens cannot pay even a cannon charge while nothing is radiating and nothing flies, or when the final target sinks (win).
- In the benchmark a game also ends after **600 s of game time**.

## 10. Difficulty configurations (fixed; the benchmark uses the one named in section 11)

| id | parameter | EASY | MEDIUM | HARD |
|---|---|---|---|---|
| finalTarget | Final target | 256 | 256 | 256 |
| maxPieces | Body limit on the sheet | 50 | 40 | 34 |
| startBodies | Bodies at start | 14 | 10 | 10 |
| depth0 | Well depth | 8 | 8 | 8 |
| spin | Sheet rotation speed | 1.5 °/s | 3 °/s | 4.5 °/s |
| wob | Tilt wobble | 1.5° | 2.5° | 3.5° |
| ammoMix | Sling ammo odds | 2:60% 4:25% 6:15% 8:0% 32:0% | 2:55% 4:25% 6:12% 8:8% 32:0% | 2:45% 4:25% 6:18% 8:12% 32:0% |
| range | Sling range | 24 m | 22 m | 20 m |
| speed | Coin launch speed | 1× | 1× | 1× |
| keep | Coin slide after landing | 55 % | 55 % | 55 % |
| fric | Coin friction | 0.15 | 0.15 | 0.15 |
| push | Laser kick | 9 m/s | 7 m/s | 7 m/s |
| photon1 | Cannon shots per charge | 2 | 1 | 1 |
| cap | Cannon capacity | 10 | 8 | 6 |
| tokStart | Tokens (Quansistors) at start | 160 | 100 | 140 |
| tokMult | Radiated from a sunk body | 2.5× value | 2× value | 1.75× value |
| tokHalf | Radiation half-life | 8 s | 10 s | 14 s |
| tokMerge | Merge reward | 15 % of value | 10 % of value | 10 % of value |
| tokTrickle | Steady radiation | 0.4 tokens/s | 0.2 tokens/s | 0 tokens/s |
| tokConv | Cannon charge cost | 1 tokens | 1 tokens | 1 tokens |
| riftOn | Rifts enabled | true | true | true |
| riftCells | Rift size | 1 segments | 2 segments | 3 segments |
| repairCost | Repair cost per segment | 5 tokens | 8 tokens | 12 tokens |

All difficulties use the default sheet shape. ammoMix = probability of each ammo value in the sling (2, 4, 6, 8, 32).

## 11. Benchmark protocol (LHC-BENCH-3)

- Difficulty: **HARD** (column above). Games: 3, seeds 101, 202, 303 — identical starting bodies and identical ammo sequence for every bot.
- Each game: up to 600 s game time, simulation speed 4×, one decision at most every 0.5 s of game time after the previous action finished.
- Ranking: **average score** over the games; ties broken by average level, then average game length.
- Physics is real-time and not perfectly deterministic; small differences between runs are normal.

## 12. Architecture of the game (for orientation)

- `hra.js` — main loop, physics world (Rapier), merges, sinking, levels, HUD, settings, agent API for bots.
- `funnel.js` — sheet profile (spline), geometry and height function h(r).
- `recipes.js` — merge recipes. `params.js` — difficulty presets. `tokens.js` — token economy. `rifts.js` — rifts and repair.
- `launcher.js` — the sling + aiming ring UI (the bot uses it exactly like a human finger would).
- `arena-runtime.js` — runs your bot: asks `decide`, executes the action through the same controls as a human player. `arena-worker.js` — the sandbox.

Write the best bot you can. Think about: which ammo to shoot where, building recipe chains efficiently (avoid useless values), keeping the body count low, managing tokens, using the cannons to bring partners together and to push the target sphere into the well while accounting for the rotating view and blocking bodies, and rifts.
