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.

state.gno

9.22 Kb · 320 lines
  1// Package engine is the pure, deterministic rules engine of a gunfight of
  2// two to four characters: plan parsing and validation, and the 5-segment
  3// resolution of a turn. It has no chain dependency so it can be unit-tested
  4// with gno test.
  5package engine
  6
  7import "gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/hex/v0"
  8
  9const (
 10	// MaxActionPoints is the time points a plan may spend in a turn (rule 7.3).
 11	MaxActionPoints = 5
 12	// Segments is the number of segments in a turn (rule 4.1).
 13	Segments = 5
 14	// EndOfTurn is the Seg of events recorded after segment 5 (cancelled
 15	// actions, passing out, the end-of-showdown check); turn-start events
 16	// (SERIOUS fatigue, aim lost on the first reveal) have Seg 0.
 17	EndOfTurn = Segments + 1
 18	// MinPlayers and MaxPlayers bound the seats of a game: the showdown's
 19	// characters (Setups) fill the seats in order, each a side of its own.
 20	MinPlayers = 2
 21	MaxPlayers = 4
 22)
 23
 24// Status is the participation state of a character.
 25type Status int
 26
 27const (
 28	// Alive characters plan and act.
 29	Alive Status = iota
 30	// Out characters (surrendered, or out of time) are skipped by Resolve.
 31	Out
 32	// Killed characters are out of the game (14.6).
 33	Killed
 34	// PassedOut characters carried more delay than endurance for two turns (14.2).
 35	PassedOut
 36	// RanAway characters moved off the map (15.5): out of the showdown,
 37	// their hex the edge hex they left from.
 38	RanAway
 39)
 40
 41// String returns the status name used in JSON views.
 42func (s Status) String() string {
 43	switch s {
 44	case Alive:
 45		return "alive"
 46	case Out:
 47		return "out"
 48	case Killed:
 49		return "killed"
 50	case PassedOut:
 51		return "passed_out"
 52	case RanAway:
 53		return "ran_away"
 54	}
 55	return "?"
 56}
 57
 58// StartEndurance is the endurance boxes a character starts with (14.2).
 59const StartEndurance = 20
 60
 61// MaxAim is the most AIM points a character can hold on a target (12.3).
 62const MaxAim = 8
 63
 64// NoSeat is the seat value for "nobody": AIM markers on a hex, a wound or a
 65// death nobody inflicted.
 66const NoSeat = -1
 67
 68// Character is the mutable state of one gunslinger.
 69type Character struct {
 70	Hex    string
 71	Facing hex.Dir
 72	Down   bool
 73	Delay  int // delay points on the sheet (rule 6)
 74	Status Status
 75	// PlayedFoot and PlayedRun record whether the current turn's plan holds
 76	// a foot action / a Run, whether or not it executes ([H12], rules 13.61-62).
 77	PlayedFoot bool
 78	PlayedRun  bool
 79	// RanLastTurn is PlayedRun carried over from the previous turn: Sprint
 80	// needs a Run on the previous turn as well (rule 9.23, "build up speed").
 81	RanLastTurn bool
 82	// Dropped records a DROP penalty this turn ("Move" target status, 13.61).
 83	Dropped bool
 84	// Guns carried (section 10).
 85	Guns Guns
 86	// Aim is the AIM points on the target (12.2). AimAt is the seat of the
 87	// character the markers follow, wherever it goes (12.3), NoSeat when
 88	// they sit on the hex AimHex. Both are meaningless while Aim is 0.
 89	Aim    int
 90	AimAt  int
 91	AimHex string
 92	// Endurance is the boxes left on the sheet (14.2); wound and fatigue
 93	// cards cross them off.
 94	Endurance int
 95	// Exhausted counts the consecutive turn ends with more delay than
 96	// endurance; two in a row pass the character out (14.2).
 97	Exhausted int
 98	// Permanent wounds (14.12): fatigue cards per turn, aim time penalties
 99	// per hand, fatigue cards per hex moved upright.
100	Serious  int
101	GunArm   int
102	OtherArm int
103	Leg      int
104	// Who did it, for the victory points (15.2, 15.21, 15.3), by seat: the
105	// seats credited with the kill (several when their KILLs landed in the
106	// same segment, or tied for the most SERIOUS points), the SERIOUS and
107	// LEG points each seat's wounds gave (the fatigue those wounds cost
108	// later is put down to it), and the endurance boxes each seat's hits
109	// crossed off.
110	Killers     [MaxPlayers]bool
111	SeriousFrom [MaxPlayers]int
112	LegFrom     [MaxPlayers]int
113	Hurt        [MaxPlayers]int
114}
115
116// GroundGun is a weapon lying in a hex (10.21). ID identifies it on the
117// ground for the whole game (plans refer to it as "g<ID>"); the gun's own
118// ID was its previous owner's and is reassigned when picked up.
119type GroundGun struct {
120	ID  int
121	Hex string
122	Gun Gun
123}
124
125// State is the whole game state the engine acts on: one character per
126// seat, in seat order.
127type State struct {
128	Chars     []Character
129	Ground    []GroundGun
130	GroundSeq int // last ground id handed out
131}
132
133// NewState is a showdown of n characters before anyone set up: each
134// upright with its starting guns and endurance, standing nowhere yet (Hex
135// "") until SetUp gives it its hex and facing ([H3], setup.gno).
136func NewState(n int) State {
137	chars := make([]Character, n)
138	for i := range chars {
139		chars[i] = newCharacter()
140	}
141	return State{Chars: chars}
142}
143
144// newCharacter is a character's sheet at the start of a showdown.
145func newCharacter() Character {
146	return Character{Guns: startingGuns(), Endurance: StartEndurance, AimAt: NoSeat}
147}
148
149// char is the character of a seat, nil for a seat the state does not have.
150func (s *State) char(seat int) *Character {
151	if seat < 0 || seat >= len(s.Chars) {
152		return nil
153	}
154	return &s.Chars[seat]
155}
156
157// CloneChars copies the characters field by field. A plain copy of the
158// slice is not enough on a node: a character written earlier in the same
159// transaction stays shared with the live state, so a turn's stored start
160// would show its end position.
161func CloneChars(cs []Character) []Character {
162	out := make([]Character, len(cs))
163	for i, c := range cs {
164		out[i] = Character{
165			Hex:         c.Hex,
166			Facing:      c.Facing,
167			Down:        c.Down,
168			Delay:       c.Delay,
169			Status:      c.Status,
170			PlayedFoot:  c.PlayedFoot,
171			PlayedRun:   c.PlayedRun,
172			RanLastTurn: c.RanLastTurn,
173			Dropped:     c.Dropped,
174			Aim:         c.Aim,
175			AimAt:       c.AimAt,
176			AimHex:      c.AimHex,
177			Endurance:   c.Endurance,
178			Exhausted:   c.Exhausted,
179			Serious:     c.Serious,
180			GunArm:      c.GunArm,
181			OtherArm:    c.OtherArm,
182			Leg:         c.Leg,
183			Killers:     c.Killers,
184			SeriousFrom: c.SeriousFrom,
185			LegFrom:     c.LegFrom,
186			Hurt:        c.Hurt,
187		}
188		for j, g := range c.Guns {
189			out[i].Guns[j] = Gun{ID: g.ID, Type: g.Type, Location: g.Location, Cocked: g.Cocked, Shells: g.Shells, Jammed: g.Jammed, Exploded: g.Exploded}
190		}
191	}
192	return out
193}
194
195// Budget is the time points seat p may plan this turn: the carried delay
196// counts against the 5 points ([H5], rule 7.3).
197func (s State) Budget(p int) int {
198	b := MaxActionPoints - s.Chars[p].Delay
199	if b < 0 {
200		return 0
201	}
202	return b
203}
204
205// AliveCount counts the characters still in the fight.
206func AliveCount(s State) int {
207	n := 0
208	for _, c := range s.Chars {
209		if c.Status == Alive {
210			n++
211		}
212	}
213	return n
214}
215
216// EndTurn is the end-of-turn upkeep (rule 5.6): half of the delay points
217// are removed, rounded up, the per-turn flags are cleared, and a character
218// left with more delay than endurance for the second turn in a row passes
219// out (14.2; checked after the halving). The state given is left as it is.
220func EndTurn(s State) (State, []Event) {
221	s.Chars = CloneChars(s.Chars)
222	var evs []Event
223	for p := range s.Chars {
224		c := &s.Chars[p]
225		c.Delay -= (c.Delay + 1) / 2
226		c.RanLastTurn = c.PlayedRun
227		c.PlayedFoot = false
228		c.PlayedRun = false
229		c.Dropped = false
230		if c.Status != Alive {
231			continue
232		}
233		if c.Delay > c.Endurance {
234			c.Exhausted++
235		} else {
236			c.Exhausted = 0
237		}
238		if c.Exhausted >= 2 {
239			c.Status = PassedOut
240			c.Down = true
241			clearAim(c)
242			evs = append(evs, Event{Seg: EndOfTurn, Player: p, Kind: EvPassOut, Down: true})
243			dropAllGuns(p, c, EndOfTurn, &s, &evs)
244		}
245	}
246	return s, evs
247}
248
249// EndShowdown is the end-of-game check (14.5): every character with SERIOUS
250// wounds draws a result card and is killed when its DELAY number plus the
251// wound points reaches four; the enemy who gave the most SERIOUS points
252// gets the kill, enemies tied for the most share it (15.21). The state
253// given is left as it is.
254func EndShowdown(s State, d Drawer) (State, []Event) {
255	s.Chars = CloneChars(s.Chars)
256	var evs []Event
257	for p := range s.Chars {
258		c := &s.Chars[p]
259		if c.Status != Alive || c.Serious == 0 {
260			continue
261		}
262		card := d.Draw()
263		n := 0
264		if card.Delay.IsPoints() {
265			n = int(card.Delay)
266		}
267		evs = append(evs, Event{Seg: EndOfTurn, Player: p, Kind: EvSeriousCheck, Card: card.N, N: n + c.Serious})
268		if n+c.Serious >= 4 {
269			kill(c, mostSeats(c.SeriousFrom))
270			evs = append(evs, Event{Seg: EndOfTurn, Player: p, Kind: EvWound, Result: string(Kill)})
271			dropAllGuns(p, c, EndOfTurn, &s, &evs)
272		}
273	}
274	return s, evs
275}
276
277// kill takes a character out of the game (14.6), killed by the seats
278// marked in by (none when nobody's hit did it): down, no delay marker, no
279// endurance left.
280func kill(c *Character, by [MaxPlayers]bool) {
281	c.Status = Killed
282	c.Down = true
283	clearAim(c)
284	c.Delay = 0
285	c.Endurance = 0
286	c.Killers = by
287}
288
289// seat marks one seat, none for NoSeat.
290func seat(p int) (out [MaxPlayers]bool) {
291	if p >= 0 && p < MaxPlayers {
292		out[p] = true
293	}
294	return out
295}
296
297// mostSeats marks the seats with the most points, every one of them on a
298// tie; none when nobody has any.
299func mostSeats(pts [MaxPlayers]int) (out [MaxPlayers]bool) {
300	most := 0
301	for _, n := range pts {
302		if n > most {
303			most = n
304		}
305	}
306	if most == 0 {
307		return out
308	}
309	for q, n := range pts {
310		out[q] = n == most
311	}
312	return out
313}
314
315// clearAim takes the AIM markers off the sheet.
316func clearAim(c *Character) {
317	c.Aim = 0
318	c.AimAt = NoSeat
319	c.AimHex = ""
320}