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}