Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

resolve.gno

45.52 Kb · 1215 lines
   1package engine
   2
   3import (
   4	"strings"
   5
   6	"gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/cards/v0"
   7	"gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/hex/v0"
   8)
   9
  10// Event kinds.
  11const (
  12	EvMove         = "move"          // From, To, Facing, Down, Action (Reason "stagger" when thrown by a hit)
  13	EvTurn         = "turn"          // Facing, Action (Reason "stagger" when thrown by a hit)
  14	EvFlip         = "flip"          // Down, Action (empty for a DROP penalty)
  15	EvDelay        = "delay"         // N gained, Reason, Card drawn (0 for crawl), Result penalty, Endurance crossed off, Delay = running total once this card is counted
  16	EvCancel       = "cancel"        // Action, Reason (To = the obstacle in the way for no_los)
  17	EvDraw         = "draw"          // Gun moved from From (holster, a hand, or "ground" with N = its ground id) to To (a hand) and cocked, unless jammed (Result "jammed")
  18	EvLoad         = "load"          // a shell loaded in the gun in both hands, N shells now, uncocked; Result "unjammed" when that completes a jammed gun
  19	EvCock         = "cock"          // Gun cocked
  20	EvUncock       = "uncock"        // Gun uncocked
  21	EvAim          = "aim"           // N = AIM points after aiming, To = the hex they sit on, Target = the seat they follow (-1 for a hex)
  22	EvLoseAim      = "lose_aim"      // Reason (To = the obstacle in the way for no_los)
  23	EvNothing      = "nothing"       // a Shoot used to do nothing, or a shot held at a hex nobody stands in (Reason empty_hex, To = the hex) or at a target out of sight (Reason no_los, To = its hex, Target = its seat) or out of the fight (Reason out_of_fight, To = its hex, Target = its seat): keeps the aim
  24	EvShot         = "shot"          // Card, Target = the seat shot at (whoever stood in the hex named, for a shot at a hex), To = the hex shot at, Hit (final location or Miss), Range, N = aim time, Reason "" | out_of_zone | misfire
  25	EvThrow        = "throw"         // like a shot: Card, Target, To = the hex thrown at, Hit, Range, N = aim time, Chart, Status, Gun, GunID, Reason "" | out_of_zone; the weapon then lands (EvDropGun, Reason throw)
  26	EvMalfunction  = "malfunction"   // Card, Result (no_effect, misfire, jams, explodes, no_penalty), Gun, GunID
  27	EvWildShot     = "wild_shot"     // Gun went off without hitting anything: uncocked, one shell less
  28	EvWound        = "wound"         // Player hit: Result = penalty kind (Kill, Stun...), N
  29	EvDirection    = "direction"     // Card drawn as a direction card: To = its HEX line
  30	EvDropGun      = "drop_gun"      // Gun fell from From (hand) to the hex To, N = its ground id; Reason throw for a thrown weapon landing, gun_move for one let go (10.22)
  31	EvMoveGun      = "move_gun"      // a gun move (10.22): Gun moved from From (both hands) to To (a hand), as it was
  32	EvPassOut      = "pass_out"      // end of turn: more delay than endurance two turns in a row
  33	EvSeriousCheck = "serious_check" // end of showdown: Card, N = DELAY + SERIOUS points (4 kills)
  34	EvVP           = "vp"            // end of turn: victory points (rule 15), N = points (< 0 for a loss), Reason = the award (VPSurvive...)
  35	EvRunAway      = "run_away"      // the character moved off the map (15.5): From = the edge hex it left, Facing = the way it went, Action
  36)
  37
  38// Delay and cancel reasons.
  39const (
  40	ReasonCrawl      = "crawl"        // moving while down (rule 9.2)
  41	ReasonSprint     = "sprint"       // sprinting draws a delay card (rule 9.23)
  42	ReasonLeap       = "leap"         // leap/drop draws two delay cards (rule 9.4)
  43	ReasonOccupied   = "occupied"     // ending a move in an occupied hex (rule 9.24)
  44	ReasonFence      = "fence"        // moving or staggering across a fence (rule 35.2)
  45	ReasonObstacle   = "obstacle"     // moving or staggering onto or off an obstacle (rule 36.4)
  46	ReasonDrop       = "drop"         // a DROP penalty draws three delay cards (rule 14.11)
  47	ReasonStun       = "stun"         // a STUN draws wound cards (14.11)
  48	ReasonLight      = "light"        // a LIGHT draws fatigue cards (14.11)
  49	ReasonSerious    = "serious"      // fatigue cards at the start of the turn (14.12)
  50	ReasonLeg        = "leg"          // fatigue cards per hex moved upright (14.12)
  51	ReasonTime       = "time"         // not executed by the end of segment 5 (rule 7.53)
  52	ReasonNoGun      = "no_gun"       // no gun in the box the action needs (11.3); a gun move of a gun the character no longer has
  53	ReasonNotInHand  = "not_in_hand"  // a gun move dropping a gun that is not in a hand box (10.22)
  54	ReasonNotBoth    = "not_both"     // a gun move to a hand of a gun that is not in both hands (10.22)
  55	ReasonGunDropped = "gun_dropped"  // a gun move dropped the gun the character aims with: its AIM points go
  56	ReasonHandFull   = "hand_full"    // the destination hand already holds a gun
  57	ReasonNoRoom     = "no_room"      // the character already carries MaxGuns weapons
  58	ReasonNotCocked  = "not_cocked"   // shooting an uncocked gun (13.1)
  59	ReasonEmpty      = "empty"        // shooting a gun without shells (13.1)
  60	ReasonJammed     = "jammed"       // shooting or cocking a jammed gun not completely reloaded yet (13.31)
  61	ReasonFull       = "full"         // loading a gun already holding its Ammo limit (10.42)
  62	ReasonBroken     = "broken"       // using or loading an exploded gun (13.31)
  63	ReasonNoTarget   = "no_target"    // the character named is out of the fight (12.6), or is the shooter, or no such seat
  64	ReasonOutOfZone  = "out_of_zone"  // the target is in the blind zone (8.32, 12.41)
  65	ReasonNoLOS      = "no_los"       // no line of sight to the target, an obstacle in the way by the heights (31): an aim is cancelled, a shot is held, AIM points are lost
  66	ReasonReveal     = "reveal"       // revealing an action other than Turn or a gun action (12.44)
  67	ReasonTransfer   = "transfer"     // a new target more than one hex from the markers (12.5)
  68	ReasonOwnHex     = "own_hex"      // a new target moving the markers into or out of the character's own hex (12.5)
  69	ReasonNoHex      = "no_hex"       // aiming at a hex that is not on the board
  70	ReasonShot       = "shot"         // aim spent by a shot (12.43)
  71	ReasonStagger    = "stagger"      // thrown by a STAGGER penalty
  72	ReasonMisfire    = "misfire"      // the shot was cancelled by a MALFUNCTION
  73	ReasonEmptyHex   = "empty_hex"    // a shot at a hex nobody stood in: held (EvNothing)
  74	ReasonOutOfFight = "out_of_fight" // a shot at a character killed, passed out, surrendered or run away: held (EvNothing)
  75	ReasonOutOfRange = "out_of_range" // a throw farther than the weapon's wielding factor (17.2)
  76	ReasonThrow      = "throw"        // aim spent by a throw (16.13); a thrown weapon landing (17.4)
  77	ReasonGunMove    = "gun_move"     // a gun move dropping the gun in the hex (10.22)
  78)
  79
  80// Malfunction results.
  81const (
  82	MalfNoEffect  = "no_effect"  // discarded, another card drawn (13.3)
  83	MalfMisfire   = "misfire"    // the shot is cancelled (13.3)
  84	MalfNoPenalty = "no_penalty" // second draw was a FIRE: the gun is fine (13.32)
  85)
  86
  87// LoadUnjammed is the Result of a load that completes a jammed gun (13.31).
  88const LoadUnjammed = "unjammed"
  89
  90// DrawJammed is the Result of a Draw & Cock that moves a jammed gun: it is
  91// not cocked (13.31).
  92const DrawJammed = "jammed"
  93
  94func jammedResult(g Gun) string {
  95	if g.Jammed {
  96		return DrawJammed
  97	}
  98	return ""
  99}
 100
 101// Event is one thing that happened during resolution, in order. Seg is 0
 102// for turn-start events and EndOfTurn for those after segment 5.
 103type Event struct {
 104	Seg       int
 105	Player    int
 106	Kind      string
 107	Action    cards.Code
 108	From      string
 109	To        string
 110	Facing    hex.Dir
 111	Down      bool
 112	N         int
 113	Delay     int
 114	Reason    string
 115	Card      int     // the result card drawn (0 when none)
 116	Result    string  // a card's penalty, a malfunction result or a wound's penalty kind
 117	Endurance int     // endurance boxes crossed off by a wound or fatigue card
 118	Target    int     // the seat shot at, for shots
 119	Hit       string  // the final hit location of a shot (Miss "-")
 120	Range     int     // the range of a shot, in hexes
 121	Gun       GunType // the gun's model, for gun events
 122	GunID     int     // the gun's id, for gun events
 123	Step      int     // which part of the segment it belongs to (StepShots, StepEnd, StepAction+seat), 0 outside the segments
 124	Chart     string  // a shot's preliminary hit read on the FIRE card's chart, before Target Status ("" when no chart was read)
 125	Status    string  // the Target Status letters (StatusMove...) that changed a shot's hit, "" when none did
 126}
 127
 128// Steps of a segment, in the order they resolve: the shots (7.52), each
 129// seat's action in seat order, then the end of the segment (the
 130// occupied-hex penalty, combat results, aims out of the zone). Events of
 131// one step happened together; a replay shows the steps one after the
 132// other. Turn-start and end-of-turn events have no step (0).
 133const (
 134	StepShots  = 1
 135	StepEnd    = 2
 136	StepAction = 3 // + the seat acting
 137)
 138
 139// stamp marks the events with their step.
 140func stamp(evs []Event, step int) {
 141	for i := range evs {
 142		evs[i].Step = step
 143	}
 144}
 145
 146// pending is the combat results a shot inflicts on a seat, applied at the
 147// end of the segment (14.1), with the shooter for STAGGER's "Long" hex.
 148type pending struct {
 149	from int
 150	pens []Penalty
 151}
 152
 153// Resolve plays the plans, one per seat, over the five segments of a turn
 154// (rules 5.5, 6, 7, 9, 12, 13, 14), deterministic for a given Drawer; the
 155// state given is left as it is. Choices were committed up front and are
 156// resolved against each character's live state as the stacks play out.
 157//
 158// Each segment, every alive character whose next action is due
 159// (usedTime + cost + delay <= segment) executes it once, shots first
 160// (7.52), in seat order. Delay gained during a segment and the results of
 161// hits take effect from the end of the segment ([H6], 14.1). Actions still
 162// pending after segment 5 are cancelled.
 163func Resolve(board *hex.Board, st State, plans []Plan, d Drawer) (State, []Event) {
 164	st.Chars = CloneChars(st.Chars)
 165	n := len(st.Chars)
 166	// A STRENGTH is part of the Throw it is played on top of (20.1).
 167	combined := make([]Plan, n)
 168	for p := 0; p < n && p < len(plans); p++ { // a seat without a plan passes
 169		combined[p] = plans[p].Combine()
 170	}
 171	plans = combined
 172	var (
 173		used = make([]int, n)
 174		idx  = make([]int, n)
 175		evs  []Event
 176	)
 177	// Turn start: SERIOUS wounds draw fatigue cards (5.3), on the sheet
 178	// before segment 1.
 179	startDelay := make([]int, n)
 180	for p := range st.Chars {
 181		c := &st.Chars[p]
 182		startDelay[p] = c.Delay
 183		if c.Status != Alive {
 184			continue
 185		}
 186		// Each SERIOUS point draws a fatigue card, put down to the seat whose wound gave it.
 187		for i := 0; i < c.Serious; i++ {
 188			c.Delay += drawCard(p, c, 0, ReasonSerious, fatigueLine, pointFrom(c.SeriousFrom, i), d, &evs)
 189		}
 190	}
 191	runningDelay(evs, startDelay)
 192	// Reveal the first actions (5.4): aim may be lost or moved (12.44, 12.5).
 193	for p := range st.Chars {
 194		if st.Chars[p].Status == Alive && len(plans[p]) > 0 {
 195			reveal(board, p, &st, plans[p][0], 0, &evs)
 196		}
 197	}
 198	due := func(p int, seg int) bool {
 199		c := &st.Chars[p]
 200		return c.Status == Alive && idx[p] < len(plans[p]) && used[p]+plans[p][idx[p]].Cost+c.Delay <= seg
 201	}
 202	advance := func(p int, seg int) {
 203		used[p] += plans[p][idx[p]].Cost
 204		idx[p]++
 205		if st.Chars[p].Status == Alive && idx[p] < len(plans[p]) {
 206			reveal(board, p, &st, plans[p][idx[p]], seg, &evs)
 207		}
 208	}
 209	for seg := 1; seg <= Segments; seg++ {
 210		var (
 211			gained  = make([]int, n)
 212			moved   = make([]bool, n)
 213			shot    = make([]bool, n)
 214			results = make([][]pending, n)
 215			first   = len(evs) // events of this segment start here
 216		)
 217		// Shots and throws are resolved before the other actions (7.52,
 218		// 16.14), simultaneously.
 219		for p := 0; p < n; p++ {
 220			if !due(p, seg) || !(plans[p][idx[p]].IsShot() || plans[p][idx[p]].IsThrow()) {
 221				continue
 222			}
 223			var (
 224				target int
 225				pens   []Penalty
 226			)
 227			if plans[p][idx[p]].IsThrow() {
 228				target, pens = throw(board, p, &st, plans[p][idx[p]], seg, d, &evs)
 229			} else {
 230				target, pens = shoot(board, p, &st, plans[p][idx[p]], seg, d, &evs)
 231			}
 232			if target >= 0 && len(pens) > 0 {
 233				results[target] = append(results[target], pending{from: p, pens: pens})
 234			}
 235			shot[p] = true
 236			advance(p, seg)
 237		}
 238		stamp(evs[first:], StepShots)
 239		for p := 0; p < n; p++ {
 240			if shot[p] || !due(p, seg) {
 241				continue
 242			}
 243			from := len(evs)
 244			a := plans[p][idx[p]]
 245			g, executed := execute(board, p, a, &st, seg, d, &evs)
 246			gained[p] += g
 247			advance(p, seg)
 248			if executed && cards.IsMove(a.Code) {
 249				moved[p] = true
 250			}
 251			stamp(evs[from:], StepAction+p)
 252		}
 253		end := len(evs)
 254
 255		// Occupied-hex penalty (9.24): when a mover ends the segment in
 256		// another character's hex, both draw one delay card (once each).
 257		for p := 0; p < n; p++ {
 258			if st.Chars[p].Status != Alive {
 259				continue
 260			}
 261			for o := 0; o < n; o++ {
 262				if o == p || st.Chars[o].Status != Alive || st.Chars[p].Hex != st.Chars[o].Hex || !(moved[p] || moved[o]) {
 263					continue
 264				}
 265				gained[p] += drawDelay(p, &st.Chars[p], seg, ReasonOccupied, d, &evs)
 266				break
 267			}
 268		}
 269
 270		// Combat results go into effect at the end of the segment (14.1).
 271		for p := 0; p < n; p++ {
 272			for _, r := range results[p] {
 273				gained[p] += applyPenalties(board, p, &st, r, seg, d, &evs)
 274			}
 275		}
 276		// A target out of the aim zone (12.41) or out of sight (31.1) at the
 277		// end of the segment loses the aim.
 278		for p := 0; p < n; p++ {
 279			c := &st.Chars[p]
 280			if c.Status != Alive || c.Aim == 0 {
 281				continue
 282			}
 283			var t *Character // the markers follow a character...
 284			if c.AimAt >= 0 {
 285				t = st.char(c.AimAt)
 286				if t == nil {
 287					loseAim(p, c, seg, ReasonNoTarget, &evs)
 288					continue
 289				}
 290				if t.Status == RanAway { // gone off the map: nothing left to aim at
 291					loseAim(p, c, seg, ReasonNoTarget, &evs)
 292					continue
 293				}
 294				if t.Status != Alive { // the aim stays on a fallen target (12.6)
 295					continue
 296				}
 297			}
 298			if !board.InAimZone(c.Hex, c.Facing, aimTarget(c, &st)) {
 299				loseAim(p, c, seg, ReasonOutOfZone, &evs)
 300				continue
 301			}
 302			if clear, blocker := canSee(board, c, c.AimHex, t); !clear { // ...or sit on a hex
 303				loseAimAt(p, c, seg, ReasonNoLOS, blocker, &evs)
 304			}
 305		}
 306
 307		stamp(evs[end:], StepEnd)
 308
 309		// Delay gained this segment takes effect next segment (6.1, [H6]);
 310		// the events carry the running total as the cards come.
 311		for p := 0; p < n; p++ {
 312			startDelay[p] = st.Chars[p].Delay
 313			if st.Chars[p].Status == Killed {
 314				continue // the delay gained before the kill is moot
 315			}
 316			st.Chars[p].Delay += gained[p]
 317		}
 318		runningDelay(evs[first:], startDelay)
 319	}
 320
 321	// Cancel any action not executed by the end of segment 5 (7.53).
 322	for p := 0; p < n; p++ {
 323		if st.Chars[p].Status != Alive {
 324			continue
 325		}
 326		for i := idx[p]; i < len(plans[p]); i++ {
 327			evs = append(evs, Event{Seg: EndOfTurn, Player: p, Kind: EvCancel, Action: plans[p][i].Code, Reason: ReasonTime})
 328		}
 329	}
 330	return st, evs
 331}
 332
 333// aimTarget is the hex the AIM markers of c sit on: the hex of the
 334// character they follow, or a hex.
 335func aimTarget(c *Character, st *State) string {
 336	if t := st.char(c.AimAt); c.AimAt >= 0 && t != nil {
 337		return t.Hex
 338	}
 339	return c.AimHex
 340}
 341
 342// standingIn is the seat of the first alive character but seat p standing
 343// in hex id, NoSeat when nobody does (two characters may share a hex:
 344// a shot at the hex finds the first in seat order).
 345func standingIn(st *State, id string, p int) int {
 346	for q := range st.Chars {
 347		if q != p && st.Chars[q].Status == Alive && st.Chars[q].Hex == id {
 348			return q
 349		}
 350	}
 351	return NoSeat
 352}
 353
 354// runningDelay stamps each delay event with the delay points its player
 355// holds once that card is counted, starting from the given totals.
 356func runningDelay(evs []Event, start []int) {
 357	total := append([]int(nil), start...)
 358	for i := range evs {
 359		if evs[i].Kind == EvDelay {
 360			total[evs[i].Player] += evs[i].N
 361			evs[i].Delay = total[evs[i].Player]
 362		}
 363	}
 364}
 365
 366// reveal applies rules 12.44, 12.5 and 17.11 when an action becomes the
 367// next one: the aim is lost unless the action is a Turn, an aim, a shot or
 368// a throw (which spend it when they go off); cocking, uncocking or doing
 369// nothing with a gun lose it like any other action. An aim, a shot or a
 370// throw naming a new target keeps the markers only within one hex of their
 371// place, and never moves them into or out of the character's own hex (both
 372// targets in it, or both out of it); a shot or a throw moves them onto its
 373// target.
 374func reveal(board *hex.Board, p int, st *State, a Action, seg int, evs *[]Event) {
 375	for _, m := range a.Moves {
 376		moveGun(p, st, m, seg, evs)
 377	}
 378	c := &st.Chars[p]
 379	if c.Aim == 0 {
 380		return
 381	}
 382	switch {
 383	case a.Code == cards.Turn:
 384	case !cards.TakesTarget(a.Code) || !takesTarget(a.Opt):
 385		loseAim(p, c, seg, ReasonReveal, evs)
 386	default:
 387		// The target named: the markers move onto it within one hex (12.5).
 388		named := a.Hex
 389		if t := st.char(a.Target); a.Target >= 0 && t != nil {
 390			named = t.Hex
 391		}
 392		old := aimTarget(c, st)
 393		same := (a.Target >= 0 && a.Target == c.AimAt) || (a.Target < 0 && c.AimAt < 0 && a.Hex == c.AimHex)
 394		if board.Distance(old, named) > 1 {
 395			loseAim(p, c, seg, ReasonTransfer, evs)
 396		} else if !same && (old == c.Hex) != (named == c.Hex) {
 397			loseAim(p, c, seg, ReasonOwnHex, evs)
 398		} else if a.Opt == OptShoot || a.Opt == OptThrow {
 399			c.AimHex = a.Hex // "" when the shot names a character
 400			c.AimAt = a.Target
 401		}
 402	}
 403}
 404
 405// pointFrom is the seat that gave the i-th of a wound's points, counted
 406// seat by seat through from; NoSeat past them (points nobody is put down
 407// for).
 408func pointFrom(from [MaxPlayers]int, i int) int {
 409	for q, n := range from {
 410		if i < n {
 411			return q
 412		}
 413		i -= n
 414	}
 415	return NoSeat
 416}
 417
 418// hasKill tells whether a hit's penalties include a KILL.
 419func hasKill(pens []Penalty) bool {
 420	for _, pen := range pens {
 421		if pen.Kind == Kill {
 422			return true
 423		}
 424	}
 425	return false
 426}
 427
 428func loseAim(p int, c *Character, seg int, reason string, evs *[]Event) {
 429	loseAimAt(p, c, seg, reason, "", evs)
 430}
 431
 432// loseAimAt is loseAim with the hex the event names: the obstacle in the
 433// way when the target is out of sight.
 434func loseAimAt(p int, c *Character, seg int, reason, to string, evs *[]Event) {
 435	if c.Aim == 0 {
 436		return
 437	}
 438	clearAim(c)
 439	*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvLoseAim, Reason: reason, To: to})
 440}
 441
 442// cardLine is which line of a drawn result card is read.
 443type cardLine int
 444
 445const (
 446	delayLine   cardLine = iota // delay card (14.21): DELAY line, no endurance
 447	fatigueLine                 // fatigue card (14.22): DELAY line, crosses off endurance
 448	woundLine                   // wound card (14.23): WOUND line, crosses off endurance
 449)
 450
 451// drawCard draws one result card for c and reads the chosen line: a number
 452// is delay points (and endurance boxes for fatigue and wound cards, put
 453// down to seat by, the enemy whose hit they come from, NoSeat for none); a
 454// penalty applies as in rule 14.11 (LOSE AIM, WILD SHOT fires every cocked
 455// gun, DROP turns the character DOWN and draws three more delay cards). One
 456// delay event is recorded per card drawn. It returns the delay points gained.
 457func drawCard(p int, c *Character, seg int, reason string, line cardLine, by int, d Drawer, evs *[]Event) (gained int) {
 458	card := d.Draw()
 459	res := card.Delay
 460	if line == woundLine {
 461		res = card.Wound
 462	}
 463	ev := Event{Seg: seg, Player: p, Kind: EvDelay, Reason: reason, Card: card.N, Result: res.String()}
 464	if res.IsPoints() {
 465		gained = int(res)
 466		ev.N = gained
 467		if line != delayLine {
 468			ev.Endurance = gained
 469			lost := gained
 470			if lost > c.Endurance {
 471				lost = c.Endurance
 472			}
 473			c.Endurance -= lost
 474			if by >= 0 && by < MaxPlayers {
 475				c.Hurt[by] += lost
 476			}
 477		}
 478	}
 479	*evs = append(*evs, ev)
 480	switch res {
 481	case LoseAim:
 482		loseAim(p, c, seg, res.String(), evs)
 483	case WildShot:
 484		wildShot(p, c, seg, evs)
 485		loseAim(p, c, seg, res.String(), evs)
 486	case Drop:
 487		gained += drop(p, c, seg, d, evs)
 488	}
 489	return gained
 490}
 491
 492// drawDelay draws one delay card (14.21).
 493func drawDelay(p int, c *Character, seg int, reason string, d Drawer, evs *[]Event) int {
 494	return drawCard(p, c, seg, reason, delayLine, NoSeat, d, evs)
 495}
 496
 497// drop is the DROP penalty (14.11): an upright character turns DOWN, draws
 498// three delay cards and loses its aim; no effect when already DOWN.
 499func drop(p int, c *Character, seg int, d Drawer, evs *[]Event) (gained int) {
 500	if c.Down {
 501		return 0
 502	}
 503	c.Down = true
 504	c.Dropped = true
 505	*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvFlip, Down: true})
 506	for i := 0; i < 3; i++ {
 507		gained += drawDelay(p, c, seg, ReasonDrop, d, evs)
 508	}
 509	loseAim(p, c, seg, ReasonDrop, evs)
 510	return gained
 511}
 512
 513// wildShot fires every cocked gun of c without effect (rule 14.11).
 514func wildShot(p int, c *Character, seg int, evs *[]Event) {
 515	for i := range c.Guns {
 516		if c.Guns[i].Type != NoGun && c.Guns[i].Cocked {
 517			fireHarmlessly(p, &c.Guns[i], seg, evs)
 518		}
 519	}
 520}
 521
 522// fireHarmlessly is one gun going off without hitting anything: uncocked,
 523// one shell less.
 524func fireHarmlessly(p int, g *Gun, seg int, evs *[]Event) {
 525	g.Cocked = false
 526	if g.Shells > 0 {
 527		g.Shells--
 528	}
 529	*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvWildShot, From: g.Location.String(), Gun: g.Type, GunID: g.ID})
 530}
 531
 532// shoot resolves a shot (13): the gun must be cocked, loaded and in a box it
 533// fires from, else the action is cancelled; the target is hit only inside
 534// the aim zone, a shot out of it still draws its card and misses (11.32,
 535// 13.1, 13.2). A MALFUNCTION with no effect is replaced; a
 536// misfire cancels the shot and a second draw may jam or blow up the gun.
 537// The shooter then uncocks, spends a shell and loses the aim (13.9). A shot
 538// at a hex nobody stands in when it comes up, at a target out of sight
 539// (31), or at a character out of the fight (killed or passed out, 12.6,
 540// 14.6; surrendered or run away), is held, as a Shoot doing nothing: no card, no shell, the gun stays
 541// cocked and the aim is kept. It returns the seat shot at (NoSeat when
 542// nobody was) and the IMPACT TABLE results of a hit, inflicted at the end
 543// of the segment.
 544func shoot(board *hex.Board, p int, st *State, a Action, seg int, d Drawer, evs *[]Event) (int, []Penalty) {
 545	c := &st.Chars[p]
 546	cancelAt := func(reason, to string) []Penalty {
 547		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvCancel, Action: a.Code, Reason: reason, To: to})
 548		return nil
 549	}
 550	cancel := func(reason string) []Penalty { return cancelAt(reason, "") }
 551	// The target: the character named, or whoever stands in the hex named
 552	// when the shot comes up; nobody there, the shot is held.
 553	target := a.Target
 554	at := a.Hex
 555	if target < 0 {
 556		if !board.Has(at) {
 557			return NoSeat, cancel(ReasonNoHex)
 558		}
 559		target = standingIn(st, at, p)
 560		if target < 0 {
 561			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code, To: at, Reason: ReasonEmptyHex})
 562			return NoSeat, nil
 563		}
 564	}
 565	t := st.char(target)
 566	if t != nil {
 567		at = t.Hex
 568	}
 569	// A character named who fell before the shot came up can no longer be
 570	// shot at (12.6, 14.6): the player would have named another target, or
 571	// done nothing (11.3), when revealing it; a plan fixed at the start of
 572	// the turn cannot, so the shot is held.
 573	if t != nil && target != p && t.Status != Alive {
 574		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code, To: at, Target: target, Reason: ReasonOutOfFight})
 575		return NoSeat, nil
 576	}
 577	// The target, a character either way by now, out of sight (31) holds
 578	// the shot like an empty hex: nothing fires, the aim stays where it is.
 579	if t != nil && t.Status == Alive && target != p {
 580		if clear, _ := canSee(board, c, at, t); !clear {
 581			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code, To: at, Target: target, Reason: ReasonNoLOS})
 582			return target, nil
 583		}
 584	}
 585	slot := c.Guns.Firing()
 586	if slot < 0 {
 587		return target, cancel(ReasonNoGun)
 588	}
 589	g := &c.Guns[slot]
 590	switch {
 591	case g.Exploded:
 592		return target, cancel(ReasonBroken)
 593	case g.Jammed:
 594		return target, cancel(ReasonJammed)
 595	case g.Shells == 0:
 596		return target, cancel(ReasonEmpty)
 597	case !g.Cocked:
 598		return target, cancel(ReasonNotCocked)
 599	case t == nil || target == p || t.Status != Alive:
 600		return target, cancel(ReasonNoTarget)
 601	}
 602	spec := g.Type.Spec()
 603	aim := a.AimTime(spec.OneHanded) + c.Aim
 604	if g.Location == GunHand || g.Location == BothHands {
 605		aim -= c.GunArm
 606	}
 607	if g.Location == OtherHand || g.Location == BothHands {
 608		aim -= c.OtherArm
 609	}
 610	rng := board.Distance(c.Hex, at)
 611	// Out of the aim zone the shot automatically misses, but the card is
 612	// drawn all the same (11.32, 13.1, 13.2).
 613	inZone := board.InAimZone(c.Hex, c.Facing, at)
 614
 615	// Draw (13.2): a MALFUNCTION with no effect is replaced; a misfire
 616	// cancels the shot and draws again for the gun (13.3, 13.31).
 617	var card ResultCard
 618	var malfs []Event // logged after the shot
 619	misfire := false
 620	for tries := 0; tries < DeckSize; tries++ {
 621		card = d.Draw()
 622		if card.IsFire() {
 623			break
 624		}
 625		if !card.Malf.Handloaded { // the Colt 45 takes handloaded shells (26.2)
 626			malfs = append(malfs, Event{Seg: seg, Player: p, Kind: EvMalfunction, Card: card.N, Result: MalfNoEffect, Gun: g.Type, GunID: g.ID})
 627			continue
 628		}
 629		misfire = true
 630		malfs = append(malfs, Event{Seg: seg, Player: p, Kind: EvMalfunction, Card: card.N, Result: MalfMisfire, Gun: g.Type, GunID: g.ID})
 631		second := d.Draw()
 632		result := MalfNoPenalty
 633		if !second.IsFire() {
 634			result = second.Malf.Second
 635			if result == GunJams {
 636				g.Shells = 0
 637				g.Jammed = true
 638			} else {
 639				g.Exploded = true
 640			}
 641		}
 642		malfs = append(malfs, Event{Seg: seg, Player: p, Kind: EvMalfunction, Card: second.N, Result: result, Gun: g.Type, GunID: g.ID})
 643		break
 644	}
 645	hit := Miss
 646	ev := Event{Seg: seg, Player: p, Kind: EvShot, Action: a.Code, Card: card.N, Target: target, To: at, Range: rng, N: aim, Gun: g.Type, GunID: g.ID}
 647	switch {
 648	case misfire:
 649		ev.Reason = ReasonMisfire
 650	case !inZone:
 651		ev.Reason = ReasonOutOfZone
 652	default:
 653		prelim := card.Fire.Lookup(aim, rng)
 654		ev.Chart = string(prelim)
 655		hit, ev.Status = targetStatus(card, prelim, t)
 656		if hit == Bullseye { // the shooter picks the location (13.7): the deadliest
 657			hit = VitalHit
 658		}
 659	}
 660	ev.Hit = string(hit)
 661	// The shot first, then the MALFUNCTION cards it drew.
 662	*evs = append(*evs, ev)
 663	*evs = append(*evs, malfs...)
 664
 665	// After the shot (13.9), even on a misfire.
 666	g.Cocked = false
 667	if g.Shells > 0 {
 668		g.Shells--
 669	}
 670	loseAim(p, c, seg, ReasonShot, evs)
 671	if hit == Miss {
 672		return target, nil
 673	}
 674	return target, Impact(spec.Impact, hit)
 675}
 676
 677// throw resolves a THROW (17): the weapon in the proper box (one-handed in
 678// the gun hand or both hands, 16.2) is thrown at the target named, a
 679// character or whoever stands in the hex named when it comes up, held like
 680// a shot when there is nobody to hit (an empty hex, a target out of sight
 681// or out of the fight: nothing thrown, the aim kept). Bare hands cannot
 682// throw, and a target farther than the weapon's wielding factor (plus
 683// STRENGTH, 20.22) cannot be thrown at (17.2): both cancel it. A hit card
 684// is drawn, MALFUNCTION cards ignored (17.3); aim time and range read the
 685// hit chart, Target Status applies, a bullseye goes where it hurts most,
 686// and a target out of the aim zone is missed (17.12). The weapon lands in
 687// the target's hex on a hit, else in the hex next to it a direction card
 688// names (17.4): from the target's facing for a character named, towards
 689// the thrower being ahead for a hex named; "long" carries on the thrower's
 690// line, "same" and a hex off the map leave it in the target's hex. The
 691// thrower then loses its aim (16.13). It returns the seat thrown at and
 692// the STRIKING TABLE results of a hit, with STRENGTH's damage (20.21),
 693// inflicted at the end of the segment.
 694func throw(board *hex.Board, p int, st *State, a Action, seg int, d Drawer, evs *[]Event) (int, []Penalty) {
 695	c := &st.Chars[p]
 696	cancel := func(reason string) []Penalty {
 697		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvCancel, Action: a.Code, Reason: reason})
 698		return nil
 699	}
 700	target := a.Target
 701	at := a.Hex
 702	if target < 0 {
 703		if !board.Has(at) {
 704			return NoSeat, cancel(ReasonNoHex)
 705		}
 706		target = standingIn(st, at, p)
 707		if target < 0 {
 708			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code, To: at, Reason: ReasonEmptyHex})
 709			return NoSeat, nil
 710		}
 711	}
 712	t := st.char(target)
 713	if t == nil || target == p {
 714		return NoSeat, cancel(ReasonNoTarget)
 715	}
 716	at = t.Hex
 717	if t.Status != Alive {
 718		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code, To: at, Target: target, Reason: ReasonOutOfFight})
 719		return NoSeat, nil
 720	}
 721	if clear, _ := canSee(board, c, at, t); !clear {
 722		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code, To: at, Target: target, Reason: ReasonNoLOS})
 723		return target, nil
 724	}
 725	slot := c.Guns.Firing()
 726	if slot < 0 {
 727		return target, cancel(ReasonNoGun)
 728	}
 729	g := c.Guns[slot]
 730	spec := g.Type.Spec()
 731	rng := board.Distance(c.Hex, at)
 732	if rng > spec.Wielding+a.Wield {
 733		return target, cancel(ReasonOutOfRange)
 734	}
 735	aim := a.AimTime(spec.OneHanded) + c.Aim
 736	if g.Location == GunHand || g.Location == BothHands {
 737		aim -= c.GunArm
 738	}
 739	if g.Location == OtherHand || g.Location == BothHands {
 740		aim -= c.OtherArm
 741	}
 742	inZone := board.InAimZone(c.Hex, c.Facing, at)
 743	var card ResultCard
 744	for tries := 0; tries < DeckSize; tries++ {
 745		if card = d.Draw(); card.IsFire() {
 746			break
 747		}
 748	}
 749	hit := Miss
 750	ev := Event{Seg: seg, Player: p, Kind: EvThrow, Action: a.Code, Card: card.N, Target: target, To: at, Range: rng, N: aim, Gun: g.Type, GunID: g.ID}
 751	if !inZone {
 752		ev.Reason = ReasonOutOfZone
 753	} else if card.IsFire() {
 754		prelim := card.Fire.Lookup(aim, rng)
 755		ev.Chart = string(prelim)
 756		hit, ev.Status = targetStatus(card, prelim, t)
 757		if hit == Bullseye { // the thrower's choice
 758			hit = StrikingBullseye(spec.Striking)
 759		}
 760	}
 761	ev.Hit = string(hit)
 762	*evs = append(*evs, ev)
 763	lands := at
 764	if hit == Miss {
 765		dirCard := d.Draw()
 766		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvDirection, Card: dirCard.N, To: string(dirCard.Hex)})
 767		if dir, ok := landing(board, c, t, a.Target >= 0, dirCard.Hex); ok {
 768			if to, ok := board.Neighbor(at, dir); ok {
 769				lands = to
 770			}
 771		}
 772	}
 773	putDown(p, c, slot, lands, ReasonThrow, seg, st, evs)
 774	loseAim(p, c, seg, ReasonThrow, evs)
 775	if hit == Miss {
 776		return target, nil
 777	}
 778	return target, Strengthen(Striking(spec.Striking, hit), a.Damage)
 779}
 780
 781// landing turns the direction card drawn for a missed throw into the
 782// direction from the target to the hex where the weapon lands (17.4):
 783// relative to the facing of a character named, or to the direction
 784// towards the thrower for a hex named; "long" carries on the thrower's
 785// line, "same" names no direction.
 786func landing(board *hex.Board, thrower, t *Character, named bool, h hex.Rel) (hex.Dir, bool) {
 787	if named || h == hex.Same || h == hex.Long {
 788		return directionOf(board, t, thrower, h)
 789	}
 790	back := (board.LongDir(thrower.Hex, t.Hex, thrower.Facing) + 3) % 6
 791	return hex.Resolve(back, h), true
 792}
 793
 794// targetStatus applies the card's Target Status entries to a preliminary
 795// hit (13.6): for Move and Run, a bullseye becomes the card's lesser hit
 796// and any other hit a miss; for Down, any hit but a bullseye misses. Head
 797// counters are not played. It also returns the letters of the entries
 798// that changed the hit.
 799func targetStatus(card ResultCard, hit HitLoc, t *Character) (HitLoc, string) {
 800	if hit == Miss {
 801		return Miss, ""
 802	}
 803	applied := ""
 804	lesser := card.Fire.Lesser()
 805	listed := func(s byte) bool { return strings.IndexByte(card.Status, s) >= 0 }
 806	for _, s := range []struct {
 807		letter byte
 808		on     bool
 809	}{{StatusMove, t.PlayedFoot || t.Dropped}, {StatusRun, t.PlayedRun}} {
 810		if !listed(s.letter) || !s.on {
 811			continue
 812		}
 813		applied += string(s.letter)
 814		if hit != Bullseye {
 815			return Miss, applied
 816		}
 817		hit = lesser
 818	}
 819	if listed(StatusDown) && t.Down && hit != Bullseye {
 820		return Miss, applied + string(StatusDown)
 821	}
 822	return hit, applied
 823}
 824
 825// applyPenalties inflicts the results of a hit on seat p (14.11, 14.12),
 826// stopping once the character is out of the fight; the shooter is put down
 827// as the one who did it (15.2, 15.3). It returns the delay points gained.
 828func applyPenalties(board *hex.Board, p int, st *State, r pending, seg int, d Drawer, evs *[]Event) (gained int) {
 829	c := &st.Chars[p]
 830	// Killed by an earlier shooter's KILL of this same segment (a character
 831	// already out cannot be shot): this KILL lands simultaneously, they
 832	// share the kill (15.2).
 833	if c.Status == Killed && hasKill(r.pens) {
 834		c.Killers[r.from] = true
 835	}
 836	for _, pen := range r.pens {
 837		if c.Status != Alive {
 838			break
 839		}
 840		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvWound, Result: string(pen.Kind), N: pen.N})
 841		switch pen.Kind {
 842		case Kill:
 843			kill(c, seat(r.from))
 844			dropAllGuns(p, c, seg, st, evs)
 845		case Stun:
 846			for i := 0; i < pen.N; i++ {
 847				gained += drawCard(p, c, seg, ReasonStun, woundLine, r.from, d, evs)
 848			}
 849			loseAim(p, c, seg, string(Stun), evs)
 850		case Stagger:
 851			gained += stagger(board, p, c, &st.Chars[r.from], seg, d, evs)
 852		case Serious:
 853			c.Serious += pen.N
 854			c.SeriousFrom[r.from] += pen.N
 855		case DropGunHand:
 856			dropHand(p, c, GunHand, OtherHand, seg, st, evs)
 857			loseAim(p, c, seg, string(DropGunHand), evs)
 858		case DropOtherHand:
 859			dropHand(p, c, OtherHand, GunHand, seg, st, evs)
 860			loseAim(p, c, seg, string(DropOtherHand), evs)
 861		case GunArm:
 862			c.GunArm += pen.N
 863		case OtherArm:
 864			c.OtherArm += pen.N
 865		case LegWound:
 866			c.Leg += pen.N
 867			c.LegFrom[r.from] += pen.N
 868		case Light:
 869			for i := 0; i < pen.N; i++ {
 870				gained += drawCard(p, c, seg, ReasonLight, fatigueLine, r.from, d, evs)
 871			}
 872		case LoseAimHit:
 873			loseAim(p, c, seg, string(LoseAimHit), evs)
 874		}
 875	}
 876	return gained
 877}
 878
 879// stagger is the STAGGER penalty (14.11): an upright character is thrown to
 880// the hex named by a first direction card, turns as named by a second one,
 881// and drops. A hex off the board keeps it in place.
 882func stagger(board *hex.Board, p int, c *Character, shooter *Character, seg int, d Drawer, evs *[]Event) (gained int) {
 883	if c.Down {
 884		return 0
 885	}
 886	first, second := d.Draw(), d.Draw()
 887	*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvDirection, Card: first.N, To: string(first.Hex)})
 888	*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvDirection, Card: second.N, To: string(second.Hex)})
 889	if dir, ok := directionOf(board, c, shooter, first.Hex); ok {
 890		if to, ok := board.Neighbor(c.Hex, dir); ok {
 891			from := c.Hex
 892			c.Hex = to
 893			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvMove, From: from, To: to, Facing: c.Facing, Down: c.Down, Reason: ReasonStagger})
 894			gained += terrain(board, p, c, from, dir, seg, d, evs)
 895		}
 896	}
 897	if dir, ok := directionOf(board, c, shooter, second.Hex); ok && dir != c.Facing {
 898		c.Facing = dir
 899		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvTurn, Facing: c.Facing, Reason: ReasonStagger})
 900	}
 901	return gained + drop(p, c, seg, d, evs)
 902}
 903
 904// terrain is the penalty of the ground crossed by a move or a stagger from
 905// the hex from in direction dir, to c's hex: two delay cards across a fence
 906// (35.2), two more onto or off an obstacle (36.4; none from one obstacle
 907// hex to another).
 908func terrain(board *hex.Board, p int, c *Character, from string, dir hex.Dir, seg int, d Drawer, evs *[]Event) (gained int) {
 909	if board.Fenced(from, dir) {
 910		for i := 0; i < 2; i++ {
 911			gained += drawDelay(p, c, seg, ReasonFence, d, evs)
 912		}
 913	}
 914	if board.Obstacle(from) != board.Obstacle(c.Hex) {
 915		for i := 0; i < 2; i++ {
 916			gained += drawDelay(p, c, seg, ReasonObstacle, d, evs)
 917		}
 918	}
 919	return gained
 920}
 921
 922// directionOf turns a direction card's HEX line into an absolute direction
 923// from the target c (14.3): relative lines are taken from its facing,
 924// "long" continues the shooter's line, "same" names no direction.
 925func directionOf(board *hex.Board, c *Character, shooter *Character, h hex.Rel) (hex.Dir, bool) {
 926	switch h {
 927	case hex.Same:
 928		return 0, false
 929	case hex.Long:
 930		return board.LongDir(shooter.Hex, c.Hex, shooter.Facing), true
 931	}
 932	return hex.Resolve(c.Facing, h), true
 933}
 934
 935// dropHand is the GUN HAND / OTHER HAND penalty (14.11): the weapon in that
 936// hand falls in the hex (a cocked gun going off), and a weapon in both
 937// hands moves to the other hand.
 938func dropHand(p int, c *Character, hand, other GunLocation, seg int, st *State, evs *[]Event) {
 939	if i := c.Guns.At(hand); i >= 0 {
 940		dropGun(p, c, i, true, "", seg, st, evs)
 941	}
 942	if i := c.Guns.At(BothHands); i >= 0 {
 943		c.Guns[i].Location = other
 944	}
 945}
 946
 947// ActionGunMove names a gun move in its cancel event.
 948const ActionGunMove cards.Code = "gun_move"
 949
 950// moveGun does a gun move as its action is revealed (10.22): the gun in
 951// BOTH HANDS to one hand, as it is, or a gun in a hand box dropped in the
 952// hex (a cocked one going off harmlessly). Dropping the gun it would fire
 953// (gun hand or both hands) loses the aim, the points spent aiming it (as a
 954// GUN HAND wound does, 12.42). A move the gun no longer allows (gone, not
 955// in a hand, not in both hands) is cancelled.
 956func moveGun(p int, st *State, m GunMove, seg int, evs *[]Event) {
 957	c := &st.Chars[p]
 958	cancel := func(reason string) {
 959		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvCancel, Action: ActionGunMove, Reason: reason, GunID: m.Gun})
 960	}
 961	i := c.Guns.Find(m.Gun)
 962	if i < 0 {
 963		cancel(ReasonNoGun)
 964		return
 965	}
 966	g := &c.Guns[i]
 967	if m.To == HandGround {
 968		if g.Location == Holstered {
 969			cancel(ReasonNotInHand)
 970			return
 971		}
 972		aimed := g.Location == GunHand || g.Location == BothHands
 973		dropGun(p, c, i, true, ReasonGunMove, seg, st, evs)
 974		if aimed && c.Aim > 0 {
 975			loseAim(p, c, seg, ReasonGunDropped, evs)
 976		}
 977		return
 978	}
 979	if g.Location != BothHands {
 980		cancel(ReasonNotBoth)
 981		return
 982	}
 983	// A gun in BOTH HANDS leaves no hand to another gun: the hand is free.
 984	to, _ := HandLocation(m.To)
 985	*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvMoveGun, From: g.Location.String(), To: to.String(), Gun: g.Type, GunID: g.ID})
 986	g.Location = to
 987}
 988
 989// dropGun lets the gun in slot i fall in the character's hex, where it lies
 990// unowned with a fresh ground id; fire makes a cocked gun go off as it
 991// falls (a released or knocked-out gun, 10.22, 14.11); reason goes on the
 992// drop event, ReasonGunMove for a gun let go.
 993func dropGun(p int, c *Character, i int, fire bool, reason string, seg int, st *State, evs *[]Event) {
 994	if fire && c.Guns[i].Cocked {
 995		fireHarmlessly(p, &c.Guns[i], seg, evs)
 996	}
 997	putDown(p, c, i, c.Hex, reason, seg, st, evs)
 998}
 999
