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}