Elefant Hunt Bundle — Widget Vocabulary Extensions¶
Bundle id: elefant_hunt
Vocab spec base: STORYTANGL_WIDGET_VOCAB.md v1.5
Status: draft v0.2 · aligned to v1.5 core vocabulary
Genre: graph-traversal sandbox board game (“inspired by Tom Wham’s Elefant Hunt, Dragon Magazine #88, 1984”)
Engine reference: BOARD_GAME_SANDBOX_DESIGN.md
Audience: authors writing graph-traversal sandbox bundles; port implementers covering the elefant_hunt profile suite
This document is a Tier P3 genre extension (per main spec §8). It introduces no new top-level vocabulary; it codifies conventions on top of v1.5 for graph-traversal sandbox / board-game gameplay.
It is also the spec’s worked proof-of-concept for the journal-as-story claim (§0.8): a board game whose mechanics produce a recognizable Proppian arc transcript from procedural mechanics, without authored fiction beyond per-location flavor.
0 · Genre summary¶
Graph-traversal sandbox games model traversal of a typed location graph with mechanical encounters: the player moves through a directed graph of named locations, draws random outcomes from backend-private pools, accumulates assets, and returns to scoring locations. The board is not a hex grid — it is a graph, with typed nodes and typed edges. Movement is choice-among-exits, not position-on-grid.
Three orthogonal patterns the genre exercises:
Pattern |
Main spec mechanism |
Genre layer adds |
|---|---|---|
Graph board with typed locations |
|
conventional location |
Backend-private random pool |
engine-side |
the canonical example of §0.3 backend authority — the client never sees pool composition |
Composite encounter resolution |
|
auto-assign vs. interactive-assign as two conforming variants |
This bundle is also the worked proof-of-concept for §0.8 journal-as- story (see §6 below).
1 · Domain vocabulary mapped to v1.5¶
Elefant Hunt concept |
v1.5 surface |
|---|---|
Board |
|
Location (port, trail, hazard, junction, hunting ground, graveyard, lost city) |
Sub-zone of the board, with |
Exit |
|
Hunter (hired mob) |
|
Animal (captured) |
|
Ivory marker |
|
Relic marker |
|
Supply |
|
Score |
|
Animal pool (engine |
Engine state. NOT in the UI contract. Client sees only |
Movement |
|
Hazard resolution |
|
Hunt resolution |
|
Random event / encounter table |
|
Port scoring |
Backend computes; emits a |
2 · Board topology¶
The board is a single zone with layout_hints.graph. Each location is
its own sub-zone (so it has an addressable UID, a kind, and a
label_text). Edges have stable UIDs.
{
uid: "z-board",
fragment_type: "group",
group_type: "zone",
zone_role: "board",
member_ids: [
"z-port-stanley", "z-trail-1", "z-river-crossing", "z-trail-2",
"z-hunting-ground-3", "z-albert-falls", "z-elefant-graveyard",
"z-lost-city", "z-port-livingston", /* ... */
],
layout_hints: {
graph: {
nodes: ["z-port-stanley", "z-trail-1", /* ... */],
edges: [
{ uid: "e-stanley-trail1", a: "z-port-stanley", b: "z-trail-1",
kind: "clockwise" },
{ uid: "e-trail1-river", a: "z-trail-1", b: "z-river-crossing",
kind: "clockwise" },
{ uid: "e-albert-left", a: "z-albert-falls", b: "z-hunting-ground-4",
kind: "left" },
{ uid: "e-albert-right", a: "z-albert-falls", b: "z-lost-city",
kind: "right" },
{ uid: "e-quicksand-even", a: "z-quicksand", b: "z-trail-3",
kind: "probabilistic", predicate_ref: "die_even" },
{ uid: "e-quicksand-odd", a: "z-quicksand", b: "z-lost-marker",
kind: "probabilistic", predicate_ref: "die_odd" }
]
}
},
hints: { label_text: "The expedition map" }
}
Location sub-zones carry their semantic role in kind:
{
uid: "z-port-stanley",
fragment_type: "group",
group_type: "zone",
member_ids: ["pc-port-clerk", "z-stanley-catalog"],
kind: "port",
hints: { label_text: "Port Stanley",
style_tags: ["scoring-location", "resupply"] }
}
{
uid: "z-river-crossing",
fragment_type: "group",
group_type: "zone",
member_ids: [],
kind: "hazard",
hints: { label_text: "River crossing",
style_tags: ["probabilistic-hazard"] }
}
{
uid: "z-hunting-ground-3",
fragment_type: "group",
group_type: "zone",
member_ids: [],
kind: "hunting_ground",
properties: { encounter_size: 3 },
hints: { label_text: "Watering hole" }
}
Conventional location kind tags:
Kind |
Meaning |
CLI prefix glyph |
|---|---|---|
|
Scoring, resupply, hire |
|
|
Empty transit; consumes supplies |
|
|
Probabilistic or guaranteed loss |
|
|
Player chooses next direction |
|
|
Encounter draws from animal pool |
|
|
Special reward + cost |
|
|
Special reward + cost |
|
Conventional edge kind tags:
Kind |
Meaning |
|---|---|
|
Forward along the standard route |
|
Direct return path (typically to nearest port) |
|
Player choice at junction |
|
Backend evaluates |
3 · Movement¶
Movement is accepts.kind="pick" over the current location’s
available exits. The cursor moves; no piece is placed.
{
uid: "f-choice-move",
fragment_type: "choice",
edge_id: "e-clockwise", // matches the edge UID
text: "Continue clockwise to the watering hole.",
accepts: { kind: "pick" },
ui_hints: { hotkey: "1", emphasis: "primary" }
},
{
uid: "f-choice-return",
fragment_type: "choice",
edge_id: "e-return-stanley",
text: "Turn back to Port Stanley.",
accepts: { kind: "pick" },
ui_hints: { hotkey: "2", emphasis: "subtle" }
}
Hazardous exits carry ui_hints.emphasis: "warning" or
"danger" so the CLI’s (!) markers and the web’s color treatment
both surface the risk. Per main spec §5.1 Decision Legibility, the
hazard’s nature is rendered as a kv_list row in the location’s
status or in projected state (“Probabilistic: 1-in-6 lose an
animal”).
At junctions, the player gets one choice per direction; at probabilistic splits, the backend evaluates the predicate and emits just one exit choice (or none — the result is forced).
ui_hints.encounter_check optional preview¶
Hazardous or probabilistic movement choices MAY advertise the visible part of the risk with a genre-specific hint:
class EncounterCheckHint(BaseModel):
label: str # "River crossing"
risk_text: str | None = None # "1-in-6 lose an animal"
predicate_ref: str | None = None # opaque backend predicate id
consequence_text: str | None = None # player-facing consequence summary
This is not a client-side roll. It is an advisory display over the same
backend-authoritative predicate_ref and RollFragment flow described
in the main spec. CLI ports render it as a short suffix; richer ports
may badge or color the exit.
4 · Hunters as mobs¶
{
uid: "pc-zartan",
fragment_type: "piece",
piece_id: "hunter-zartan",
kind: "hunter",
owner: null, // single-cursor; multi-cursor sets cursor_id
zone_ref: "z-expedition",
properties: {
name: "Zartan",
hunting_value: 4,
status: "fit" // "fit" | "injured" | "lost"
},
hints: { label_text: "Zartan (HV 4)" }
}
Loss is a delete control fragment. When a hazard kills a hunter,
the backend emits:
{ uid: "f-loss-zartan", fragment_type: "control",
ref_type: "fragment", ref_id: "pc-zartan",
// no payload; this is a delete
fragment_type: "delete" }
Accompanied by a content fragment with the narrative: “The river
takes Zartan. The rest of the expedition presses on.”
Per §0.8 journal-as-story, narrative content accompanies every
narratively significant state change. The CLI transcript reads as
prose.
5 · The hunt — composite RollFragment¶
The hunt resolution is the most complex single fragment in this
bundle. Per the engine design, the hunt is one resolution event,
not a game loop — it produces a single RollFragment(kind="custom")
with structured inputs and outcome.
{
uid: "f-roll-hunt-3",
fragment_type: "roll",
label: "Hunt at the watering hole",
kind: "custom",
inputs: {
drawn: [
{ species: "hippo", point_value: 6, is_killer: true },
{ species: "zebra", point_value: 2, is_killer: false },
{ species: "vulture", point_value: 1, is_killer: false }
],
assignments: [
{ hunter: "Zartan", target: "hippo", d6: 5, total: 9 },
{ hunter: "Ned Net", target: "zebra", d6: 3, total: 5 },
{ hunter: "Skip", target: "vulture", d6: 1, total: 2 }
],
captures: ["hippo"],
escapes: ["zebra", "vulture"],
casualties: []
},
outcome: "mixed_success",
narrative: "You spot a hippo and two zebras—no, a zebra and a
vulture, circling. Zartan brings the hippo down with a
clean shot. Ned Net's zebra slips through the reeds.
Skip rolls a one; the vulture takes wing and is gone.",
ritual_hints: {
skip_label: "Skip the hunt",
duration_ms: 2400,
allow_replay: true
}
}
Two conforming variants:
Auto-assign (MVP per engine design): the backend greedily assigns hunters to animals by
hunting_valueand emits theRollFragmentwithassignmentspopulated. Player has no pre-roll choice; the hunt just resolves.Interactive assign: the prior envelope contains a
ChoiceFragment(accepts.kind="compose")with N parts, one per drawn animal:{ accepts: { kind: "compose", parts: [ { role: "hippo_assignment", accepts: { kind: "pieces", min: 1, max: 1, constraints: { target_zone_ref: "z-expedition", target_kind: ["hunter"] } } }, { role: "zebra_assignment", accepts: { kind: "pieces", min: 1, max: 1, constraints: { target_zone_ref: "z-expedition", target_kind: ["hunter"] } } }, /* ... */ ] } }
After commit, the resolution
RollFragmentfires.
Engine TokenPool is invisible. The client never sees the animal pool’s
composition. inputs.drawn materializes only the animals actually
drawn for this encounter. Per §0.3 backend authority, the client
renders what’s given; pool state is engine territory.
This is the canonical worked example for §0.3.
6 · Hazard examples¶
River Crossing (probabilistic loss)¶
Backend emits on entry:
[
{ uid: "f-prose-river", fragment_type: "content",
content: "The river runs fast and brown. You wade in, supplies held high." },
{ uid: "f-roll-river", fragment_type: "roll",
label: "River crossing",
kind: "dice",
inputs: { dice: "1d6", rolled: [6], target: 6 },
outcome: "failure", // a 6 means loss in original Wham rules
narrative: "A zebra is lost to the current.",
ritual_hints: { duration_ms: 1000 } },
{ uid: "f-loss-zebra", fragment_type: "control",
ref_type: "fragment", ref_id: "pc-animal-zebra-1",
fragment_type: "delete" },
// proceed to next location
{ uid: "f-choice-next", fragment_type: "choice",
edge_id: "e-trail-3",
text: "Continue along the trail.",
accepts: { kind: "pick" },
ui_hints: { hotkey: "1", emphasis: "primary" } }
]
Albert Falls (mandatory next-turn junction)¶
Albert Falls is a junction whose exits are suppressed until the next turn. Per §0.6 narrative authoring stance, this is the backend authoring state on demand: the player arrives, the backend emits a content fragment and no choices, then the next envelope opens both junction exits.
// Arrival envelope
[
{ uid: "f-prose-albert", fragment_type: "content",
content: "Albert Falls roars below. You make camp at the cliff's
edge, considering your next move." }
// No choices — cursor is parked
]
// Next-turn envelope
[
{ uid: "f-choice-left", fragment_type: "choice",
edge_id: "e-albert-left", text: "Take the western path.",
accepts: { kind: "pick" }, ui_hints: { hotkey: "1" } },
{ uid: "f-choice-right", fragment_type: "choice",
edge_id: "e-albert-right", text: "Take the eastern path.",
accepts: { kind: "pick" }, ui_hints: { hotkey: "2" } }
]
The client doesn’t know exits will be suppressed; it just renders what’s in the envelope. Per §0.3 / §0.6, the contract surface is the rendered turn, not a world-model query.
Elefant Graveyard (special: cost + reward)¶
[
{ uid: "f-prose-graveyard", fragment_type: "content",
content: "Bones, white and vast, scatter the clearing. The smell of
old grass and older time." },
// Cost: lose one elefant if held
{ uid: "f-elefant-loss", fragment_type: "control",
ref_type: "fragment", ref_id: "pc-animal-elefant-1",
fragment_type: "delete" },
{ uid: "f-prose-loss", fragment_type: "content",
content: "The young elefant breaks free at the sight and crashes into
the trees." },
// Reward: ivory marker
{ uid: "pc-ivory-1", fragment_type: "piece",
piece_id: "ivory-1", kind: "ivory",
zone_ref: "z-expedition",
properties: { value: null }, // resolved at port scoring
hints: { label_text: "Ivory marker" } },
{ uid: "f-prose-ivory", fragment_type: "content",
content: "You pry loose a tusk fragment. It will fetch something at
market." }
]
7 · Port scoring¶
At port return, the backend emits a sequence of RollFragments for
ivory and relic valuations (3d6 each), aggregates the score, and
deletes the scored assets.
[
{ uid: "f-prose-return", fragment_type: "content",
content: "Port Stanley. The harbormaster greets you with a ledger." },
{ uid: "f-roll-ivory-1", fragment_type: "roll",
label: "Ivory appraisal",
kind: "dice",
inputs: { dice: "3d6", rolled: [5, 4, 6], total: 15 },
outcome: "success",
narrative: "The ivory weighs out at 15 points." },
{ uid: "f-roll-relic-1", fragment_type: "roll",
label: "Relic appraisal",
kind: "dice",
inputs: { dice: "3d6", rolled: [2, 1, 3], total: 6 },
outcome: "modest",
narrative: "The artifact is curious but not valuable: 6 points." },
// Apply score
{ uid: "f-score-update", fragment_type: "control",
ref_type: "section", ref_id: "score",
payload: { value: { value_type: "scalar", value: 47 } } },
// Delete scored assets
{ uid: "f-del-ivory", fragment_type: "control",
ref_type: "fragment", ref_id: "pc-ivory-1",
fragment_type: "delete" },
{ uid: "f-del-relic", fragment_type: "control",
ref_type: "fragment", ref_id: "pc-relic-1",
fragment_type: "delete" },
// Reset supplies, offer next expedition
{ uid: "f-choice-next", fragment_type: "choice",
edge_id: "e-resupply",
text: "Resupply and depart again (consumes 4 stamina).",
accepts: { kind: "pick" }, ui_hints: { hotkey: "1" } },
{ uid: "f-choice-end", fragment_type: "choice",
edge_id: "e-end-session",
text: "End the expedition.",
accepts: { kind: "pick" }, ui_hints: { hotkey: "2", emphasis: "subtle" } }
]
8 · Solitaire mode — beat the blind¶
Solo play variant. A target score is drawn at game start and revealed
at session end. The bundle MAY surface the target as a ProjectedState
section with kind="target", value hidden behind a placeholder until
revealed:
{
section_id: "target",
title: "Par",
kind: "target",
value: { value_type: "scalar", value: "—" },
hints: { style_tags: ["hidden-until-reveal"] }
}
At session end, the backend emits a control updating value to the
revealed par, then renders the win/loss commentary.
9 · Multi-cursor framing (Tier P1 target)¶
Per main spec §1.5 (Tier P1), each player is a cursor with their own channel. Single-cursor hot-seat is the v1.x reality; multi-cursor is the future direction once §1.5 graduates.
Single-cursor hot-seat: the bundle emits “Pass the keyboard to
Player Red” content fragments between turns. Each player’s score and
expedition are projected with kind="score_<player_name>" so all
players can see all scores.
Multi-cursor target: per §1.5 per-cursor projection of shared state:
The board is shared world state. Each cursor sees the board zone in its envelopes.
Each hunter
PieceFragmentcarriesowner=<cursor_id>. Cursor A sees their own hunters and (pervisibility="public") the positions of other cursors’ parties.The animal pool stays engine state (the
TokenPool); no cursor sees pool composition.When cursor A captures animals, cursor B’s channel receives a
deletefor the relevant encounter zone members and acontentfragment narrating the capture (“Hunter Red bags a hippo at the north watering hole.”).Score projections are per-cursor; the projected
kv_listof all players’ scores is also visible to every cursor.
Ape Man as cross-cursor interaction. Per the engine design, the Ape Man rule lets one player’s interaction modify another player’s inventory. Implementation: the affected cursor receives a control fragment authored by the other cursor’s commit. This is the one genuine cross-cursor mechanism, and it falls cleanly under §1.5’s “backend coordinates shared world state.”
10 · The journal-as-story validation¶
Per main spec §0.8, this bundle is the worked proof-of-concept for the journal-as-story claim. The test:
Run a complete expedition through
cli_reference_port.py.Capture stdout.
Read the result.
Expected transcript shape (per the design doc, slightly formalized):
Port Stanley. The harbormaster greets you with a ledger. You hire
two hunters and load four days of supplies.
1) Set off into the interior.
> 1
You move deeper into the interior. Camp. Consumed 1 supply.
1) Continue to the river.
> 1
The river runs fast and brown. You wade in, supplies held high.
[River crossing] rolled 6 / target 6
A zebra is lost to the current.
1) Continue along the trail.
> 1
You move deeper still. Camp. Consumed 1 supply.
1) Approach the watering hole.
> 1
You spot a hippo and two zebras—no, a zebra and a vulture, circling.
Zartan brings the hippo down with a clean shot. Ned Net's zebra slips
through the reeds. Skip rolls a one; the vulture takes wing and is
gone.
[Hunt at the watering hole]
captures: hippo
escapes: zebra, vulture
1) Continue clockwise.
2) Turn back to Port Stanley.
> 2
[long return journey, abbreviated]
Port Stanley. The harbormaster greets you with a ledger.
[Ivory appraisal] rolled 5 + 4 + 6 = 15
The ivory weighs out at 15 points.
Expedition returns to Port Stanley. Score: 21 points.
1) Resupply and depart again (consumes 4 stamina).
2) End the expedition.
The claim: this transcript reads as a naturalist’s field notes or a pulp adventure log — without any prose beyond per-location flavor and outcome narratives. The Proppian arc (departure → trials → boons → return → recognition) emerges from graph topology and fragment sequencing.
The test: check this transcript in
engine/contrib/conformance/transcripts/elefant_hunt_one_expedition.txt
and update with each bundle revision. The smoke-test assertion: a
human reader recognizes it as a story.
This is also the artifact to point at when explaining StoryTangl to outsiders: “this is what falls out, with no extra authoring.”
11 · Port parity addendum¶
Widget |
Web (Vue) |
CLI |
tkinter |
Hypothetical Godot |
|---|---|---|---|---|
Board zone |
graph diagram with location nodes |
|
|
3D scene, top-down camera |
Location (active) |
highlighted node + nameplate |
location heading + |
bordered |
spotlight + camera focus |
Exit choice |
button per exit |
|
|
clickable path |
Hunter piece |
portrait chip with HV badge |
|
small portrait card |
NPC actor |
Animal captured |
thumbnail in expedition tray |
line in expedition list |
small icon |
inventory shelf |
Hazard roll |
one-shot animation + result |
inline outcome line |
brief flash + label |
rolling die overlay |
Hunt roll |
composite animation (draw, assign, resolve) |
structured block with sections |
tabbed |
cinematic sequence |
Score |
tile with number + delta |
line: |
large |
scoreboard prop |
Multi-cursor turn pass |
“Pass to Player X” banner |
line: |
modal |
curtain transition |
Appendix — Prior art¶
Tom Wham’s Elefant Hunt (Dragon Magazine #88, 1984) is the namesake. Wham’s design DNA traces through Snit’s Revenge (1977), The Awful Green Things from Outer Space (1979), and his other magazine-insert games — all characterized by compact rules, named locations carrying narrative weight, and dice-driven mechanics that generate quotable session stories.
Adjacent designs the vocabulary supports: Magic Realm (1979) with its tiled exploration map; Tales of the Arabian Nights (1985) with its encounter-table-driven narrative; Hellboy: The Board Game (2019) with its mission-graph traversal. Modern legacy / campaign games (Pandemic Legacy, Gloomhaven) share the directed-graph-with- typed-locations DNA at a higher complexity ceiling.
Reframing the colonial premise. The original Elefant Hunt is a 1980s safari game and shows it. The mechanics support a contemporary reframing as wildlife conservation / ecotourism / nature photography: animals are captured alive, lost animals are penalized, and the Ape Man’s explicit role is animal liberation. “Elefant Ecotourism” or “wildlife survey” fits the same graph and the same pool draws. The genre profile is medium-neutral; the narrative skin is bundle-authored.
The engine-side architecture (SandboxScope, SandboxLocation,
SandboxExit, SandboxMob, TokenPool) is documented in
BOARD_GAME_SANDBOX_DESIGN.md. The engine-side abstractions are
authoring concerns; this document is rendering contract. The
TokenPool, in particular, is contract-invisible — clients never see
it directly, only its draw outcomes. This is the canonical example
of §0.3 backend authority working under pressure.
End of elefant_hunt EXTENSIONS v0.2.