aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-08-19 15:37:09 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:57:40 +0200
commit3d36f5c92ec204f72d55df5b41e9bacdf5e87ba8 (patch)
tree25f8ce5b91b34e6f56380f1c1a32fcd08ff8d419
parent790706dbf1e797607dfb6c6717ab863a9e4cb5ca (diff)
downloadmuhqs-game-3d36f5c92ec204f72d55df5b41e9bacdf5e87ba8.tar.gz
muhqs-game-3d36f5c92ec204f72d55df5b41e9bacdf5e87ba8.zip
fix spawn tile constraint for water spawns
This is not a general solution because spawns could be a valid tile type constraint as well as meaning available spawn tiles. The separation and a clean implementation is still a TODO.
-rw-r--r--README.md1
-rw-r--r--go/game/targets.go16
-rw-r--r--go/game/targets_test.go67
3 files changed, 81 insertions, 3 deletions
diff --git a/README.md b/README.md
index b74c6334..30337abb 100644
--- a/README.md
+++ b/README.md
@@ -125,3 +125,4 @@ Things that should be done eventually:
- [ ] finish error handling for remote PlayerControls
- [ ] support big card grids (for sealed)
- [ ] implement replacement effects
+- [ ] distinguish between a spawn tile type constraint and available spawn tiles
diff --git a/go/game/targets.go b/go/game/targets.go
index e056a5c0..05d690c0 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -3,11 +3,11 @@ package game
import (
"errors"
"fmt"
- "log"
"strconv"
"strings"
"golang.org/x/exp/slices"
+ "muhq.space/muhqs-game/go/log"
"muhq.space/muhqs-game/go/utils"
)
@@ -215,6 +215,8 @@ func (t *Target) Options() (options []any) {
for _, candidate := range candidates {
if err := t.constraint(candidate); err == nil {
options = append(options, candidate)
+ } else {
+ log.Debug("no valid option", "candidate", candidate, "error", err)
}
}
return options
@@ -294,6 +296,7 @@ func (t *Targets) Next() { t.idx++ }
func (t *Targets) Targets() []*Target { return t.ts }
func (t *Targets) CheckTargets(s *LocalState) error {
+ log.Debug("Check all sub targets", "targets", t.ts)
for _, target := range t.ts {
if err := target.CheckSelection(s); err != nil {
return err
@@ -627,6 +630,7 @@ func availableTileConstraint(action Action, card *Card) TargetConstraintFunc {
if tile.IsAvailableForCard(card) {
return nil
}
+ log.Debug("tile not available", "tile", tile, "card", card)
return fmt.Errorf("tile %v is not available for %s", tile, card.Name)
}
}
@@ -682,7 +686,10 @@ func parseTileTargetConstraint(desc string, s *LocalState, action Action) []Targ
constraints = append(constraints, func(t any) (err error) {
tile := relaxedTileTarget(action, t)
- if slices.Contains(s.AvailableSpawnTiles(player, card), tile) {
+ availableSpawnTiles := s.AvailableSpawnTiles(player, card)
+ log.Debug("check possible spawn tile", "tile", tile, "spawns", availableSpawnTiles)
+ if slices.Contains(availableSpawnTiles, tile) {
+ log.Debug("is spawn tile", "tile", tile)
return nil
}
return fmt.Errorf("tile %v is no spawn tile", tile)
@@ -725,6 +732,11 @@ func parseTileTargetConstraint(desc string, s *LocalState, action Action) []Targ
for name := range TileNames {
if strings.Contains(desc, name) {
+ // FIXME: skip spawn tile type constraint since it colides with available water spawns
+ if name == "spawn" {
+ continue
+ }
+
tileType := TileNames[name]
if strings.Contains(desc, "connected") {
// Find connected tiles
diff --git a/go/game/targets_test.go b/go/game/targets_test.go
index ae451dbf..eed94f4f 100644
--- a/go/game/targets_test.go
+++ b/go/game/targets_test.go
@@ -9,7 +9,7 @@ func TestTileTargets(t *testing.T) {
mapDef := `map: |1-
HSTS
HSFS
- TSWS
+ TSWD
symbols:
T: tower
@@ -17,6 +17,7 @@ symbols:
F: farm
S: street
W: deep water
+ D: docks
`
s := NewLocalState()
r := strings.NewReader(mapDef)
@@ -72,6 +73,70 @@ symbols:
if len(opts) != 6 {
t.Fatal("expected 6 available candidates not:", len(opts))
}
+
+ fisher := NewCard("nautics/fisher")
+ p.Hand.AddCard(fisher)
+ pa := NewPlayAction(p, fisher)
+ opts = pa.Target().Options()
+ if len(opts) != 1 {
+ t.Fatal("expected 1 available candidates not:", len(opts))
+ }
+}
+
+func TestSpawnTargets(t *testing.T) {
+ s := NewLocalState()
+ m, _ := GetMap("the-kraken")
+ s.SetMap(m)
+
+ s.activePhase = actionPhase
+
+ p := s.AddNewPlayer("player", NewDeck())
+ ctrl := newMockPlayerControl(p)
+ ctrl.actionToSend = NewPassPriority(p)
+ p.Ctrl = ctrl
+
+ p.gainResource(2)
+
+ farmer := NewCard("misc/farmer")
+ p.Hand.AddCard(farmer)
+ fisher := NewCard("nautics/fisher")
+ p.Hand.AddCard(fisher)
+
+ pa := NewPlayAction(p, farmer)
+ if !pa.Target().RequireSelection() {
+ t.Fatal("playing farmer has not require selection")
+ }
+ if !pa.Target().AllowSelection() {
+ t.Fatal("playing farmer has no valid targets")
+ }
+ farmerPos := Position{3, 13}
+ err := pa.Target().AddSelection(m.TileAt(farmerPos))
+ if err != nil {
+ t.Fatal("Not allowed to play farmer at:", farmerPos)
+ }
+ s.declareAction(pa)
+ if s.stack.IsEmpty() {
+ t.Fatal("farmer play action not declared")
+ }
+ s.stack.resolve()
+
+ pa2 := NewPlayAction(p, fisher)
+ if !pa2.Target().RequireSelection() {
+ t.Fatal("playing fisher has not require selection")
+ }
+ if !pa2.Target().AllowSelection() {
+ t.Fatal("playing fisher has no valid targets")
+ }
+ fisherPos := Position{3, 10}
+ pa2.Target().AddSelection(m.TileAt(fisherPos))
+ if err != nil {
+ t.Fatal("Not allowed to play fisher at:", fisherPos)
+ }
+ s.declareAction(pa2)
+ if s.stack.IsEmpty() {
+ t.Fatal("fisher play action not declared")
+ }
+ s.stack.resolve()
}
func TestFlexAttackTargets(t *testing.T) {