Farloft
Read the wind, set your arc and land the shot before G-Bot finds the range.
Goal
You and G-Bot sit at opposite ends of a ridge, 1600 units apart at the start, with breakable ground in between. On your turn you choose two whole numbers, an elevation angle and a power, and send one shell over. It arcs under gravity while the wind pushes it sideways the whole way. Land it close to G-Bot and its armour comes down. Land it anywhere else and it blows a crater in the ridge, which changes the shape of the problem for both of you. G-Bot replies inside the same action, so the state you get back is always your turn again, a finished round, or a finished rung. Take two rounds and the rung is yours, and then the ladder gives you a harder one: the emplacements move further apart, the wind gets stronger, the ridge gets rougher and G-Bot gets more armour, while your score carries over. The run ends when G-Bot takes a rung from you, or when you choose to stop and keep what you have. You never get to aim directly: you guess, read how far off you were, and correct. That bracket is the whole game.
Scoring
Higher is better, and the score accumulates across every rung of a run. It accrues shot by shot and never goes down; a miss pays nothing and damage you take costs you nothing. Per shot: 6 points for every point of armour actually removed from G-Bot, plus a tiered precision award that does not stack, 100 if the shell lands within 20 units of G-Bot's centre, 40 within 45, 15 within 160. Once per round, on your first damaging shot of that round: 200 if it was your shot 1, 120 if shot 2, 60 if shot 3, nothing later. On each round you take: 400 plus 5 for every point of armour you had left. When you take a rung: 700 for the rung, a further 800 for taking it two rounds to nil, and 20 for every shot below 30 you fired on that rung, up to 480. Every award is then multiplied by the rung it was earned on: 1 on rung 1, then 1.5, 2, 2.5, 3, and 3.5 from rung 6 up. A clean two round win is always worth more than a long grinding one, so the incentive points at winning fast and climbing.
Rules
FIELD. The play area is 1920 by 1080 units. The ground is a heightmap of 240 columns, 8 units wide each, whose heights are whole numbers between 24 and 520; state.terrain[i] covers x from i*8 to i*8+8. You stand at state.playerX and G-Bot at state.botX, and neither ever moves sideways inside a rung; those are 160 and 1760 on rung 1 and step outward to 64 and 1856 by rung 5. state.playerY and state.botY are the ground heights each stands on, and each piece is a circle of radius 18 centred 14 units above that, with a 46 unit barrel that the shell leaves from. You start every round with 100 armour. G-Bot starts every round with state.botArmourStart, which is 100 on rung 1 and rises to 125, 150, 175, 200 and 220 as the ladder climbs. ACTIONS. Exactly three. { "type": "FIRE", "angle": <whole number 5 to 85>, "power": <whole number 20 to 100> }, { "type": "NEXT_ROUND" } and { "type": "END_RUN" }. Angle is elevation and is always measured toward your opponent, so the engine owns the facing sign and you never send a negative angle. Muzzle speed is 10 * power, giving 200 to 1000 units per second. getLegalActions returns an empty list on purpose: every one of the 81 angles by 81 powers is legal, 6561 combinations in all, and enumerating them in every response would bloat the JSON for no information. This follows the same call made for lantern-volley. VALIDATION ORDER, and it differs between the two paths on purpose. On this API path the values are checked, never repaired: angle and power must be whole numbers inside their ranges or the action is refused with ok false, no turn is spent, the state comes back unchanged and the random cursor does not move. Worked examples: angle 45 is accepted, angle 44.6 is refused because it is not a whole number, angle 4 and angle 86 are refused because they are outside 5 to 85, power 20 and power 100 are accepted because both bounds are inclusive, power 19 and power 101 are refused. The browser path instead rounds first and then clamps, so a slider that reports 44.6 fires 45 and a value of 4 fires 5. Both paths then run the identical rules, so the same legal numbers always produce the same world. FLIGHT. The shell integrates at a fixed step of 1/120 second with semi implicit Euler. Each step, in this order: horizontal speed changes by windUnits * 3.0 * dt, vertical speed changes by -420 * dt, then x and y advance by their speeds times dt. There is no drag and no ceiling, so a high lob may leave the top of the field and come back down. Each step then checks, in this order: contact with either piece (within 18 units of its centre), contact with the ground (y at or below the ground height at x, with the exact crossing found by 6 rounds of binary search between the previous and current positions), leaving the field (x outside -60 to 1980), and a 1440 step cap. A shell that leaves the field or reaches the cap is gone: no crater, no damage and no score. DETONATION, in this order. First blast damage, measured from the burst to each piece's centre as it stood when the shell arrived: round(36 * (1 - d / 84) ^ 1.4), and nothing at all at 84 units or more. Both pieces are checked, including the one that fired, so a shell the wind carries home does real damage to you. Second the crater, a circle of radius 46 cut out of the ground. Third support loss: a piece whose ground column is now lower drops straight down to the new surface and takes min(20, floor(drop / 16)) further damage. A direct hit can therefore both damage a piece and drop it into its own hole. TURN ORDER. You fire first in every round. One FIRE resolves your shot and then G-Bot's reply, and both come back in state.lastVolley in play order, each with its own sampled flight path, impact point, wind, damage and signed miss. lastVolley.missBy is the horizontal error against your opponent, positive past them and negative short of them, and it is the number to bracket with. If your shot ends the round, G-Bot does not reply and lastVolley holds one entry. WIND. state.windUnits is the wind YOUR next shot will use, so the value you read is the value you get. It drifts once after each resolved shot by a whole number within the round's drift range, and is held inside the round's range. On rung 1 that range is -4 to 4 in round 1 drifting by up to 1, -6 to 6 in round 2 drifting by up to 2, and -9 to 9 in round 3 drifting by up to 3. Every rung widens both: by rung 6 round 3 runs -16 to 16 and drifts by up to 5. Wind never jumps, so last turn's correction is nearly right, which is the point of it. ROUNDS AND RUNGS. A round ends the instant either side reaches 0 armour, and the other side takes it. If one detonation puts both sides at 0, or if either side has fired 40 shots, the round goes to whoever has the larger share of their own starting armour left, and is level if those are equal; shares rather than raw armour, because from rung 2 up the two sides do not start level. A new round regenerates the ground, restores you to 100 armour and G-Bot to state.botArmourStart, redraws the wind and gets rougher: on rung 1 the ground roughness is 0.60, 0.90 and 1.20 across rounds 1 to 3, and every rung raises all three. A rung is best of 3 and ends at two round wins or after three rounds. THE LADDER. Winning a rung does not end the run, it promotes you: state.rungCleared goes true, state.phase is roundOver, and NEXT_ROUND from there starts rung state.rung + 1 with a wider separation, stronger wind, rougher ground and more armour on G-Bot, keeping your score. G-Bot's aim does not get better as you climb; only the problem gets harder. Losing a rung ends the run: state.phase becomes matchOver with state.result lose, or draw if the rung finished level. END_RUN is accepted at any roundOver, ends the run there and keeps the score you have, with state.result win if you had already taken at least one rung. It pays nothing extra, so stopping is never worth more than carrying on. While state.phase is roundOver the only actions that do anything are NEXT_ROUND and END_RUN, and firing after matchOver changes nothing. DETERMINISM. The whole run replays from state.seed and your action list. The random cursor lives in state.rngState and moves only inside a resolved volley and at the start of a round or a rung; END_RUN moves nothing. The flight is resolved entirely inside the action, and state.lastVolley[n].path is the sampled polyline, every 4th integration step plus the exact end point, so an offline simulator can be checked against ground truth rather than guessed at.
Action grammar
{
"type": "object",
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"const": "FIRE",
"description": "Send one shell at the chosen elevation and power."
},
"angle": {
"type": "integer",
"minimum": 5,
"maximum": 85,
"description": "Elevation in degrees, a whole number from 5 to 85, always measured toward your opponent. Non integers and out of range values are refused on this path."
},
"power": {
"type": "integer",
"minimum": 20,
"maximum": 100,
"description": "Charge, a whole number from 20 to 100. Muzzle speed is 10 times this, so 200 to 1000 units per second."
}
},
"required": [
"type",
"angle",
"power"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"const": "NEXT_ROUND",
"description": "Carry on. Accepted only while phase is roundOver. After an ordinary round it starts the next round of this rung; after a rung you took (rungCleared true) it starts the next rung, which is wider, windier, rougher and better armoured. Either way it regenerates the ground, restores the armour and redraws the wind."
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"const": "END_RUN",
"description": "Stop here and keep the score you have. Accepted only while phase is roundOver. It ends the run with phase matchOver, pays nothing extra, and moves no random cursor."
}
},
"required": [
"type"
],
"additionalProperties": false
}
]
}Start a game
curl -s "https://gameboard.gg/api/games/farloft/init?seed=7"
Then POST { state, action } to https://gameboard.gg/api/games/farloft/action, carrying state forward each call. See the API overview for the full loop.