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.

plan.gno

16.42 Kb · 534 lines
  1package engine
  2
  3import (
  4	"errors"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/cards/v0"
  9	"gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/hex/v0"
 10)
 11
 12const (
 13	// MaxPlanEntries bounds the number of actions in a plan; five 1-point
 14	// actions already fill the budget.
 15	MaxPlanEntries = MaxActionPoints
 16	// MaxPlanLen bounds the plan string length.
 17	MaxPlanLen = 128
 18)
 19
 20// Action is one card side chosen for the turn, with its committed choice.
 21type Action struct {
 22	Card   int
 23	Side   byte // cards.FrontSide or cards.BackSide
 24	Code   cards.Code
 25	Cost   int
 26	Dir    hex.Rel // committed relative direction, "" when the side takes none
 27	Gun    int     // id of the character's gun for a gun choice (Draw & Cock), 0 when none
 28	Ground int     // id of the gun on the ground to pick up (Draw & Cock, "g<id>"), 0 when none
 29	Hand   int     // destination hand code for a gun choice (HandGun...), -1 when the side takes none
 30	Opt    string  // option of Cock/Aim/Shoot, Shoot and Throw (OptCock...) or Strength (OptDamage, OptWield), "" when the side takes none
 31	Hex    string  // the hex named by an aim, a shot or a throw at a hex
 32	Target int     // the seat named by an aim, a shot or a throw at a character; -1 otherwise
 33	// Wield and Damage are what the STRENGTHs played on top of a Throw add
 34	// to the weapon's wielding factor and to the penalties (20.2), set by
 35	// Combine.
 36	Wield  int
 37	Damage int
 38	// Moves are the gun moves done when the action is revealed (10.22),
 39	// written before it in the plan string.
 40	Moves []GunMove
 41}
 42
 43// GunMove is what a character may do with its guns, for free, each time it
 44// reveals an action (10.22): move the gun in BOTH HANDS to one hand, or drop
 45// a gun from a hand box into its hex. The other hand, like Draw & Cock's,
 46// comes with a second gun: Validate refuses it for now. Its plan string entry, "h<gun id>:<to>",
 47// comes before the action it goes with: To is HandGun (0) or HandOther (1),
 48// or "g" (HandGround) to drop it ("h1:g,1f:ahead": drop the Colt as the
 49// Advance is revealed). Whether the gun is there, where the move needs
 50// it, is checked when it is done: it may be one picked up earlier in the
 51// plan.
 52type GunMove struct {
 53	Gun int // id of the gun on the character's sheet
 54	To  int // HandGun, HandOther or HandGround
 55}
 56
 57// String is the move's plan string entry.
 58func (m GunMove) String() string {
 59	to := strconv.Itoa(m.To)
 60	if m.To == HandGround {
 61		to = "g"
 62	}
 63	return "h" + strconv.Itoa(m.Gun) + ":" + to
 64}
 65
 66// Shoot options: what a Cock/Aim/Shoot does (11.3), or a Shoot (shoot or
 67// nothing). Aiming and shooting name their target (11.31): a hex of the
 68// aim zone ("5f:aim:A-F6", "7f:shoot:A-F6") or a character by seat
 69// ("5f:aim:@1", "7f:shoot:@1"). A shot at a hex hits the character standing
 70// there when it goes off, and is wasted otherwise.
 71const (
 72	OptCock    = "cock"
 73	OptUncock  = "uncock"
 74	OptAim     = "aim"
 75	OptShoot   = "shoot"
 76	OptNothing = "nothing"
 77	// OptThrow is what a Throw does instead of nothing (17.1), naming its
 78	// target like a shot ("6b:throw:@1"; a bare "6b" does nothing).
 79	OptThrow = "throw"
 80)
 81
 82// Strength options (20.2): a STRENGTH adds to the penalties ("7b:damage",
 83// also a bare "7b", what a STRENGTH does when its use is not stated) or
 84// to the wielding factor ("7b:wield") of the Throw it is played on top of.
 85const (
 86	OptDamage = "damage"
 87	OptWield  = "wield"
 88)
 89
 90// Card 7's STRENGTH adds 4 to the wielding factor or 2 to the penalties.
 91const (
 92	StrengthWield  = 4
 93	StrengthDamage = 2
 94)
 95
 96// IsShot reports whether the action fires the gun.
 97func (a Action) IsShot() bool { return cards.IsShooting(a.Code) && a.Opt == OptShoot }
 98
 99// IsThrow reports whether the action throws the weapon in hand (17.1).
