What a seat sees
Last edited by · ·
What a seat sees
The guiding line: a seat sees what its cog's eyes see, plus what its team says. There is no free-cam and no map-wide enemy list — the fog is the game.
Structurally this is ViZDoom's depth buffer plus labels buffer, written as
text: sixteen ray distances across the cone, and one labelled row per thing the
cone or the bubble actually contains. Both come out of marchRays in
src/vzd/egoview.nim — the same proc the viewer's first-person inset uses —
so the wall the model reads about and the wall the spectator sees can never
disagree.
The observation is a JSON object appended to the user message, and it is
mirrored (minus your_notes) into the replay's directive record, so the
replay explains every decision.
{
"you": "RED-beta",
"team": "RED",
"turn": 7, "turns": 24,
"clock": {"played_s": 31, "left_s": 77},
"map": {"w": 1235, "h": 659,
"zones": [{"id":"A1","c":[123,110],"t":"cover"},
{"id":"C2","c":[617,330],"t":"corridor"}],
"your_spawn": "A2", "their_spawn": "E2",
"medkits": [{"at":[617,120],"zone":"C1"}]},
"you_at": {"pos": [402,318], "aim": 8, "facing": "E", "zone": "B2",
"hp": 2, "alive": true, "respawn_in_ticks": 0, "fire_ready": true,
"frags": 2, "deaths": 1, "streak": 1},
"rays": [[-45,318,-1],[-39,341,-1],[-33,377,-1],[-27,441,-1],[-21,612,-1],
[-15,905,-1],[-9,1204,-1],[-3,-1,742],[3,-1,742],[9,1188,-1],
[15,884,-1],[21,598,-1],[27,430,-1],[33,369,-1],[39,334,-1],
[45,312,-1]],
"contacts": [
{"label":"enemy","id":"BLUE-alpha","bearing":-4,"dist":688,"zone":"C2","hp":3,"ticks_ago":0},
{"label":"enemy","id":"BLUE-delta","bearing":22,"dist":915,"zone":"D3","hp":null,"ticks_ago":41},
{"label":"medkit","id":null,"bearing":-31,"dist":214,"zone":"B1","hp":null,"ticks_ago":0}
],
"team_net": [
{"id":"RED-alpha","pos":[268,140],"hp":3,"alive":true,"zone":"A1","radio":"holding the north lane"}
],
"heard": [{"team":"BLUE","text":"push mid","at":[640,330],"ticks_ago":18}],
"score": {"you": 6, "them": 4, "margin": 2,
"your_team": [{"id":"RED-alpha","f":3,"d":1}]},
"your_last_order": {"intent":"hunt","at":"C2","result":"chasing"},
"your_notes": "hold the B2 doorway, they funnel through C2"
}
Field rules
mapis sent ONCE, at this seat's first turn: fifteen zones is a lot of tokens to resend twenty-four times for a board that cannot move. The terrain is public knowledge — only bodies are fogged — because a seat that cannot navigate cannot play.raysis the depth strip: 16 entries spanning the cone from −45° to +45° in 6° steps, each[bearing_deg, wall_dist_px, glass_dist_px], with-1meaning "nothing withinvisionRange". Glass is transparent to vision and solid to bullets, so a column can report a glass pane in front of a wall.contactsis one row per entity the cog can legitimately see right now (inside the cone with a clear line, or inside the 90 px bubble), plus every enemy this cog saw within the last 72 ticks taggedticks_agoand positioned where it was when seen.labelis exactly ViZDoom's labels-buffer object class:enemy,allyormedkit. An enemy withticks_agoabove 0 is a MEMORY, not a sighting — it has moved.aimis brads (256 per turn, 0 = east, counter-clockwise);facingis the nearest of the eight compass points, because a model reasons better about "NE" than about "32".bearingis degrees relative to your own aim, negative left — an egocentric quantity, never a world angle.marginisteamNet[you] − teamNet[them], so "higher is better for me" always holds.your_last_order.resultis the driver's honest report of how the previous order ended:moving | arrived | chasing | holding | firing | no_route | dead | respawned | unknown_target. It is what lets a seat recover from a race it could not see.
Hidden
Every other seat's order, notes, radio and prompt; every seat's real player
name, policy name and kind; the enemy team's per-cog frag/death breakdown (only
its team total is public); enemy positions outside your cone, bubble and
72-tick memory; enemy aim and intent; the seed; the unselected pool entries;
and the map's own mapSpec document — a seat gets zones, not wall rectangles.
Reply
One JSON object, beginning with {:
{"intent": "hunt", "at": "C2", "to": [617, 330], "face": [700, 330],
"say": "mid", "radio": "BLUE-alpha is in C2 at 3hp, I take the north door",
"notes": "hold B2 next turn if this fails"}
Every field is repaired, never rejected: an unknown intent becomes
hunt, an unresolvable at falls back to to, an out-of-board to is
clamped, and a reply that names no order at all keeps last turn's order (else
rusher's). A reply with a valid say/radio but no intent is usable:
the cog carries on and the line is delivered. The starter's cogs: [...] array
form is still accepted, with its single entry read as the flat order.
Every string that lands in the replay — say, radio, notes, the policy
label, stopDetail, recorded error text — is truncated on RUNE boundaries,
never by byte index. Byte truncation is what makes a replay that renders in a
browser fail a strict UTF-8 parser.