1000// putDown takes the gun in slot i off c's sheet and lays it in hex at,
1001// unowned with a fresh ground id, as it is.
1002func putDown(p int, c *Character, i int, at, reason string, seg int, st *State, evs *[]Event) {
1003	g := &c.Guns[i]
1004	dropped := *g
1005	dropped.Location = Holstered
1006	st.GroundSeq++
1007	st.Ground = append(st.Ground, GroundGun{ID: st.GroundSeq, Hex: at, Gun: dropped})
1008	*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvDropGun, From: g.Location.String(), To: at, Gun: g.Type, GunID: g.ID, N: st.GroundSeq, Reason: reason})
1009	*g = Gun{}
1010}
1011
1012// dropAllGuns is what happens to the weapons of a character killed or
1013// passed out (14.6): they all fall in the hex as they are, where anyone can
1014// take them; nothing goes off.
1015func dropAllGuns(p int, c *Character, seg int, st *State, evs *[]Event) {
1016	for i := range c.Guns {
1017		if c.Guns[i].Type != NoGun {
1018			dropGun(p, c, i, false, "", seg, st, evs)
1019		}
1020	}
1021}
1022
1023// execute runs one action other than a shot, mutating seat p's character
1024// (and st for the guns on the ground). It returns the delay points gained
1025// and whether the action took effect.
1026func execute(board *hex.Board, p int, a Action, st *State, seg int, d Drawer, evs *[]Event) (gained int, executed bool) {
1027	c := &st.Chars[p]
1028	cancelAt := func(reason, to string) (int, bool) {
1029		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvCancel, Action: a.Code, Reason: reason, To: to})
1030		return gained, false
1031	}
1032	cancel := func(reason string) (int, bool) { return cancelAt(reason, "") }
1033	// move takes the character one hex in dir; off the map it runs away
1034	// (15.5): it leaves the showdown, guns and all, and the move counts as
1035	// done.
1036	move := func(dir hex.Dir) bool {
1037		to, ok := board.Neighbor(c.Hex, dir)
1038		if !ok {
1039			c.Status = RanAway
1040			clearAim(c)
1041			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvRunAway, Action: a.Code, From: c.Hex, Facing: dir})
1042			return true
1043		}
1044		from := c.Hex
1045		c.Hex = to
1046		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvMove, Action: a.Code, From: from, To: to, Facing: c.Facing, Down: c.Down})
1047		if c.Down { // crawl penalty (9.2): a fixed 2 delay, no card
1048			gained += 2
1049			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvDelay, N: 2, Reason: ReasonCrawl})
1050		} else {
1051			for i := 0; i < c.Leg; i++ { // LEG wound (14.12), each card put down to the seat whose wound gave it
1052				gained += drawCard(p, c, seg, ReasonLeg, fatigueLine, pointFrom(c.LegFrom, i), d, evs)
1053			}
1054		}
1055		gained += terrain(board, p, c, from, dir, seg, d, evs)
1056		return true
1057	}
1058
1059	switch a.Code {
1060	case cards.Advance, cards.Run, cards.BackUp:
1061		ok := move(hex.Resolve(c.Facing, a.Dir))
1062		return gained, ok
1063	case cards.Sprint:
1064		if !move(c.Facing) || c.Status != Alive { // straight ahead (9.23); gone off the map, nothing more
1065			return gained, true
1066		}
1067		gained += drawDelay(p, c, seg, ReasonSprint, d, evs)
1068		return gained, true
1069	case cards.Turn, cards.SpinAround:
1070		c.Facing = hex.Resolve(c.Facing, a.Dir)
1071		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvTurn, Action: a.Code, Facing: c.Facing})
1072		return 0, true
1073	case cards.GetUpDown:
1074		c.Down = !c.Down
1075		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvFlip, Action: a.Code, Down: c.Down})
1076		return 0, true
1077	case cards.LeapDrop:
1078		c.Down = !c.Down
1079		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvFlip, Action: a.Code, Down: c.Down})
1080		// Two delay cards (9.4).
1081		gained += drawDelay(p, c, seg, ReasonLeap, d, evs)
1082		gained += drawDelay(p, c, seg, ReasonLeap, d, evs)
1083		return gained, true
1084	case cards.DrawAndCock:
1085		dest, ok := HandLocation(a.Hand)
1086		if !ok {
1087			return cancel(ReasonNoGun)
1088		}
1089		if a.Ground > 0 {
1090			// Pick a weapon up from the hex into the hand and cock it (11.1,
1091			// 11.11): it gets a fresh id on this character's sheet.
1092			gi := -1
1093			for i, gg := range st.Ground {
1094				if gg.ID == a.Ground && gg.Hex == c.Hex {
1095					gi = i
1096				}
1097			}
1098			if gi < 0 {
1099				return cancel(ReasonNoGun)
1100			}
1101			if c.Guns.At(dest) >= 0 {
1102				return cancel(ReasonHandFull)
1103			}
1104			slot := c.Guns.Free()
1105			if slot < 0 {
1106				return cancel(ReasonNoRoom)
1107			}
1108			g := st.Ground[gi].Gun
1109			g.ID = c.Guns.NextID()
1110			g.Location = dest
1111			g.Cocked = !g.Jammed // a jammed gun cannot be cocked until completely reloaded (13.31)
1112			c.Guns[slot] = g
1113			st.Ground = append(st.Ground[:gi:gi], st.Ground[gi+1:]...)
1114			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvDraw, Action: a.Code, From: "ground", To: dest.String(), Gun: g.Type, GunID: g.ID, N: a.Ground, Result: jammedResult(g)})
1115			return 0, true
1116		}
1117		// Move the chosen gun from its box (a holster, or a hand) to the
1118		// one named and cock it (11.1).
1119		slot := c.Guns.Find(a.Gun)
1120		if slot < 0 || c.Guns[slot].Location == dest {
1121			return cancel(ReasonNoGun)
1122		}
1123		if c.Guns.Blocked(dest, slot) {
1124			return cancel(ReasonHandFull)
1125		}
1126		g := &c.Guns[slot]
1127		from := g.Location
1128		g.Location = dest
1129		g.Cocked = !g.Jammed // a jammed gun cannot be cocked until completely reloaded (13.31)
1130		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvDraw, Action: a.Code, From: from.String(), To: dest.String(), Gun: g.Type, GunID: g.ID, Result: jammedResult(*g)})
1131		return 0, true
1132	case cards.Load:
1133		// One shell into the gun in BOTH HANDS (11.2, 27.2), which uncocks
1134		// it; a jammed gun fires again once completely reloaded (13.31).
1135		slot := c.Guns.At(BothHands)
1136		if slot < 0 {
1137			return cancel(ReasonNoGun)
1138		}
1139		g := &c.Guns[slot]
1140		if g.Exploded {
1141			return cancel(ReasonBroken)
1142		}
1143		capacity := g.Type.Spec().Capacity
1144		if g.Shells >= capacity {
1145			return cancel(ReasonFull)
1146		}
1147		g.Shells++
1148		g.Cocked = false
1149		ev := Event{Seg: seg, Player: p, Kind: EvLoad, Action: a.Code, N: g.Shells, Gun: g.Type, GunID: g.ID}
1150		if g.Jammed && g.Shells == capacity {
1151			g.Jammed = false
1152			ev.Result = LoadUnjammed
1153		}
1154		*evs = append(*evs, ev)
1155		return 0, true
1156	case cards.Throw: // a throw resolves with the shots; this one does nothing (17.1)
1157		*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code})
1158		return 0, true
1159	case cards.CockAimShoot, cards.Shoot:
1160		if a.Opt == OptNothing {
1161			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvNothing, Action: a.Code})
1162			return 0, true
1163		}
1164		slot := c.Guns.Firing() // a gun in the proper box (11.3)
1165		if slot < 0 {
1166			return cancel(ReasonNoGun)
1167		}
1168		g := &c.Guns[slot]
1169		if g.Exploded { // useless for the rest of the game (13.31)
1170			return cancel(ReasonBroken)
1171		}
1172		switch a.Opt {
1173		case OptCock:
1174			if g.Jammed { // not before it is completely reloaded (13.31)
1175				return cancel(ReasonJammed)
1176			}
1177			g.Cocked = true
1178			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvCock, Action: a.Code, Gun: g.Type, GunID: g.ID})
1179		case OptUncock:
1180			g.Cocked = false
1181			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvUncock, Action: a.Code, Gun: g.Type, GunID: g.ID})
1182		case OptAim:
1183			// Two AIM points (12.1, 12.2) on the target named: a character,
1184			// wherever it is, or a hex; either must be in the aim zone.
1185			hexAt := a.Hex
1186			var at *Character
1187			if a.Target >= 0 {
1188				at = st.char(a.Target)
1189				if at == nil || a.Target == p || at.Status != Alive {
1190					return cancel(ReasonNoTarget)
1191				}
1192				hexAt = at.Hex
1193			} else if !board.Has(a.Hex) {
1194				return cancel(ReasonNoHex)
1195			}
1196			if !board.InAimZone(c.Hex, c.Facing, hexAt) {
1197				return cancel(ReasonOutOfZone)
1198			}
1199			// ...and in sight (31.1): a target behind the trough cannot be aimed at.
1200			if clear, blocker := canSee(board, c, hexAt, at); !clear {
1201				return cancelAt(ReasonNoLOS, blocker)
1202			}
1203			c.Aim += 2
1204			if c.Aim > MaxAim {
1205				c.Aim = MaxAim
1206			}
1207			c.AimHex = a.Hex // "" for a character: the markers follow it
1208			c.AimAt = a.Target
1209			*evs = append(*evs, Event{Seg: seg, Player: p, Kind: EvAim, Action: a.Code, To: hexAt, Target: a.Target, N: c.Aim})
1210		}
1211		return 0, true
1212	}
1213	// Not implemented yet (loading, throwing, brawling): Validate rejects these upfront.
1214	return cancel("unsupported")
1215}