100func (a Action) IsThrow() bool { return a.Code == cards.Throw && a.Opt == OptThrow }
101
102// AimTime is the aim time printed on a shooting action or a Throw for a
103// one- or two-handed weapon (13.52, 17.3).
104func (a Action) AimTime(oneHanded bool) int {
105	switch a.Code {
106	case cards.Throw:
107		if oneHanded {
108			return 2
109		}
110		return 1
111	case cards.CockAimShoot:
112		if oneHanded {
113			return 2
114		}
115		return 1
116	case cards.Shoot:
117		if oneHanded {
118			return 1
119		}
120		return 0
121	}
122	return 0
123}
124
125// validHex is a loose check of a hex id in a plan string ("A-F6", "A-I11*"
126// for a half-hex); the board decides whether it exists.
127func validHex(h string) bool {
128	if len(h) < 3 || len(h) > 8 {
129		return false
130	}
131	for i := 0; i < len(h); i++ {
132		c := h[i]
133		if !(c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '*') {
134			return false
135		}
136	}
137	return true
138}
139
140func validOpt(code cards.Code, opt string) bool {
141	switch code {
142	case cards.CockAimShoot:
143		return opt == OptCock || opt == OptUncock || opt == OptAim || opt == OptShoot
144	case cards.Shoot:
145		return opt == OptShoot || opt == OptNothing
146	case cards.Throw:
147		return opt == OptThrow || opt == OptNothing
148	case cards.Strength:
149		return opt == OptDamage || opt == OptWield
150	}
151	return false
152}
153
154// takesTarget reports whether the option names a target.
155func takesTarget(opt string) bool { return opt == OptAim || opt == OptShoot || opt == OptThrow }
156
157// Plan is the ordered stack of actions a player committed for the turn.
158//
159// Its string form, used as the transaction argument, is a comma-separated
160// list of entries "<card><f|b>[:<choice>]", the choice being a relative
161// direction for move and turn cards ("1f:ahead_left"), for Draw & Cock a
162// gun and the hand it goes to ("9f:1:0": the character's gun 1 to the gun
163// hand, "9f:g2:0": the gun lying in its hex with ground id 2; hand codes: 0
164// gun hand, 1 other hand, 2 both hands), or for Cock/Aim/Shoot and
165// Shoot the option ("5f:aim:A-F6", "7f:shoot:@1"; a bare "7f" does
166// nothing), for Throw the same ("6b:throw:@1", a bare "6b" does nothing),
167// for Strength its use ("7b:damage", "7b:wield"), e.g.
168// "1f:ahead_left,2f:ahead,3f"; gun moves ("h1:g", GunMove) come before the
169// action they go with and do not count as actions. An empty string is a
170// pass.
171type Plan []Action
172
173// ParsePlan parses the plan string. It checks the syntax and that every card
174// side exists; Validate checks the game rules.
175func ParsePlan(s string) (Plan, error) {
176	if s == "" {
177		return Plan{}, nil
178	}
179	if len(s) > MaxPlanLen {
180		return nil, errors.New("plan is too long")
181	}
182	entries := strings.Split(s, ",")
183	plan := make(Plan, 0, len(entries))
184	var moves []GunMove
185	for _, e := range entries {
186		if strings.HasPrefix(e, "h") {
187			m, err := parseMove(e)
188			if err != nil {
189				return nil, err
190			}
191			if len(moves) >= MaxGuns {
192				return nil, errors.New("too many gun moves before an action")
193			}
194			moves = append(moves, m)
195			continue
196		}
197		a, err := parseEntry(e)
198		if err != nil {
199			return nil, err
200		}
201		a.Moves, moves = moves, nil
202		plan = append(plan, a)
203		if len(plan) > MaxPlanEntries {
204			return nil, errors.New("too many actions in plan")
205		}
206	}
207	if len(moves) > 0 {
208		return nil, errors.New("a gun move goes before an action: it is done when that action comes up")
209	}
210	return plan, nil
211}
212
213// parseMove parses a gun move entry, "h<gun id>:<0|1|g>".
214func parseMove(e string) (GunMove, error) {
215	parts := strings.Split(e[1:], ":")
216	if len(parts) != 2 {
217		return GunMove{}, errors.New("gun move must be h<gun id>:<to> in " + strconv.Quote(e))
218	}
219	id, err := strconv.Atoi(parts[0])
220	if err != nil || id < 1 {
221		return GunMove{}, errors.New("invalid gun id in " + strconv.Quote(e))
222	}
223	switch parts[1] {
224	case "0":
225		return GunMove{Gun: id, To: HandGun}, nil
226	case "1":
227		return GunMove{Gun: id, To: HandOther}, nil
228	case "g":
229		return GunMove{Gun: id, To: HandGround}, nil
230	}
231	return GunMove{}, errors.New("a gun moves to 0 (gun hand), 1 (other hand) or g (the ground) in " + strconv.Quote(e))
232}
233
234func parseEntry(e string) (Action, error) {
235	if e == "" {
236		return Action{}, errors.New("empty action in plan")
237	}
238	choice := ""
239	if i := strings.IndexByte(e, ':'); i >= 0 {
240		e, choice = e[:i], e[i+1:]
241		if choice == "" {
242			return Action{}, errors.New("missing choice in " + strconv.Quote(e+":"))
243		}
244	}
245	if len(e) < 2 {
246		return Action{}, errors.New("invalid action " + strconv.Quote(e))
247	}
248	side := e[len(e)-1]
249	num, err := strconv.Atoi(e[:len(e)-1])
250	if err != nil || (side != cards.FrontSide && side != cards.BackSide) {
251		return Action{}, errors.New("invalid action " + strconv.Quote(e))
252	}
253	def, ok := cards.Lookup(num, side)
254	if !ok {
255		return Action{}, errors.New("invalid action card " + strconv.Quote(e))
256	}
257	a := Action{Card: num, Side: side, Code: def.Code, Cost: def.Cost, Hand: -1, Target: -1}
258	switch {
259	case def.Code == cards.Strength:
260		if choice == "" { // a STRENGTH whose use is not stated adds to the damage (20.2)
261			choice = OptDamage
262		}
263		if !validOpt(def.Code, choice) {
264			return Action{}, errors.New("invalid option " + strconv.Quote(choice) + " for " + def.Name + " (damage or wield)")
265		}
266		a.Opt = choice
267	case cards.TakesTarget(def.Code):
268		if choice == "" && (def.Code == cards.Shoot || def.Code == cards.Throw) {
269			choice = OptNothing
270		}
271		opt, arg := choice, ""
272		if i := strings.IndexByte(choice, ':'); i >= 0 {
273			opt, arg = choice[:i], choice[i+1:]
274		}
275		if !validOpt(def.Code, opt) {
276			return Action{}, errors.New("invalid option " + strconv.Quote(opt) + " for " + def.Name)
277		}
278		if takesTarget(opt) {
279			switch {
280			case strings.HasPrefix(arg, "@"):
281				seat, err := strconv.Atoi(arg[1:])
282				if err != nil || seat < 0 || seat >= MaxPlayers {
283					return Action{}, errors.New("invalid character " + strconv.Quote(arg) + " to " + opt + " at")
284				}
285				a.Target = seat
286			case validHex(arg):
287				a.Hex = arg
288			default:
289				return Action{}, errors.New(opt + " needs a hex or a character to " + opt + " at, e.g. " + strconv.Quote(e+":"+opt+":A-F6") + " or " + strconv.Quote(e+":"+opt+":@1"))
290			}
291		} else if arg != "" {
292			return Action{}, errors.New("option " + strconv.Quote(opt) + " takes no argument")
293		}
294		a.Opt = opt
295	case choice == "":
296	case def.Choice.NeedsGun():
297		parts := strings.Split(choice, ":")
298		if len(parts) != 2 {
299			return Action{}, errors.New("gun choice must be <gun id>:<hand> in " + strconv.Quote(e+":"+choice))
300		}
301		ref := strings.TrimPrefix(parts[0], "g")
302		id, err := strconv.Atoi(ref)
303		if err != nil || id < 1 {
304			return Action{}, errors.New("invalid gun id " + strconv.Quote(parts[0]))
305		}
306		hand, err := strconv.Atoi(parts[1])
307		if _, ok := HandLocation(hand); err != nil || !ok {
308			return Action{}, errors.New("invalid hand " + strconv.Quote(parts[1]) + " (0 gun hand, 1 other hand, 2 both hands)")
309		}
310		if ref != parts[0] {
311			a.Ground = id
312		} else {
313			a.Gun = id
314		}
315		a.Hand = hand
316	default:
317		r, ok := hex.ParseRel(choice)
318		if !ok {
319			return Action{}, errors.New("invalid direction " + strconv.Quote(choice))
320		}
321		a.Dir = r
322	}
323	return a, nil
324}
325
326// String is the canonical plan string, parseable by ParsePlan.
327func (p Plan) String() string {
328	var b strings.Builder
329	for i, a := range p {
330		if i > 0 {
331			b.WriteByte(',')
332		}
333		for _, m := range a.Moves {
334			b.WriteString(m.String())
335			b.WriteByte(',')
336		}
337		b.WriteString(strconv.Itoa(a.Card))
338		b.WriteByte(a.Side)
339		if a.Gun > 0 || a.Ground > 0 {
340			b.WriteByte(':')
341			if a.Ground > 0 {
342				b.WriteByte('g')
343				b.WriteString(strconv.Itoa(a.Ground))
344			} else {
345				b.WriteString(strconv.Itoa(a.Gun))
346			}
347			b.WriteByte(':')
348			b.WriteString(strconv.Itoa(a.Hand))
349		} else if a.Opt != "" && !((a.Code == cards.Shoot || a.Code == cards.Throw) && a.Opt == OptNothing) {
350			b.WriteByte(':')
351			b.WriteString(a.Opt)
352			if takesTarget(a.Opt) {
353				b.WriteByte(':')
354				if a.Target >= 0 {
355					b.WriteByte('@')
356					b.WriteString(strconv.Itoa(a.Target))
357				} else {
358					b.WriteString(a.Hex)
359				}
360			}
361		} else if a.Dir != "" {
362			b.WriteByte(':')
363			b.WriteString(string(a.Dir))
364		}
365	}
366	return b.String()
367}
368
369// Cost is the total time points of the plan.
370func (p Plan) Cost() int {
371	n := 0
372	for _, a := range p {
373		n += a.Cost
374	}
375	return n
376}
377
378// Has reports whether the plan holds the action.
379func (p Plan) Has(code cards.Code) bool {
380	for _, a := range p {
381		if a.Code == code {
382			return true
383		}
384	}
385	return false
386}
387
388// PlayedFoot reports whether the plan holds any foot action ([H12]).
389func (p Plan) PlayedFoot() bool {
390	for _, a := range p {
391		if def, ok := cards.Lookup(a.Card, a.Side); ok && def.Kind == cards.KindFoot {
392			return true
393		}
394	}
395	return false
396}
397
398// Validate checks the plan against the selection rules for the character
399// that will play it (port of the BGA actConfirmPlan checks).
400func (p Plan) Validate(c Character) error {
401	used := map[int]bool{}
402	for i, a := range p {
403		def, ok := cards.Lookup(a.Card, a.Side)
404		if !ok {
405			return errors.New("invalid action card")
406		}
407		if !cards.Enabled(def.Code) {
408			return errors.New("that action is not available yet")
409		}
410		if used[a.Card] { // rule 7.2: one side per card, once per turn
411			return errors.New("you cannot use both sides of a card")
412		}
413		used[a.Card] = true
414		// The gun may be one the plan picks up first (its id is the sheet's
415		// next): whether it is there is checked when the move is done.
416		for _, m := range a.Moves {
417			if m.To == HandOther { // the other hand comes with a second gun, as for Draw & Cock
418				return errors.New("a gun moves to the gun hand or the ground for now")
419			}
420			if m.To != HandGun && m.To != HandGround {
421				return errors.New("a gun moves to the gun hand or the ground")
422			}
423		}
424		switch {
425		case def.Code == cards.Strength:
426			if !validOpt(def.Code, a.Opt) {
427				return errors.New("choose what the strength adds to")
428			}
429			// Played just before (on top of) the action it combines with
430			// (20.1): a Throw, until the attacks come.
431			if i+1 >= len(p) || p[i+1].Code != cards.Throw {
432				return errors.New("strength is played just before a throw")
433			}
434		case cards.TakesTarget(def.Code):
435			if !validOpt(def.Code, a.Opt) {
436				if def.Code == cards.Throw {
437					return errors.New("choose whether to throw")
438				}
439				return errors.New("choose what to do with the gun")
440			}
441			if takesTarget(a.Opt) && a.Hex == "" && a.Target < 0 {
442				return errors.New("choose what to " + a.Opt + " at")
443			}
444		case def.Choice.IsAheadRel():
445			if !a.Dir.IsAhead() {
446				return errors.New("choose a forward direction")
447			}
448		case def.Choice.IsBackRel():
449			if !a.Dir.IsBack() {
450				return errors.New("choose a backward direction")
451			}
452		case def.Choice.NeedsGun():
453			if a.Gun == 0 && a.Ground == 0 {
454				return errors.New("choose the gun to draw")
455			}
456			if a.Hand == HandOther { // the other hand comes with a second gun
457				return errors.New("only the gun hand or both hands can take a gun for now")
458			}
459			dest, _ := HandLocation(a.Hand)
460			slot := -1
461			if a.Gun != 0 { // a gun on the ground is checked against the hex when the action executes
462				if slot = c.Guns.Find(a.Gun); slot < 0 {
463					return errors.New("you have no such gun")
464				}
465				if c.Guns[slot].Location == dest {
466					return errors.New("that gun is already there")
467				}
468			}
469			if c.Guns.Blocked(dest, slot) {
470				return errors.New("that hand already holds a gun")
471			}
472		default:
473			if a.Dir != "" {
474				return errors.New("that action takes no direction")
475			}
476			if a.Gun != 0 || a.Ground != 0 {
477				return errors.New("that action takes no gun")
478			}
479		}
480	}
481	budget := MaxActionPoints - c.Delay // [H5]
482	if p.Cost() > budget {
483		return errors.New("those actions cost more time points than you have")
484	}
485	// Prerequisite chains (rule 9.21).
486	if p.Has(cards.Run) && !p.Has(cards.Advance) {
487		return errors.New("run can only be played together with advance")
488	}
489	if p.Has(cards.Sprint) {
490		if !p.Has(cards.Run) {
491			return errors.New("sprint can only be played together with run")
492		}
493		if !c.RanLastTurn { // rule 9.23: speed is built up over two turns
494			return errors.New("sprint can only be played after a run on the previous turn")
495		}
496	}
497	return nil
498}
499
500// Combine folds each run of STRENGTHs into the Throw it is played on top
501// of (20.1): treated as part of that action, revealed with it, their time
502// points added to its own, their bonuses in its Wield and Damage, their
503// gun moves done as it is revealed. Validate has checked that each
504// STRENGTH comes just before a Throw; one that does not is dropped.
505func (p Plan) Combine() Plan {
506	out := make(Plan, 0, len(p))
507	var (
508		cost, wield, damage int
509		moves               []GunMove
510	)
511	for _, a := range p {
512		if a.Code == cards.Strength {
513			cost += a.Cost
514			if a.Opt == OptWield {
515				wield += StrengthWield
516			} else {
517				damage += StrengthDamage
518			}
519			moves = append(moves, a.Moves...)
520			continue
521		}
522		if a.Code == cards.Throw {
523			a.Cost += cost
524			a.Wield += wield
525			a.Damage += damage
526			if len(moves) > 0 {
527				a.Moves = append(moves, a.Moves...)
528			}
529		}
530		cost, wield, damage, moves = 0, 0, 0, nil
531		out = append(out, a)
532	}
533	return out
534}