diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-07-24 05:25:12 -0400 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-07-24 12:22:10 -0400 |
| commit | e78e8fb6ae67e9cd188515c1c806dfa2751327c4 (patch) | |
| tree | 373b070a267caafce3016f97ab0dd07f7e4cdb74 | |
| parent | ed473052c558fbda6bb03d9083b425cb2e7a808f (diff) | |
| download | muhqs-game-e78e8fb6ae67e9cd188515c1c806dfa2751327c4.tar.gz muhqs-game-e78e8fb6ae67e9cd188515c1c806dfa2751327c4.zip | |
support street actions
| -rw-r--r-- | go/game/action.go | 42 | ||||
| -rw-r--r-- | go/game/action_test.go | 27 | ||||
| -rw-r--r-- | go/game/ai.go | 105 | ||||
| -rw-r--r-- | go/game/ai_test.go | 2 | ||||
| -rw-r--r-- | go/game/map_test.go | 44 | ||||
| -rw-r--r-- | go/game/targets.go | 62 | ||||
| -rw-r--r-- | go/game/unit.go | 16 |
7 files changed, 253 insertions, 45 deletions
diff --git a/go/game/action.go b/go/game/action.go index 5f478de7..077f4026 100644 --- a/go/game/action.go +++ b/go/game/action.go @@ -379,6 +379,48 @@ func (a *MoveAction) String() string { return fmt.Sprintf("%v -> %v", u, t.Position) } +type StreetAction struct { + ActionBase +} + +var streetActionTargetDesc = TargetDesc{"available connected street tile", "1"} + +func NewStreetAction(u *Unit) *StreetAction { + a := &StreetAction{ + ActionBase{ + source: u, + Card: u.Card(), + }, + } + + a.targets = newTargets(newTarget(u.Controller().gameState, streetActionTargetDesc, a)) + + a.resolveFunc = func(s *LocalState) { + t := targetTile(a.Target()) + movePermanent(u, t) + } + + a.costFunc = func(*LocalState) bool { + ok := u.AvailStreetActions > 0 && u.Tile() != nil && u.Tile().Type == TileTypes.Street + if ok { + u.AvailStreetActions = u.AvailStreetActions - 1 + } + return ok + } + + return a +} + +func (a *StreetAction) String() string { + u := a.source.(*Unit) + if a.targets.RequireSelection() { + return fmt.Sprintf("street %s", u.Movement.String()) + } + + t := targetTile(a.Target()) + return fmt.Sprintf("%v s-s> %v", u, t.Position) +} + func genBaseActionCost(u *Unit, attack, move int) ActionCostFunc { return func(*LocalState) bool { if u.AvailAttackActions < attack || u.AvailMoveActions < move { diff --git a/go/game/action_test.go b/go/game/action_test.go index b7a92bae..452a0544 100644 --- a/go/game/action_test.go +++ b/go/game/action_test.go @@ -180,3 +180,30 @@ symbols: t.Fatal("archer took damage in range combat from melee unit") } } + +func TestStreetAction(t *testing.T) { + mapDef := `map: |1- + HSH + HSH + HSS + HSH + +symbols: + H: house + S: street +` + s := NewLocalState() + r := strings.NewReader(mapDef) + m, _ := readMap(r) + s.SetMap(m) + + p := s.AddNewPlayer("player", NewDeck()) + + c := s.addNewUnit(NewCard("base/cavalry"), Position{1, 0}, p) + + a := NewStreetAction(c) + opts := a.Target().Options() + if len(opts) != 4 { + t.Fatal("Unexpected amount of street targets", len(opts)) + } +} diff --git a/go/game/ai.go b/go/game/ai.go index 7b20d187..07440899 100644 --- a/go/game/ai.go +++ b/go/game/ai.go @@ -122,11 +122,16 @@ func selectRandomTargets(rand *rand.Rand, targets *Targets) error { var ErrNoPath = errors.New("no path found") -func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph { +func generateGraph(tiles []*Tile) *dijkstra.Graph { graph := dijkstra.NewGraph() - for _, t := range m.AllTiles() { + for _, t := range tiles { graph.AddMappedVertex(t.Position.String()) } + return graph +} + +func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph { + graph := generateGraph(m.AllTiles()) for _, t := range m.AllTiles() { tId := t.Position.String() @@ -146,35 +151,65 @@ func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph { return graph } -// func (m *Map) generateMovementRangeGraphFor(u *Unit) *dijkstra.Graph { -// origin := TileOrContainingPermTile(u).Position -// graph := dijkstra.NewGraph() -// for _, t := range tilesInRangeFromOrigin(m, origin, u.Movement.Range, true) { -// graph.AddMappedVertex(t.Position.String()) -// } - -// for _, t := range TilesInRange(m, u, u.Movement.Range) { -// tId := t.Position.String() -// for _, neighbour := range tilesInRangeFromOrigin(m, t.Position, 1, true) { -// if !IsPositionInRange(neighbour.Position, origin, u.Movement.Range) { -// continue -// } - -// if u.IsAvailableTile(neighbour) { -// cost := 2 -// if t.OnDiagonal(neighbour) { -// cost = 3 -// } -// log.Printf("add %v -> %v: %d", tId, neighbour.Position.String(), int64(cost)) -// err := graph.AddMappedArc(tId, neighbour.Position.String(), int64(cost)) -// if err != nil { -// log.Panicf("Failed to add mapped arc: %s", err) -// } -// } -// } -// } -// return graph -// } +func (m *Map) generateMovementRangeGraphFor(u *Unit) *dijkstra.Graph { + origin := TileOrContainingPermTile(u).Position + graph := generateGraph(tilesInRangeFromOrigin(m, origin, u.Movement.Range, true)) + + // Add connections for all tiles and their neighbours + for _, t := range tilesInRangeFromOrigin(m, origin, u.Movement.Range, true) { + tId := t.Position.String() + for _, neighbour := range TilesInRangeFromOrigin(m, t.Position, 1) { + // Skip neighbours not in movement range + if !IsPositionInRange(neighbour.Position, origin, u.Movement.Range) { + continue + } + + if u.IsAvailableTile(neighbour) { + cost := 2 + if t.OnDiagonal(neighbour) { + cost = 3 + } + err := graph.AddMappedArc(tId, neighbour.Position.String(), int64(cost)) + if err != nil { + log.Panicf("Failed to add mapped arc: %s", err) + } + } + } + } + return graph +} + +func (m *Map) generateConnectedMovementRangeGraphFor(u *Unit, conn TileType) *dijkstra.Graph { + origin := TileOrContainingPermTile(u).Position + graph := generateGraph(tilesInRangeFromOrigin(m, origin, u.Movement.Range, true)) + + // Add connections for all tiles and their neighbours + for _, t := range tilesInRangeFromOrigin(m, origin, u.Movement.Range, true) { + tId := t.Position.String() + // Skip tiles with different types + if t.Type != conn { + continue + } + for _, neighbour := range TilesInRangeFromOrigin(m, t.Position, 1) { + // Skip neighbours not in movement range or with different type + if !IsPositionInRange(neighbour.Position, origin, u.Movement.Range) || neighbour.Type != conn { + continue + } + + if u.IsAvailableTile(neighbour) { + cost := 2 + if t.OnDiagonal(neighbour) { + cost = 3 + } + err := graph.AddMappedArc(tId, neighbour.Position.String(), int64(cost)) + if err != nil { + log.Panicf("Failed to add mapped arc: %s", err) + } + } + } + } + return graph +} func findPathTo(graph *dijkstra.Graph, u *Unit, pos Position) (dijkstra.BestPath, error) { srcId, err := graph.GetMapping(TileOrContainingPermTile(u).Position.String()) @@ -276,14 +311,17 @@ func moveAwayFromEnemies(ai *UnitAI) Action { return nil } + // A Movement graph is not sufficient because we have to check the distance to units potentially not in range. graph := ai.s.Map().generateMapGraphFor(ai.u) var farest *Tile var farestEnemyDistance int64 - for _, availableTile := range ai.u.MoveRangeTiles(ai.s.Map()) { + // Check all reachable tiles + for _, availableTile := range ai.u.MoveRangeTiles() { var nearestEnemy int64 = -1 for _, enemyUnit := range ai.s.EnemyUnits(ai.u.Controller()) { + // Get distance to the enemy unit p, err := findPathTo(graph, enemyUnit, availableTile.Position) if err != nil { continue @@ -299,6 +337,7 @@ func moveAwayFromEnemies(ai *UnitAI) Action { } } + // No move possible if farest == nil { return nil } @@ -331,7 +370,7 @@ func actionFromPaths(ai *UnitAI, graph *dijkstra.Graph, paths []*dijkstra.BestPa var x, y int _, _ = fmt.Sscanf(mapping, "(%d, %d)", &x, &y) nextStep := m.TileAt(Position{X: x, Y: y}) - if slices.Contains(ai.u.MoveRangeTiles(m), nextStep) { + if slices.Contains(ai.u.MoveRangeTiles(), nextStep) { target = nextStep } else { break diff --git a/go/game/ai_test.go b/go/game/ai_test.go index 854f38dd..60ab1082 100644 --- a/go/game/ai_test.go +++ b/go/game/ai_test.go @@ -92,7 +92,7 @@ symbols: ai := NewUnitAI(s, c) if ai == nil { - t.Fatal("No AI available for Bauernfeind") + t.Fatal("No AI available for Clownfish") } ai.promptAction() diff --git a/go/game/map_test.go b/go/game/map_test.go index 15fa05cb..bc17dd55 100644 --- a/go/game/map_test.go +++ b/go/game/map_test.go @@ -4,6 +4,8 @@ import ( "reflect" "strings" "testing" + + "golang.org/x/exp/slices" ) func parseMapString(t *testing.T, mapDef string) *Map { @@ -113,3 +115,45 @@ symbols: t.Fatal("Tile at (0,0) is not a house") } } + +func TestMovementRangeTiles(t *testing.T) { + mapDef := `map: |1- + T H HT + FSSSSS2H + SHSH HS + S SST S + H HFH H + S TSS S + SH HSHS + H1SSSSSF + TH H T + +symbols: + T: tower + H: house + F: farm + S: street + 1: spawn player 1 + 2: spawn player 2 +` + + s := NewLocalState() + m := parseMapString(t, mapDef) + s.SetMap(m) + p := s.AddNewPlayer("p", NewDeck()) + o := Position{2, 2} + a := s.addNewUnit(NewCard("base/archer"), o, p) + + tiles := a.MoveRangeTiles() + exp := TilesInRange(m, a, a.Movement.Range) + + if len(tiles) != len(exp) { + t.Fatal("expected", len(exp), "tiles not", len(tiles)) + } + + for _, tile := range exp { + if !slices.Contains(tiles, tile) { + t.Fatal("missing", tile, "in tiles") + } + } +} diff --git a/go/game/targets.go b/go/game/targets.go index 236b4850..b4456f55 100644 --- a/go/game/targets.go +++ b/go/game/targets.go @@ -623,17 +623,22 @@ func availableTileConstraint(action Action, card *Card) TargetConstraintFunc { } } -func moveConstraint(s *LocalState, action Action, u *Unit) TargetConstraintFunc { - return func(t interface{}) (err error) { +func moveConstraint(_ *LocalState, action Action, u *Unit) TargetConstraintFunc { + return func(t any) (err error) { // Some actions allow also unit targets as though they were tiles tile := relaxedTileTarget(action, t) - if slices.Contains(u.MoveRangeTiles(s.Map()), tile) { + if slices.Contains(u.MoveRangeTiles(), tile) { return nil } return fmt.Errorf("tile %v is not in %v's movement range", tile, u) } } +var ( + ErrTargetWrongTileType = errors.New("wrong tile type") + ErrTargetNotConnectedTile = errors.New("not connected") +) + func parseTileTargetConstraint(desc string, s *LocalState, action Action) []TargetConstraintFunc { constraints := []TargetConstraintFunc{} @@ -656,6 +661,8 @@ func parseTileTargetConstraint(desc string, s *LocalState, action Action) []Targ card = a.Card case *MoveAction: card = a.Card + case *StreetAction: + card = a.Card } if strings.Contains(desc, "spawn") { @@ -663,7 +670,7 @@ func parseTileTargetConstraint(desc string, s *LocalState, action Action) []Targ log.Panicf("Not implemented spawn tile constraint for %T", action) } - constraints = append(constraints, func(t interface{}) (err error) { + constraints = append(constraints, func(t any) (err error) { tile := relaxedTileTarget(action, t) if slices.Contains(s.AvailableSpawnTiles(player, card), tile) { return nil @@ -677,7 +684,7 @@ func parseTileTargetConstraint(desc string, s *LocalState, action Action) []Targ } if strings.Contains(desc, "water") { - constraints = append(constraints, func(t interface{}) (err error) { + constraints = append(constraints, func(t any) (err error) { tile := t.(*Tile) if tile.Water { return nil @@ -691,7 +698,7 @@ func parseTileTargetConstraint(desc string, s *LocalState, action Action) []Targ } if strings.Contains(desc, "free") { - constraints = append(constraints, func(t interface{}) (err error) { + constraints = append(constraints, func(t any) (err error) { tile := t.(*Tile) if tile.IsFree() { return nil @@ -700,6 +707,49 @@ func parseTileTargetConstraint(desc string, s *LocalState, action Action) []Targ }) } + for name := range TileNames { + if strings.Contains(desc, name) { + tileType := TileNames[name] + if strings.Contains(desc, "connected") { + // Find connected tiles + u := action.Source().(*Unit) + graph := s.Map().generateConnectedMovementRangeGraphFor(u, tileType) + tiles := []*Tile{} + for _, pos := range PositionsInRange(u.Tile().Position, u.Movement.Range, false) { + tile := s.Map().TileAt(pos) + if tile == nil || tile.Type != tileType { + continue + } + + _, err := findPathTo(graph, u, pos) + if err != nil { + continue + } + + tiles = append(tiles, tile) + } + + // Check if tile is connected + constraints = append(constraints, func(t any) (err error) { + tile := t.(*Tile) + if !slices.Contains(tiles, tile) { + err = ErrTargetNotConnectedTile + } + return + }) + } + + // Tile type constraint + constraints = append(constraints, func(t any) error { + tile := t.(*Tile) + if tile.Type == tileType { + return nil + } + return ErrTargetWrongTileType + }) + } + } + return constraints } diff --git a/go/game/unit.go b/go/game/unit.go index 1bf3afa1..d1192dc0 100644 --- a/go/game/unit.go +++ b/go/game/unit.go @@ -5,6 +5,7 @@ import ( ) const ( + DEFAULT_AVAIL_STREET_ACTIONS = 1 DEFAULT_AVAIL_MOVE_ACTIONS = 1 DEFAULT_AVAIL_ATTACK_ACTIONS = 1 ) @@ -18,6 +19,7 @@ type Unit struct { FullActions []*FullAction FreeActions []*FreeAction // Effects []Effects + AvailStreetActions int AvailMoveActions int additionalMoveActions int AvailAttackActions int @@ -77,6 +79,7 @@ func (u *Unit) UpkeepCost() int { } func (u *Unit) resetBaseActions() { + u.AvailStreetActions = DEFAULT_AVAIL_STREET_ACTIONS + u.additionalMoveActions u.AvailMoveActions = DEFAULT_AVAIL_MOVE_ACTIONS + u.additionalMoveActions u.AvailAttackActions = DEFAULT_AVAIL_ATTACK_ACTIONS + u.additionalAttackActions } @@ -86,11 +89,10 @@ func (u *Unit) onUpkeep() { u.resetBaseActions() } -func (u *Unit) MoveRangeTiles(m *Map) []*Tile { +func (u *Unit) MoveRangeTiles() []*Tile { tiles := []*Tile{} - // TODO: build graph only containing possible tile in movement range - graph := m.generateMapGraphFor(u) - // graph := m.generateMovementRangeGraphFor(u) + m := u.controller.gameState.Map() + graph := m.generateMovementRangeGraphFor(u) origin := TileOrContainingPermTile(u).Position for _, pos := range PositionsInRange(origin, u.Movement.Range, false) { tile := m.TileAt(pos) @@ -124,7 +126,7 @@ func (u *Unit) AttackableTiles() []*Tile { return tilesInRange } - // House tiles are not attackable from $anges > 1 + // House tiles are not attackable from ranges > 1 aTiles := []*Tile{} for _, t := range tilesInRange { if DistanceBetweenPositions(t.Position, u.tile.Position) > 1 && t.Type == TileTypes.House { @@ -181,6 +183,10 @@ func (u *Unit) AvailSlowActions() (actions []Action) { actions = append(actions, NewMoveAction(u)) } + if u.AvailStreetActions > 0 && u.Movement != INVALID_MOVEMENT() && u.Tile().Type == TileTypes.Street { + actions = append(actions, NewStreetAction(u)) + } + m := u.Controller().gameState.Map() if u.AvailAttackActions > 0 && !INVALID_ATTACK(u.Attack) && len(u.AttackableEnemyPermanents()) > 0 { |
