diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-25 21:10:44 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-01-26 22:17:17 +0100 |
| commit | d5e078ec59002f519a040fb88c36ea76ce6ce207 (patch) | |
| tree | b186f3b78ad8d36c4358e4cc03c72a423cf39227 | |
| parent | 6ec593fb28a0ab789795f1718fba6705ea02ee49 (diff) | |
| download | muhqs-game-d5e078ec59002f519a040fb88c36ea76ce6ce207.tar.gz muhqs-game-d5e078ec59002f519a040fb88c36ea76ce6ce207.zip | |
intermediate commit
| -rw-r--r-- | go/client/main.go | 30 | ||||
| -rw-r--r-- | go/game/action.go | 69 | ||||
| -rw-r--r-- | go/game/ai.go | 291 | ||||
| -rw-r--r-- | go/game/ai_test.go | 66 | ||||
| -rw-r--r-- | go/game/attack.go | 50 | ||||
| -rw-r--r-- | go/game/card.go | 74 | ||||
| -rw-r--r-- | go/game/deck.go | 14 | ||||
| -rw-r--r-- | go/game/kraken.go | 150 | ||||
| -rw-r--r-- | go/game/map.go | 27 | ||||
| -rw-r--r-- | go/game/map_test.go | 35 | ||||
| -rw-r--r-- | go/game/movement.go | 47 | ||||
| -rw-r--r-- | go/game/permanent.go | 9 | ||||
| -rw-r--r-- | go/game/phase.go | 28 | ||||
| -rw-r--r-- | go/game/player.go | 48 | ||||
| -rw-r--r-- | go/game/playerControl.go | 8 | ||||
| -rw-r--r-- | go/game/range.go | 12 | ||||
| -rw-r--r-- | go/game/range_test.go | 8 | ||||
| -rw-r--r-- | go/game/state.go | 101 | ||||
| -rw-r--r-- | go/game/tile.go | 24 | ||||
| -rw-r--r-- | go/game/unit.go | 84 | ||||
| -rw-r--r-- | go/go.mod | 1 | ||||
| -rw-r--r-- | go/go.sum | 8 | ||||
| -rw-r--r-- | go/ui/hand.go | 24 | ||||
| -rw-r--r-- | go/ui/stateBar.go | 23 | ||||
| -rw-r--r-- | go/ui/textBox.go | 4 | ||||
| -rw-r--r-- | go/webtools/main.go | 6 |
26 files changed, 985 insertions, 256 deletions
diff --git a/go/client/main.go b/go/client/main.go index f2b7e36f..a3dbeeec 100644 --- a/go/client/main.go +++ b/go/client/main.go @@ -15,7 +15,7 @@ import ( ) const ( - screenWidth = 1280 + screenWidth = 800 screenHeight = 1024 DEFAULT_BUTTON_HEIGHT = 40 @@ -26,9 +26,7 @@ const ( HOVER_THRESHOLD = 60 HAND_VIEW_HEIGHT = 500 - HAND_VIEW_WIDTH = 700 - - STATE_BAR_WIDTH = 50 + HAND_VIEW_WIDTH = 500 ) var playerColors = []color.RGBA{ @@ -142,12 +140,12 @@ func (g *Game) initPlayerUi(player *game.Player) { g.playerCtrl = game.NewChanPlayerControl() player.Ctrl = g.playerCtrl - g.handLayer = ui.NewHandView(STATE_BAR_WIDTH, g.mapView.GetHeight(), - HAND_VIEW_WIDTH, screenHeight-g.mapView.GetHeight(), player.Hand) + g.handLayer = ui.NewHandView(0, g.mapView.GetHeight(), + HAND_VIEW_WIDTH, screenHeight-g.mapView.GetHeight()-DEFAULT_BUTTON_HEIGHT, player.Hand) g.widgets = append(g.widgets, g.handLayer) - g.stateBar = ui.NewStateBar(0, screenHeight-HAND_VIEW_HEIGHT, - STATE_BAR_WIDTH, HAND_VIEW_HEIGHT, g.gameState, player) + g.stateBar = ui.NewStateBar(0, screenHeight-DEFAULT_BUTTON_HEIGHT, + screenWidth-g.passButton.Width, DEFAULT_BUTTON_HEIGHT, g.gameState, player) g.widgets = append(g.widgets, g.stateBar) } @@ -168,7 +166,7 @@ func (g *Game) removeWidget(widget ui.Widget) { } } -func (g *Game) addActionChoice(perm game.Permanent) { +func (g *Game) addActionChoice(perm game.Permanent, x, y int) { actions := perm.GetAvailActionPrototypes() if len(actions) > 0 { g.selectedObject = perm @@ -176,7 +174,7 @@ func (g *Game) addActionChoice(perm game.Permanent) { g.removeActionChoice() g.selectedObject = actions[c.GetChoosen(x, y)] } - g.actionChoice = ui.NewActionChoice(500, 200, actions, onClick) + g.actionChoice = ui.NewActionChoice(x, y, actions, onClick) g.addWidget(g.actionChoice) } } @@ -207,7 +205,6 @@ func (g *Game) passPriority() { func (g *Game) handlePlayerNotifications() { n := g.playerCtrl.RecvNotification() for n != nil { - log.Printf("Recv player notification %v\n", n) switch n.Notification { case game.PriorityNotification: g.addWidget(g.passButton) @@ -281,7 +278,7 @@ func (g *Game) Update() error { case game.Permanent: perm := obj if g.selectedObject == nil { - g.addActionChoice(perm) + g.addActionChoice(perm, x, y) } else { switch obj := g.selectedObject.(type) { case *game.AttackActionPrototype: @@ -307,7 +304,7 @@ func (g *Game) Update() error { g.mapView.HighlightPermanents(perms) case *game.Card: if obj.IsPermanent() { - spawnTiles := g.gameState.SpawnTiles(g.activePlayer) + spawnTiles := g.gameState.AvailableSpawnTiles(g.activePlayer, obj) g.mapView.HighlightTiles(spawnTiles) } } @@ -366,10 +363,9 @@ func main() { game.NewStore()) g.initPlayerUi(muhq) - enemy := g.addPlayer("enemy", - game.NewDeckFromDeckList(g.gameState.Map.StartDeckList), - game.NewStore()) - enemy.Ctrl = &game.DummyPlayerControl{enemy} + krakenId := len(g.gameState.Players) + kraken := game.NewKraken(krakenId, g.gameState, playerColors[krakenId]) + g.gameState.Players = append(g.gameState.Players, kraken) go func() { w := g.gameState.Loop() diff --git a/go/game/action.go b/go/game/action.go index a705a0fe..c121d0c8 100644 --- a/go/game/action.go +++ b/go/game/action.go @@ -8,6 +8,7 @@ import ( type ActionPrototype interface { Specialize(...interface{}) Action String() string + CanPayCosts(s *State) bool } type Action interface { @@ -18,11 +19,11 @@ type Action interface { type PassPriorityAction struct{ Source *Player } -func (*PassPriorityAction) Declare(*State) {} -func (*PassPriorityAction) Resolve(*State) {} -func (*PassPriorityAction) PayCosts(*State) bool{ return true } -func (*PassPriorityAction) String() string { return "pass" } -func NewPassPriorityAction(p *Player) Action { return &PassPriorityAction{p} } +func (*PassPriorityAction) Declare(*State) {} +func (*PassPriorityAction) Resolve(*State) {} +func (*PassPriorityAction) PayCosts(*State) bool { return true } +func (*PassPriorityAction) String() string { return "pass" } +func NewPassPriorityAction(p *Player) Action { return &PassPriorityAction{p} } type FastAction interface { Action @@ -62,30 +63,28 @@ type PlayAction struct { } func NewPlayAction(p *Player, c *Card, targets interface{}) Action { - p.Hand.RemoveCard(c) - - resolveFunc := func(s *State) { - s.ResolvePlay(p, c, targets) - } - costFunc := func(*State) bool { - var cost int - switch c.Type { - case UnitType: - cost = c.Values["upkeep"].(int) - default: - cost = c.Values["play"].(int) - } + cost := c.GetPlayCosts() if p.Ressource < cost { - log.Println("Cant pay", cost) return false } p.Ressource -= cost return true } + return NewPlayActionWithSpecialCosts(p, c, targets, costFunc) +} + +func NewPlayActionWithSpecialCosts(p *Player, c *Card, targets interface{}, + costFunc func(*State) bool, +) Action { + p.Hand.RemoveCard(c) + + resolveFunc := func(s *State) { + s.ResolvePlay(p, c, targets) + } desc := fmt.Sprintf("p %s@%v", c.Name, targets) - return &ActionBase{p, c, resolveFunc, costFunc, desc} + return &PlayAction{ActionBase{p, c, resolveFunc, costFunc, desc}, targets} } type MoveActionPrototype struct { @@ -96,8 +95,12 @@ func NewMoveActionPrototype(u *Unit) *MoveActionPrototype { return &MoveActionPrototype{u} } +func (proto *MoveActionPrototype) CanPayCosts(s *State) bool { + return proto.Unit.AvailMoveActions > 0 +} + func (proto *MoveActionPrototype) String() string { - return fmt.Sprintf("move %d", proto.Unit.Movement) + return fmt.Sprintf("move %s", proto.Unit.Movement.String()) } func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action { @@ -108,7 +111,8 @@ func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action { u.move(s, target) } - costFunc := func(*State) bool { + costFunc := func(*State) bool { + log.Printf("Move Actions of %s: %d\n", u.GetCard().Name, u.AvailMoveActions) if u.AvailMoveActions < 1 { return false } @@ -124,6 +128,10 @@ type AttackActionPrototype struct { Unit *Unit } +func (proto *AttackActionPrototype) CanPayCosts(s *State) bool { + return proto.Unit.AvailAttackActions > 0 +} + func NewAttackActionPrototype(u *Unit) *AttackActionPrototype { return &AttackActionPrototype{u} } @@ -156,6 +164,14 @@ type FullActionPrototype struct { Unit *Unit } +func NewFullActionPrototype(u *Unit) *FullActionPrototype { + return &FullActionPrototype{u} +} + +func (proto *FullActionPrototype) CanPayCosts(s *State) bool { + return proto.Unit.AvailAttackActions > 0 && proto.Unit.AvailMoveActions > 0 +} + func (proto *FullActionPrototype) String() string { return "full_action" } @@ -164,7 +180,7 @@ func (proto *FullActionPrototype) Specialize(args ...interface{}) Action { u := proto.Unit resolveFunc := args[0].(func(s *State)) costFunc := func(*State) bool { - if u.AvailAttackActions < 1 && u.AvailMoveActions < 1{ + if u.AvailAttackActions < 1 && u.AvailMoveActions < 1 { return false } u.AvailAttackActions-- @@ -176,7 +192,12 @@ func (proto *FullActionPrototype) Specialize(args ...interface{}) Action { } type FreeActionPrototype struct { - Permanent Permanent + Permanent Permanent + canPayFunc func(s *State) bool +} + +func (proto *FreeActionPrototype) CanPayCosts(s *State) bool { + return proto.canPayFunc(s) } func (proto *FreeActionPrototype) String() string { diff --git a/go/game/ai.go b/go/game/ai.go index 18e58957..1b4b4e79 100644 --- a/go/game/ai.go +++ b/go/game/ai.go @@ -1,21 +1,215 @@ package game -func moveToRandomTile(s *State, u *Unit) { - moveTargets := u.MoveRangeTiles(s.Map) - if len(moveTargets) > 0 { - moveTarget := moveTargets[s.Rand.Intn(len(moveTargets))-1] - s.MoveUnit(u, moveTarget) +import ( + "fmt" + "log" + "strconv" + "strings" + "sync" + + "github.com/RyanCarrier/dijkstra" +) + +type UnitAI struct { + s *State + u *Unit + actions chan Action + syncGameState *sync.WaitGroup +} + +func (ai *UnitAI) NextAction() Action { + return <-ai.actions +} + +func (ai *UnitAI) PostAndContinue(a Action) bool { + if a == nil { + return true } + + ai.actions <- a + return ai.awaitGameStateSync() } -func attackAttackableEnemyPerm(s *State, u *Unit) { - attackablePerms := u.AttackablePermanents(s.Map) - if len(attackablePerms) > 0 { - attackablePerm := attackablePerms[s.Rand.Intn(len(attackablePerms))-1] - s.Fight(u, attackablePerm) +func (ai *UnitAI) awaitGameStateSync() bool { + ai.syncGameState.Wait() + ai.syncGameState.Add(1) + return !ai.u.IsDestroyed() +} + +func (ai *UnitAI) promptAction() { + ai.syncGameState.Done() +} + +func (ai *UnitAI) Execute(f func(*UnitAI)) { + defer close(ai.actions) + if ai.awaitGameStateSync() { + f(ai) } } +func NewUnitAI(s *State, u *Unit) *UnitAI { + aiDesc := u.Card.getAI() + if aiDesc == "" { + return nil + } + + c := make(chan Action) + wg := new(sync.WaitGroup) + wg.Add(1) + ai := &UnitAI{s, u, c, wg} + + switch aiDesc { + case "aggressive": + go ai.Execute(AggressiveAI) + case "shy": + go ai.Execute(ShyAI) + case "wandering": + aiDesc := u.Card.getAI() + x, err := strconv.Atoi(strings.Split(u.Card.getAI(), " ")[1]) + if err != nil { + log.Fatalf("Invalid wandering AI description %s", aiDesc) + } + go ai.Execute(func(ai *UnitAI) { WanderingAI(ai, x) }) + case "target-oriented": + go ai.Execute(TargetOrientedAI) + } + + return ai +} + +func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph { + graph := dijkstra.NewGraph() + for _, t := range m.AllTiles() { + graph.AddMappedVertex(t.Position.String()) + } + + for _, t := range m.AllTiles() { + tId := t.Position.String() + for _, neighbour := range TilesInRangeFromOrigin(m, t.Position, 1) { + 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.Fatalf("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(u.GetTile().Position.String()) + if err != nil { + log.Fatalf("No mapping %s in Graph: %s", u.GetTile().Position.String(), err) + } + + destId, err := graph.GetMapping(pos.String()) + if err != nil { + log.Fatalf("No mapping %s in Graph: %s", pos.String(), err) + } + + return graph.Shortest(srcId, destId) +} + +func findPathsToUnits(graph *dijkstra.Graph, m *Map, u *Unit, units []*Unit) []*dijkstra.BestPath { + paths := []*dijkstra.BestPath{} + + for _, unit := range units { + var bestPathToUnit *dijkstra.BestPath = nil + for _, adjacentTile := range TilesInRangeFromOrigin(m, unit.GetTile().Position, 1) { + path, err := findPathTo(graph, u, adjacentTile.Position) + if err != nil { + continue + } else if bestPathToUnit == nil || path.Distance < bestPathToUnit.Distance { + bestPathToUnit = &path + } + } + paths = append(paths, bestPathToUnit) + } + return paths +} + +func moveToRandomTile(ai *UnitAI) Action { + if ai.u.AvailMoveActions == 0 { + return nil + } + + moveTargets := ai.u.MoveRangeTiles(ai.s.Map) + if len(moveTargets) == 0 { + return nil + } + + idx := 0 + if len(moveTargets) > 1 { + idx = ai.s.Rand.Intn(len(moveTargets) - 1) + } + moveTarget := moveTargets[idx] + a := NewMoveActionPrototype(ai.u).Specialize(moveTarget) + return a +} + +func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { + if ai.u.AvailMoveActions == 0 { + return nil + } + + graph := ai.s.Map.generateMapGraphFor(ai.u) + enemyUnits := ai.s.EnemyUnits(ai.u.GetController()) + if len(enemyUnits) == 0 { + return nil + } + + paths := findPathsToUnits(graph, ai.s.Map, ai.u, enemyUnits) + nearest := 0 + for i, p := range paths { + if p == nil { + continue + } + + if paths[nearest] == nil || p.Distance < paths[nearest].Distance { + nearest = i + } + } + + if paths[nearest] == nil { + return nil + } + + var target Position + for _, id := range paths[nearest].Path[1:] { + mapping, _ := graph.GetMapped(id) + var x, y int + fmt.Sscanf(mapping, "(%d, %d)", &x, &y) + nextStep := Position{X: x, Y: y} + if IsPositionInRange(ai.u.GetTile().Position, nextStep, ai.u.Movement.Range) { + target = nextStep + } else { + break + } + } + a := NewMoveActionPrototype(ai.u).Specialize(ai.s.Map.TileAt(target)) + return a +} + +func attackAttackableEnemyPerm(ai *UnitAI) Action { + attackablePerms := ai.u.AttackableEnemyPermanents(ai.s.Map) + if ai.u.AvailAttackActions == 0 || len(attackablePerms) == 0 { + return nil + } + + idx := 0 + if len(attackablePerms) > 1 { + idx = ai.s.Rand.Intn(len(attackablePerms) - 1) + } + attackablePerm := attackablePerms[idx] + a := NewAttackActionPrototype(ai.u).Specialize(attackablePerm) + return a +} + // 1. If enemy Unit in attack Range then // attack enemy Unit in Range // 2. While move action available do @@ -24,20 +218,27 @@ func attackAttackableEnemyPerm(s *State, u *Unit) { // if attack action avail then proceed at 1. // 3b. Otherwise // move to random tile in movement range -func AggressiveAi(s *State, u *Unit) { +func AggressiveAI(ai *UnitAI) { // 1. - attackAttackableEnemyPerm(s, u) + a := attackAttackableEnemyPerm(ai) + if !ai.PostAndContinue(a) { + return + } // 2. - if u.AvailMoveActions > 0 { - enemyUnits := s.EnemyUnits(u.GetController()) - if len(enemyUnits) > 0 { // 3a. - // TODO: implement path finding - if u.AvailAttackActions > 0 { - AggressiveAi(s, u) - } + if ai.u.AvailMoveActions > 0 { + a = moveTowardsNearestEnemyUnit(ai) + if !ai.PostAndContinue(a) { + return + } + + if a != nil && ai.u.AvailAttackActions > 0 { + AggressiveAI(ai) } else { // 3b. - moveToRandomTile(s, u) + a = moveToRandomTile(ai) + if a != nil { + ai.actions <- a + } } } } @@ -49,20 +250,22 @@ func AggressiveAi(s *State, u *Unit) { // move to most distant point from all enemy units // 3. If enemy Unit in attack Range then // attack enemy Unit in Range -func ShyAi(s *State, u *Unit) { +func ShyAI(ai *UnitAI) { // 1. inEnemyAttackRange := false out: - for _, enemyUnit := range s.EnemyUnits(u.GetController()) { - for _, p := range enemyUnit.AttackablePermanents(s.Map) { - if p == u { + for _, enemyUnit := range ai.s.EnemyUnits(ai.u.GetController()) { + for _, p := range enemyUnit.AttackablePermanents(ai.s.Map) { + if p == ai.u { inEnemyAttackRange = true break out } } } - if !inEnemyAttackRange && u.HasFullAction() { - u.FullAction() + if !inEnemyAttackRange && ai.u.HasFullAction() { + // return NewFullActionPrototype(u).Specialize(u.FullActionResolveFunc()) + a := NewFullActionPrototype(ai.u).Specialize() + ai.actions <- a } // 2. @@ -71,8 +274,8 @@ out: // } // 3. - if u.AvailAttackActions > 0 { - attackAttackableEnemyPerm(s, u) + if ai.u.AvailAttackActions > 0 { + attackAttackableEnemyPerm(ai) } } @@ -82,18 +285,18 @@ out: // 2. If move action available then // move to random tile in movement Range // proceed at 1. -func WanderingAi(s *State, u *Unit, x int) { +func WanderingAI(ai *UnitAI, x int) { // 1. - for _, enemyUnit := range s.EnemyUnits(u.GetController()) { - if IsPositionInRange(u.GetTile().Position, enemyUnit.GetTile().Position, x) { - AggressiveAi(s, u) + for _, enemyUnit := range ai.s.EnemyUnits(ai.u.GetController()) { + if IsPositionInRange(ai.u.GetTile().Position, enemyUnit.GetTile().Position, x) { + AggressiveAI(ai) break } } // 2. - if u.AvailMoveActions > 0 { - moveToRandomTile(s, u) - WanderingAi(s, u, x) + if ai.u.AvailMoveActions > 0 { + moveToRandomTile(ai) + WanderingAI(ai, x) } } @@ -108,16 +311,16 @@ func WanderingAi(s *State, u *Unit, x int) { // 2b. Otherwise // // execute wandering 3 AI -func TargetOrientedAi(s *State, u *Unit, t *Position) { +func TargetOrientedAI(ai *UnitAI) { // 1. - if u.HasFullAction() { - u.FullAction() + if ai.u.HasFullAction() { + ai.u.FullAction() } - if t != nil { - // TODO: path finding again - attackAttackableEnemyPerm(s, u) - } else { - WanderingAi(s, u, 3) - } + // if ai.t != nil { + // // TODO: path finding again + // attackAttackableEnemyPerm(ai) + // } else { + WanderingAI(ai, 3) + // } } diff --git a/go/game/ai_test.go b/go/game/ai_test.go new file mode 100644 index 00000000..5b68ae27 --- /dev/null +++ b/go/game/ai_test.go @@ -0,0 +1,66 @@ +package game + +import ( + "strings" + "testing" +) + +func TestGenerateGraph(t *testing.T) { + mapDef := `map: |1- + HST + HSF + TST +symbols: + T: tower + H: house + F: farm + S: street +` + r := strings.NewReader(mapDef) + m, _ := readMap(r) + a := NewUnit(NewCard("base/archer"), m.TileAt(Position{0, 0}), nil) + f := NewUnit(NewCard("misc/farmer"), m.TileAt(Position{2, 2}), nil) + + graph := m.generateMapGraphFor(a) + p, err := findPathTo(graph, a, Position{1, 1}) + if err != nil { + t.Fatal(err) + } + + if p.Distance != 3 { + t.Fatalf("Wrong distance %d", p.Distance) + } + + paths := findPathsToUnits(graph, m, a, []*Unit{f}) + if paths[0] == nil { + t.Fatal("No path found") + } + + if paths[0].Distance != 3 { + t.Fatalf("Wrong distance %d", p.Distance) + } +} + +func TestAngrySquidDiagonal(t *testing.T) { + mapDef := `map: |1- + WWWWWWW + W W + WW WW + WWWWWWW + WWWWWWW + W W +symbols: + W: deep water +` + r := strings.NewReader(mapDef) + m, _ := readMap(r) + s := NewUnit(NewCard("kraken/angry_squid"), m.TileAt(Position{5, 0}), nil) + f := NewUnit(NewCard("misc/farmer"), m.TileAt(Position{6, 5}), nil) + + graph := m.generateMapGraphFor(s) + + paths := findPathsToUnits(graph, m, s, []*Unit{f}) + if paths[0] == nil { + t.Fatal("No path found") + } +} diff --git a/go/game/attack.go b/go/game/attack.go new file mode 100644 index 00000000..97881d48 --- /dev/null +++ b/go/game/attack.go @@ -0,0 +1,50 @@ +package game + +import ( + "fmt" + "log" + "strconv" + "strings" +) + +type Attack struct { + Damage int + Range int +} + +func INVALID_ATTACK() Attack { + return Attack{-1, -1} +} + +func (a Attack) String() string { + if a == INVALID_ATTACK() { + return "invalid attack" + } + + if a.Range == 1 { + return fmt.Sprintf("%d", a.Damage) + } + return fmt.Sprintf("%d range %d", a.Damage, a.Range) +} + +func parseAttack(attack interface{}) Attack { + if damage, ok := attack.(int); ok { + return Attack{damage, 1} + } + + attackStr := attack.(string) + tokens := strings.Split(attackStr, " ") + a, err := strconv.Atoi(tokens[0]) + if err != nil { + log.Fatalf("Attack %s has no valid Damage value\n", attackStr) + } + + r := 1 + if len(tokens) == 3 { + r, err = strconv.Atoi(tokens[2]) + if err != nil { + log.Fatalf("Attack %s has no valid Range value\n", attackStr) + } + } + return Attack{a, r} +} diff --git a/go/game/card.go b/go/game/card.go index 87bbe723..6f8a74a8 100644 --- a/go/game/card.go +++ b/go/game/card.go @@ -5,8 +5,10 @@ import ( "log" "net/http" "path" + "strconv" "strings" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) @@ -141,24 +143,65 @@ func (card *Card) parseDefinition(definition []byte) { } } -func (card *Card) getEffects() []string { - effects, found := card.Values["effect"] +func (card *Card) getCanonicalValues(key string) []string { + var values []string + untypedValues, found := card.Values[key] if !found { return []string{} } - if effect, ok := effects.(string); ok { - return []string{effect} + switch typedValues := untypedValues.(type) { + case map[string]interface{}: + enValues := typedValues["en"] + switch typedEnValues := enValues.(type) { + case string: + values = []string{typedEnValues} + case []string: + values = typedEnValues + case []interface{}: + values = []string{} + for _, v := range typedEnValues { + values = append(values, v.(string)) + } + } + case string: + values = []string{typedValues} + case int: + values = []string{strconv.Itoa(typedValues)} + case []string: + values = typedValues } - if effectsMap, ok := effects.(map[string]interface{}); ok { - enEffects := effectsMap["en"] - if effect, ok := enEffects.(string); ok { - return []string{effect} - } - return enEffects.([]string) + for i, value := range values { + values[i] = strings.ToLower(value) } - return effects.([]string) + + return values +} + +func (card *Card) getEffects() []string { + return card.getCanonicalValues("effect") +} + +func (card *Card) hasEffect(effect string) bool { + return slices.Contains(card.getEffects(), effect) +} + +func (card *Card) hasMovementEffect(effect string) bool { + if slices.Contains(card.getEffects(), effect) { + return true + } + + movementDesc := card.getCanonicalValues("movement")[0] + return strings.Contains(movementDesc, effect) +} + +func (card *Card) getAI() string { + aiDesc := card.getCanonicalValues("ai") + if len(aiDesc) == 0 { + return "" + } + return strings.Split(aiDesc[0], " ")[0] } func NewCard(cardPath string) *Card { @@ -186,3 +229,12 @@ func (c *Card) Path() string { func (c *Card) IsPermanent() bool { return c.Type != UndefinedCardType && c.Type != SpellType && c.Type != IntentionType } + +func (c *Card) GetPlayCosts() int { + switch c.Type { + case UnitType: + return c.Values["upkeep"].(int) + default: + return c.Values["play"].(int) + } +} diff --git a/go/game/deck.go b/go/game/deck.go index 58a08985..f1290fa4 100644 --- a/go/game/deck.go +++ b/go/game/deck.go @@ -5,7 +5,6 @@ import ( "math/rand" "strconv" "strings" - "time" ) type Deck struct { @@ -17,16 +16,21 @@ func NewDeck() *Deck { return &d } -func (d *Deck) Shuffle() { - rand.Seed(time.Now().UnixNano()) - rand.Shuffle(d.Size(), func(i, j int) { +func (d *Deck) Shuffle(r *rand.Rand) { + r.Shuffle(d.Size(), func(i, j int) { d.cards[i], d.cards[j] = d.cards[j], d.cards[i] }) } func (d *Deck) Draw(amount int) []*Card { + if d.Size() < amount { + drawn := d.cards + d.cards = []*Card{} + return drawn + } + drawn := d.cards[0:amount] - d.cards = d.cards[:amount] + d.cards = d.cards[amount:] return drawn } diff --git a/go/game/kraken.go b/go/game/kraken.go index d88d87a3..b0108361 100644 --- a/go/game/kraken.go +++ b/go/game/kraken.go @@ -2,49 +2,145 @@ package game import ( "image/color" + "log" + "strings" + "sync" ) -var KrakenDecklist = `angry_squid -chachalot -clownfish -deja_vu! -devour_the_poor! -dolphin -drowned_sailor -flying_dutchmen -frost_pylon -gigantic_hail -ice_berg -sailfish -sold_out! -suprise! -tentacle_slap -the_kraken -tides_change! -unholy_cannonball` +// var KrakenDecklist = `2 kraken/angry_squid +// 2 kraken/chachalot +// 2 kraken/clownfish +// 2 kraken/deja_vu! +// 2 kraken/devour_the_poor! +// 2 kraken/dolphin +// 2 kraken/flying_dutchmen +// 2 kraken/frost_pylon +// 2 kraken/gigantic_hail +// 2 kraken/sailfish +// 2 kraken/sold_out! +// 2 kraken/suprise! +// 2 kraken/tentacle_slap +// 2 kraken/tides_change! +// 2 kraken/unholy_cannonball` + +var KrakenDecklist = `1 kraken/angry_squid` func NewKraken(id int, s *State, c color.Color) *Player { d := NewDeckFromDeckList(KrakenDecklist) kraken := NewPlayer(id, "The Kraken", d, NewStore(), s, c) + d.Shuffle(s.Rand) + kraken.DrawPerTurn = 0 - kraken.Ctrl = &KrakenControl{kraken} + + the_kraken := NewUnitFromPath("kraken/the_kraken", s.Map.TileAt(Position{3, 1}), kraken) + s.AddPermanent(the_kraken) + + ctrl := &KrakenControl{kraken, make(chan Action), new(sync.WaitGroup)} + ctrl.syncGameState.Add(1) + + kraken.Ctrl = ctrl + go func() { + for len(s.Map.WinCondition(s)) == 0 { + ctrl.KrakenTurn() + } + }() + return kraken } type KrakenControl struct { - kraken *Player + kraken *Player + actions chan Action + syncGameState *sync.WaitGroup +} + +func (ctrl *KrakenControl) awaitGameStateSync() { + ctrl.syncGameState.Wait() + ctrl.syncGameState.Add(1) +} + +func (ctrl *KrakenControl) KrakenTurn() { + s := ctrl.kraken.gameState + kraken := ctrl.kraken + + ctrl.awaitGameStateSync() + + for { + drawn := kraken.Deck.Draw(1) + if len(drawn) == 0 { + break + } + c := drawn[0] + + if c.Name == "Frost Pylon" { + // TODO: implement frost pylon placement + kraken.DiscardPile.AddCard(c) + continue + } + + if c.BuyCost > kraken.Ressource { + log.Println("Kraken discards", c.Name, "because ressource to low", kraken.Ressource) + kraken.DiscardPile.AddCard(c) + break + } + kraken.Hand.AddCard(c) + + costFunc := func(s *State) bool { + kraken.Ressource -= c.BuyCost + return true + } + + // TODO: select targets + var targets interface{} + if c.IsPermanent() { + spawns := s.AvailableSpawnTiles(kraken, c) + if len(spawns) == 0 { + log.Println("No spawn available for", c.Name) + kraken.DiscardPile.AddCard(c) + continue + } + + idx := 0 + if len(spawns) > 1 { + idx = s.Rand.Intn(len(spawns) - 1) + } + targets = spawns[idx].Position + } + ctrl.actions <- NewPlayActionWithSpecialCosts(kraken, c, targets, costFunc) + ctrl.awaitGameStateSync() + } + + for _, u := range s.OwnUnits(kraken) { + unitAI := NewUnitAI(s, u) + if unitAI == nil { + continue + } + + unitAI.promptAction() + for a := range unitAI.actions { + ctrl.actions <- a + ctrl.awaitGameStateSync() + unitAI.promptAction() + } + } + + ctrl.actions <- NewPassPriorityAction(ctrl.kraken) } func (ctrl *KrakenControl) RecvAction() Action { - if ctrl.kraken.gameState.ActivePlayerId == ctrl.kraken.Id { + s := ctrl.kraken.gameState + kraken := ctrl.kraken + if s.ActivePlayerId != kraken.Id || + s.ActivePhase != actionPhase || !s.Stack.IsEmpty() { + return NewPassPriorityAction(ctrl.kraken) } - // TODO: implement AI - return NewPassPriorityAction(ctrl.kraken) + ctrl.syncGameState.Done() + return <-ctrl.actions } -func (*KrakenControl) SendNotification(PlayerNotification) { +func (ctrl *KrakenControl) SendNotification(n PlayerNotification) { } func (s *State) FindKraken() *Player { @@ -55,3 +151,9 @@ func (s *State) FindKraken() *Player { } return nil } + +func (s *State) KrakenSpawnTiles() []*Tile { + return s.Map.FilterTiles(func(t *Tile) bool { + return strings.Contains(t.Raw, "deep water spawn") + }) +} diff --git a/go/game/map.go b/go/game/map.go index 5783c9dd..72e17ad4 100644 --- a/go/game/map.go +++ b/go/game/map.go @@ -10,7 +10,6 @@ import ( "os" "path" "strings" - "time" "gopkg.in/yaml.v3" ) @@ -25,8 +24,9 @@ const ( ) type MapYml struct { - Map string `yaml:"map"` - Symbols map[string]string `yaml:"symbols"` + Map string `yaml:"map"` + Symbols map[string]string `yaml:"symbols"` + StartDeckList string `yaml:"start_deck_list"` } func DummyWinCondition(*State) []*Player { @@ -61,10 +61,14 @@ func ParseMap(data []byte) (*Map, error) { m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK, DummyWinCondition} + if mapYml.StartDeckList != "" { + m.StartDeckList = mapYml.StartDeckList + } + rows := strings.Split(mapYml.Map, "\n") for y := 0; y < len(rows); y++ { - m.Tiles = append(m.Tiles, []Tile{}) row := rows[y] + m.Tiles = append(m.Tiles, []Tile{}) for x := 0; x < len(rows[y]); x++ { s := string(row[x]) tile, err := NewTileFromString(mapYml.Symbols[s], Position{x, y}) @@ -107,7 +111,7 @@ func (m *Map) FindNeighbours(x, y int) (left, right, above, below *Tile) { above = &m.Tiles[y-1][x] } - if y < len(m.Tiles) { + if y < len(m.Tiles)-1 { below = &m.Tiles[y+1][x] } @@ -115,7 +119,7 @@ func (m *Map) FindNeighbours(x, y int) (left, right, above, below *Tile) { left = &m.Tiles[y][x-1] } - if x < len(m.Tiles[y]) { + if x < len(m.Tiles[y])-1 { right = &m.Tiles[y][x+1] } @@ -198,7 +202,7 @@ func (m *Map) TileAt(p Position) *Tile { return &m.Tiles[p.Y][p.X] } -func (m *Map) RandomTile(types []TileType) Position { +func (m *Map) RandomTile(r *rand.Rand, types []TileType) Position { candidates := []Position{} for y := 0; y < len(m.Tiles); y++ { for x := 0; x < len(m.Tiles[y]); x++ { @@ -212,12 +216,11 @@ func (m *Map) RandomTile(types []TileType) Position { } } - r := rand.New(rand.NewSource(time.Now().UnixNano())) randIdx := r.Intn(len(candidates)) return candidates[randIdx] } -func (m *Map) RandomTileFromSymbols(symbols []string) (Position, error) { +func (m *Map) RandomTileFromSymbols(r *rand.Rand, symbols []string) (Position, error) { types := make([]TileType, 0, len(symbols)) for _, symbol := range symbols { tileString, ok := m.symbols[symbol] @@ -232,7 +235,7 @@ func (m *Map) RandomTileFromSymbols(symbols []string) (Position, error) { types = append(types, tileType) } - return m.RandomTile(types), nil + return m.RandomTile(r, types), nil } func (m *Map) FilterTiles(filter func(t *Tile) bool) []*Tile { @@ -247,3 +250,7 @@ func (m *Map) FilterTiles(filter func(t *Tile) bool) []*Tile { } return tiles } + +func (m *Map) AllTiles() []*Tile { + return m.FilterTiles(func(*Tile) bool { return true }) +} diff --git a/go/game/map_test.go b/go/game/map_test.go index c8933c69..96efc459 100644 --- a/go/game/map_test.go +++ b/go/game/map_test.go @@ -5,8 +5,33 @@ import ( "testing" ) +func parseMapString(t *testing.T, mapDef string) *Map { + r := strings.NewReader(mapDef) + m, err := readMap(r) + if err != nil { + t.Fatalf("Parsing failed with: %v", err) + } + return m +} + +func TestParseMapEndingStreets(t *testing.T) { + mapDef := `map: |1- + HST + HSF + TST + +symbols: + T: tower + H: house + F: farm + S: street +` + + parseMapString(t, mapDef) +} + func TestParseMap(t *testing.T) { - s := `map: |1+ + s := `map: |1- T H HT FSSSSS2H SHSH HS @@ -29,18 +54,14 @@ kings: - (1,9) - (9,1)` - r := strings.NewReader(s) - m, err := readMap(r) - if err != nil { - t.Fatalf("Parsing failed with: %v", err) - } + m := parseMapString(t, s) if m.Tiles[0][0].Type != TileTypes.Tower { t.Fatalf("Unexpected tile at 0,0: %v (expected %v)", m.Tiles[0][0], TileTypes.Tower) } for y := 0; y < len(m.Tiles); y++ { - for x := 0; y < len(m.Tiles[y]); y++ { + for x := 0; x < len(m.Tiles[y]); x++ { if m.TileAt(Position{x, y}) != &m.Tiles[y][x] { t.Fatalf("TileAt and Tiles differ") } diff --git a/go/game/movement.go b/go/game/movement.go new file mode 100644 index 00000000..df7e4138 --- /dev/null +++ b/go/game/movement.go @@ -0,0 +1,47 @@ +package game + +import ( + "fmt" + "log" + "strconv" + "strings" +) + +type Movement struct { + Range int + Swimming bool +} + +func INVALID_MOVEMENT() Movement { + return Movement{-1, false} +} + +func parseMovement(desc interface{}) Movement { + if r, ok := desc.(int); ok { + return Movement{r, false} + } + + movementStr := desc.(string) + tokens := strings.Split(movementStr, " ") + r, err := strconv.Atoi(tokens[0]) + if err != nil { + log.Fatalf("Invalid movement description %s", movementStr) + } + + swimming := false + if len(tokens) > 1 && tokens[1] == "swimming" { + swimming = true + } + + return Movement{r, swimming} +} + +func (m Movement) String() string { + if m == INVALID_MOVEMENT() { + return "invalid movement" + } + if m.Swimming { + return fmt.Sprintf("%d swimming", m.Range) + } + return fmt.Sprintf("%d", m.Range) +} diff --git a/go/game/permanent.go b/go/game/permanent.go index ce1d3105..acdcd85f 100644 --- a/go/game/permanent.go +++ b/go/game/permanent.go @@ -19,6 +19,7 @@ type Permanent interface { IsDestroyed() bool Attackable() bool GetAvailActionPrototypes() []ActionPrototype + Swimming() bool } type PermanentBase struct { @@ -69,3 +70,11 @@ func (p *PermanentBase) String() string { func (p *PermanentBase) AddDamage(damage int) { p.Damage += damage } + +func (p *PermanentBase) Swimming() bool { + if p.Card.Type == UnitType { + return parseMovement(p.Card.getCanonicalValues("movement")).Swimming + } else { + return p.Card.hasEffect("swimming") + } +} diff --git a/go/game/phase.go b/go/game/phase.go new file mode 100644 index 00000000..efcd430a --- /dev/null +++ b/go/game/phase.go @@ -0,0 +1,28 @@ +package game + +type PhaseType int + +const ( + drawPhase = iota + upkeepPhase + actionPhase + buyPhase + discardPhase +) + +func (p PhaseType) String() string { + switch p { + case drawPhase: + return "Draw" + case upkeepPhase: + return "Upkeep" + case actionPhase: + return "Actions" + case buyPhase: + return "Buy" + case discardPhase: + return "Discard" + } + + return "" +} diff --git a/go/game/player.go b/go/game/player.go index 1e8c32f9..017e8407 100644 --- a/go/game/player.go +++ b/go/game/player.go @@ -2,6 +2,7 @@ package game import ( "image/color" + "log" ) const ( @@ -12,24 +13,40 @@ type Player struct { Id int Name string Turn int + Ressource int DrawPerTurn int Hand *Hand DiscardPile *DiscardPile Deck *Deck Store *Store - Ressource int gameState *State Color color.Color Ctrl PlayerControl } -func NewPlayer(id int, name string, deck *Deck, store *Store, gameState *State, color color.Color) *Player { - p := Player{id, name, 0, MAX_DRAW, NewHand(), NewDiscardPile(), deck, store, 0, gameState, color, nil} +func NewPlayer(id int, name string, + deck *Deck, store *Store, + gameState *State, color color.Color, +) *Player { + p := Player{ + id, name, + 0, 0, MAX_DRAW, + NewHand(), NewDiscardPile(), deck, store, + gameState, color, nil, + } return &p } +func (p *Player) resourceGain() int { + if p.Name == "The kraken" { + return p.gameState.Map.RessourceGain * (len(p.gameState.Players) - 1) + } else { + return p.gameState.Map.RessourceGain + } +} + func (p *Player) Upkeep() { - p.Ressource += p.gameState.Map.RessourceGain + p.Ressource += p.resourceGain() // TODO: handle upkeep triggers @@ -40,15 +57,21 @@ func (p *Player) Upkeep() { // TODO: let players decide if they want to pay the upkeep p.Ressource -= unit.Upkeep + // TODO: better refresh unit actions + unit.AvailAttackActions = 1 + unit.AvailMoveActions = 1 } } func (p *Player) ActionPhase() { - a := PromptAction(p.Ctrl) - for _, ok := a.(*PassPriorityAction); !ok; { + for { + a := PromptAction(p.Ctrl) + log.Printf("Received %T action from %s\n", a, p.Name) + if _, ok := a.(*PassPriorityAction); ok { + return + } p.gameState.DeclareAction(a) p.gameState.Stack.Resolve() - a = PromptAction(p.Ctrl) } } @@ -57,7 +80,7 @@ func (p *Player) buy(c *Card) { p.DiscardPile.AddCard(c) } -func (p *Player) BuyStep() { +func (p *Player) BuyPhase() { // TODO: select card to buy } @@ -65,7 +88,7 @@ func (p *Player) DiscardCard(c *Card) { p.Hand.MoveCard(c, p.DiscardPile) } -func (p *Player) DiscardStep() { +func (p *Player) DiscardPhase() { // TODO: select cards to discard } @@ -75,9 +98,10 @@ func (p *Player) Draw() { return } - drawnCards := p.Deck.Draw(toDraw) - p.Hand.AddCards(drawnCards) + p.DrawN(toDraw) } -func (p *Player) PlayPermanent(card *Card) { +func (p *Player) DrawN(n int) { + drawnCards := p.Deck.Draw(n) + p.Hand.AddCards(drawnCards) } diff --git a/go/game/playerControl.go b/go/game/playerControl.go index 2491dab7..6d5e8e14 100644 --- a/go/game/playerControl.go +++ b/go/game/playerControl.go @@ -1,5 +1,4 @@ package game -import "log" type PlayerNotificationType int @@ -9,6 +8,10 @@ const ( PriorityNotification ) +func (n PlayerNotification) IsPriorityNotification() bool { + return n.Notification == PriorityNotification +} + type PlayerNotification struct { Notification PlayerNotificationType Action Action @@ -34,7 +37,7 @@ type PlayerControl interface { type DummyPlayerControl struct{ P *Player } func (c *DummyPlayerControl) RecvAction() Action { return NewPassPriorityAction(c.P) } -func (*DummyPlayerControl) SendNotification(PlayerNotification) { return } +func (*DummyPlayerControl) SendNotification(PlayerNotification) {} type ChanPlayerControl struct { Actions chan Action @@ -60,7 +63,6 @@ func NewChanPlayerControl() *ChanPlayerControl { } func PromptAction(ctrl PlayerControl) Action { - log.Println("PromptAction") ctrl.SendNotification(NewPriorityNotification()) return ctrl.RecvAction() } diff --git a/go/game/range.go b/go/game/range.go index 1aa50f8a..af1e8fd0 100644 --- a/go/game/range.go +++ b/go/game/range.go @@ -7,6 +7,10 @@ import ( func IsPositionInRange(origin Position, pos Position, r int) bool { x, y := float64(pos.X-origin.X), float64(pos.Y-origin.Y) ax, ay := math.Abs(x), math.Abs(y) + if ax > float64(r) || ay > float64(r) { + return false + } + var abs float64 if pos.X > pos.Y { abs = ax + math.Floor(ay/2) @@ -38,9 +42,9 @@ func PositionsInRange(origin Position, r int, includeOrigin bool) []Position { return positions } -func TilesInRange(m *Map, p Permanent, r int) []*Tile { +func TilesInRangeFromOrigin(m *Map, origin Position, r int) []*Tile { tiles := []*Tile{} - for _, pos := range PositionsInRange(p.GetTile().Position, r, false) { + for _, pos := range PositionsInRange(origin, r, false) { tile := m.TileAt(pos) if tile != nil { tiles = append(tiles, tile) @@ -48,3 +52,7 @@ func TilesInRange(m *Map, p Permanent, r int) []*Tile { } return tiles } + +func TilesInRange(m *Map, p Permanent, r int) []*Tile { + return TilesInRangeFromOrigin(m, p.GetTile().Position, r) +} diff --git a/go/game/range_test.go b/go/game/range_test.go index 274b3b85..4b6e05ad 100644 --- a/go/game/range_test.go +++ b/go/game/range_test.go @@ -51,3 +51,11 @@ func TestPositionsInRange(t *testing.T) { } } } + +func TestPositionNotInRange(t *testing.T) { + o := Position{5, 0} + p := Position{6, 3} + if IsPositionInRange(o, p, 2) { + t.Fatalf("Invalid range inclusion %v not in %v", o, PositionsInRange(o, 2, false)) + } +} diff --git a/go/game/state.go b/go/game/state.go index 1a6ae093..29267c28 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -6,6 +6,7 @@ import ( "math/rand" "strconv" "strings" + "time" ) type State struct { @@ -17,15 +18,17 @@ type State struct { DiscardPiles []*DiscardPile Players []*Player ActivePlayerId int + ActivePhase PhaseType Permanents []Permanent Units []*Unit Artifacts []*Artifact - Rand rand.Rand + Rand *rand.Rand } func NewState() *State { s := &State{} s.Stack = NewStack(s) + s.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) return s } @@ -38,33 +41,32 @@ func (s *State) Loop() []*Player { winners := []*Player{} for len(winners) == 0 { for i, p := range s.Players { - log.Printf("Starting %s turn %d\n", p.Name, p.Turn) s.ActivePlayerId = i p.Turn++ if p.Deck.IsEmpty() { log.Printf("Shuffle %s pile into the deck\n", p.Name) p.DiscardPile.MoveInto(p.Deck) - p.Deck.Shuffle() + p.Deck.Shuffle(s.Rand) } - log.Printf("%s draw\n", p.Name) + s.ActivePhase = drawPhase p.Draw() - log.Printf("%s Upkeep\n", p.Name) + s.ActivePhase = upkeepPhase p.Upkeep() s.PhaseChange(i) - log.Printf("%s Actions\n", p.Name) + s.ActivePhase = actionPhase p.ActionPhase() s.PhaseChange(i) - log.Printf("%s Buystep\n", p.Name) - p.BuyStep() + s.ActivePhase = buyPhase + p.BuyPhase() s.PhaseChange(i) - log.Printf("%s Discard\n", p.Name) - p.DiscardStep() + s.ActivePhase = discardPhase + p.DiscardPhase() winners = s.Map.WinCondition(s) } @@ -75,7 +77,7 @@ func (s *State) Loop() []*Player { func (s *State) PushAction(a Action) { if !a.PayCosts(s) { - log.Fatalf("Cost for a could not be paid") + log.Fatalf("Cost for %s could not be paid", a) } s.Stack.Push(a) } @@ -84,6 +86,11 @@ func (s *State) DeclareAction(a Action) { if !s.Stack.IsEmpty() { log.Fatalf("Slow action can not be declared while other actions are on the stack") } + + if s.ActivePhase != actionPhase { + log.Fatalf("Slow action can not be declared during one's actions phase") + } + s.PushAction(a) } @@ -101,14 +108,14 @@ func (s *State) AllPassing() bool { case *PassPriorityAction: log.Printf("%s passed %d/%d\n", p.Name, i, nPlayers) default: - s.Stack.Push(a) + s.DeclareAction(a) return false } } return true } -func (s *State) PhaseChange(activePlaerId int) { +func (s *State) PhaseChange(activePlayerId int) { log.Println("Phase change") for !s.AllPassing() { s.Stack.Resolve() @@ -122,19 +129,6 @@ func (s *State) broadcastNotification(n PlayerNotification) { } } -func (s *State) Upkeep(p *Player) { - p.Ressource += s.Map.RessourceGain - // TODO: handle upkeep trigger - for _, unit := range s.Units { - if unit.Controller != p { - continue - } - - // TODO: let players decide if they want to pay the upkeep - p.Ressource -= unit.Upkeep - } -} - func (s *State) AddPermanent(perm Permanent) { s.Permanents = append(s.Permanents, perm) switch p := perm.(type) { @@ -191,6 +185,7 @@ func (s *State) DestroyPermanent(p Permanent) { case *Artifact: s.Artifacts = removePtr(s.Artifacts, p) } + p.GetOwner().DiscardPile.AddCard(p.GetCard()) } func (s *State) Fight(p1, p2 Permanent) { @@ -208,26 +203,49 @@ func (s *State) MoveUnit(u *Unit, t *Tile) { u.move(s, t) } -func (s *State) EnemyUnits(player *Player) []*Unit { - // TODO: support coop / teams - enemyUnits := []*Unit{} +func (s *State) FilterUnits(filter func(*Unit) bool) []*Unit { + units := []*Unit{} for _, u := range s.Units { - if u.GetController() != player { - enemyUnits = append(enemyUnits, u) + if filter(u) { + units = append(units, u) } } - return enemyUnits + return units +} + +func (s *State) OwnUnits(player *Player) []*Unit { + return s.FilterUnits(func(u *Unit) bool { + return u.GetController() == player + }) +} + +func (s *State) EnemyUnits(player *Player) []*Unit { + return s.FilterUnits(func(u *Unit) bool { + // TODO: support coop / teams + return u.GetController() != player + }) } func (s *State) ResolvePlay(p *Player, c *Card, targets interface{}) { switch c.Type { case UnitType: s.AddNewUnit(c, targets.(Position), p) + case ArtifactType: + s.AddNewArtifact(c, targets.(Position), p) + default: + log.Printf("Resolving unhandled card type: %s\n", c.Type.String()) + } + + if !c.IsPermanent() { + p.DiscardPile.AddCard(c) } - p.DiscardPile.AddCard(c) } func (s *State) SpawnTiles(p *Player) []*Tile { + if p.Name == "The Kraken" { + return s.KrakenSpawnTiles() + } + spawns := s.Map.FilterTiles(func(t *Tile) bool { if t.Type != TileTypes.Spawn { return false @@ -238,3 +256,20 @@ func (s *State) SpawnTiles(p *Player) []*Tile { return spawns } + +func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile { + if !c.IsPermanent() { + log.Fatalf("AvailableSpawnTiles called for %s", c.Type.String()) + } + + candidates := s.SpawnTiles(p) + tiles := []*Tile{} + + for _, candidate := range candidates { + if candidate.IsAvailableForCard(c) { + tiles = append(tiles, candidate) + } + } + + return tiles +} diff --git a/go/game/tile.go b/go/game/tile.go index cbceb6f7..55e00b8f 100644 --- a/go/game/tile.go +++ b/go/game/tile.go @@ -1,7 +1,6 @@ package game import ( - "log" "strings" ) @@ -31,7 +30,6 @@ func (t TileType) String() string { case spawn: return "spawn" } - log.Panicf("Switch not exhausting %d", t) return "" } @@ -64,7 +62,7 @@ type Tile struct { Position Position Permanent Permanent Type TileType - Water bool + Water bool Raw string } @@ -75,7 +73,7 @@ func INVALID_TILE() Tile { func NewTileFromString(t string, pos Position) (Tile, error) { tile := strings.ToLower(t) tokens := strings.Split(tile, " ") - tileType, found:= TileNames[tokens[0]] + tileType, found := TileNames[tokens[0]] if !found { if strings.Contains(tile, "spawn") { tileType = spawn @@ -88,3 +86,21 @@ func NewTileFromString(t string, pos Position) (Tile, error) { func (t *Tile) IsFree() bool { return t.Permanent == nil } + +func (t *Tile) IsAvailableForCard(card *Card) bool { + if t.Water && !card.hasMovementEffect("swimming") { + return false + } + if !t.Water && card.hasMovementEffect("swimming") { + return false + } + return t.IsFree() +} + +func (t *Tile) OnDiagonal(other *Tile) bool { + p1, p2 := t.Position, other.Position + return p1.X == p2.X-1 && p1.Y == p2.Y-1 || + p1.X == p2.X+1 && p1.Y == p2.Y-1 || + p1.X == p2.X+1 && p1.Y == p2.Y+1 || + p1.X == p2.X-1 && p1.Y == p2.Y+1 +} diff --git a/go/game/unit.go b/go/game/unit.go index 02ba9830..0d39e904 100644 --- a/go/game/unit.go +++ b/go/game/unit.go @@ -1,10 +1,7 @@ package game import ( - "fmt" "log" - "strconv" - "strings" "golang.org/x/exp/slices" ) @@ -19,44 +16,10 @@ const ( Ward ) -type Attack struct { - Damage int - Range int -} - -func (a *Attack) String() string { - if a.Range == 1 { - return fmt.Sprintf("%d", a.Damage) - } - return fmt.Sprintf("%d range %d", a.Damage, a.Range) -} - -func parseAttack(attack interface{}) Attack { - if damage, ok := attack.(int); ok { - return Attack{damage, 1} - } - - attackStr := attack.(string) - tokens := strings.Split(attackStr, " ") - a, err := strconv.Atoi(tokens[0]) - if err != nil { - log.Fatalf("Attack %s has no valid Damage value\n", attackStr) - } - - r := 1 - if len(tokens) == 3 { - r, err = strconv.Atoi(tokens[2]) - if err != nil { - log.Fatalf("Attack %s has no valid Range value\n", attackStr) - } - } - return Attack{a, r} -} - type Unit struct { PermanentBase Health int - Movement int + Movement Movement Attack Attack Upkeep int FullActions []*FullActionPrototype @@ -68,12 +31,33 @@ type Unit struct { } func NewUnit(card *Card, tile *Tile, owner *Player) *Unit { + var movement Movement + movementDesc, found := card.Values["movement"] + if !found { + movement = INVALID_MOVEMENT() + } else { + movement = parseMovement(movementDesc) + } + + upkeep, ok := card.Values["upkeep"].(int) + if !ok { + upkeep = 0 + } + + var attack Attack + attackDesc, found := card.Values["attack"] + if !found { + attack = INVALID_ATTACK() + } else { + attack = parseAttack(attackDesc) + } + u := &Unit{ NewPermanentBase(card, tile, owner), card.Values["health"].(int), - card.Values["movement"].(int), - parseAttack(card.Values["attack"]), - card.Values["upkeep"].(int), + movement, + attack, + upkeep, []*FullActionPrototype{}, []*FreeActionPrototype{}, []UnitState{}, @@ -98,12 +82,13 @@ func (u *Unit) move(s *State, tile *Tile) { } u.Tile.Permanent = nil + tile.Permanent = u u.Tile = tile } func (u *Unit) MoveRangeTiles(m *Map) []*Tile { tiles := []*Tile{} - for _, pos := range PositionsInRange(u.Tile.Position, u.Movement, false) { + for _, pos := range PositionsInRange(u.Tile.Position, u.Movement.Range, false) { tile := m.TileAt(pos) if tile != nil && u.IsAvailableTile(tile) { tiles = append(tiles, tile) @@ -126,8 +111,21 @@ func (u *Unit) AttackablePermanents(m *Map) []Permanent { return permanents } +func (u *Unit) AttackableEnemyPermanents(m *Map) []Permanent { + controller := u.GetController() + permanents := []Permanent{} + for _, t := range u.AttackableTiles(m) { + if t.Permanent != nil && t.Permanent.Attackable() && + t.Permanent.GetController() != controller { + + permanents = append(permanents, t.Permanent) + } + } + return permanents +} + func (u *Unit) IsAvailableTile(tile *Tile) bool { - return tile.IsFree() + return tile.IsAvailableForCard(u.Card) } func (u *Unit) Fight(p Permanent) { @@ -3,6 +3,7 @@ module muhq.space/muhqs-game/go go 1.19 require ( + github.com/RyanCarrier/dijkstra v1.1.0 github.com/hajimehoshi/ebiten/v2 v2.4.15 golang.org/x/exp v0.0.0-20230116083435-1de6713980de golang.org/x/image v0.3.0 @@ -1,4 +1,10 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/RyanCarrier/dijkstra v1.1.0 h1:/NDihjfJA3CxFaZz8EdzTwdFKFZDvvB881OVLdakRcI= +github.com/RyanCarrier/dijkstra v1.1.0/go.mod h1:5agGUBNEtUAGIANmbw09fuO3a2htPEkc1jNH01qxCWA= +github.com/RyanCarrier/dijkstra-1 v0.0.0-20170512020943-0e5801a26345 h1:fgSpoKViTSqRb4hjDNj10ig5wUvO0CayCzFdLf6fuRM= +github.com/RyanCarrier/dijkstra-1 v0.0.0-20170512020943-0e5801a26345/go.mod h1:OK4EvWJ441LQqGzed5NGB6vKBAE34n3z7iayPcEwr30= +github.com/albertorestifo/dijkstra v0.0.0-20160910063646-aba76f725f72 h1:uGeGZl8PxSq8VZGG4QK5njJTFA4/G/x5CYORvQVXtAE= +github.com/albertorestifo/dijkstra v0.0.0-20160910063646-aba76f725f72/go.mod h1:o+JdB7VetTHjLhU0N57x18B9voDBQe0paApdEAEoEfw= github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg= github.com/ebitengine/purego v0.1.1 h1:HI8nW+LniW9Yb34k34jBs8nz+PNzsw68o7JF8jWFHHE= github.com/ebitengine/purego v0.1.1/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg= @@ -21,6 +27,8 @@ github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jfreymuth/oggvorbis v1.0.4/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= +github.com/mattomatic/dijkstra v0.0.0-20130617153013-6f6d134eb237 h1:acuCHBjzG7MFTugvx3buC4m5rLDLaKC9J8C9jtlraRc= +github.com/mattomatic/dijkstra v0.0.0-20130617153013-6f6d134eb237/go.mod h1:UOnLAUmVG5paym8pD3C4B9BQylUDC2vXFJJpT7JrlEA= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/go/ui/hand.go b/go/ui/hand.go index 0fdde253..b07d177b 100644 --- a/go/ui/hand.go +++ b/go/ui/hand.go @@ -8,47 +8,47 @@ import ( "muhq.space/muhqs-game/go/game" ) -const ( - HAND_CARD_WIDTH = 200 -) - type HandCard struct { C *game.Card } type HandView struct { WidgetBase - hand *game.Hand - scale float64 + hand *game.Hand + cardWidth float64 + scale float64 } func NewHandView(x, y, width, height int, h *game.Hand) *HandView { - hv := &HandView{NewWidgetBase(x, y, width, height), h, -1} + hv := &HandView{NewWidgetBase(x, y, width, height), h, 0, 0} hv.renderImpl = func() *ebiten.Image { return hv.render() } return hv } func (hv *HandView) render() *ebiten.Image { img := ebiten.NewImage(hv.Width, hv.Height) - for i, card := range hv.hand.GetCards() { + handCards := hv.hand.GetCards() + nHandCards := len(handCards) + for i, card := range handCards { cardImg := assets.GetCard(card.Path(), "en") if cardImg == nil { log.Fatalf("No image found for %s", card.Path()) } - if hv.scale == -1 { + if hv.scale == 0 { b := cardImg.Bounds() - x_scale := float64(HAND_CARD_WIDTH) / float64(b.Dx()) + x_scale := (float64(hv.Width) / float64(nHandCards)) / float64(b.Dx()) y_scale := float64(hv.Height) / float64(b.Dy()) hv.scale = x_scale if y_scale < x_scale { hv.scale = y_scale } + hv.cardWidth = float64(b.Dx()) * hv.scale } op := &ebiten.DrawImageOptions{} op.GeoM.Scale(hv.scale, hv.scale) - op.GeoM.Translate(float64(i*HAND_CARD_WIDTH), 0) + op.GeoM.Translate(float64(i)*hv.cardWidth, 0) img.DrawImage(cardImg, op) } return img @@ -59,7 +59,7 @@ func (hv *HandView) FindObjectAt(x, y int) interface{} { return nil } - i := x / HAND_CARD_WIDTH + i := int(float64(x) / hv.cardWidth) if i >= hv.hand.Size() { return nil } diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go index e175c0c5..d7d86ea9 100644 --- a/go/ui/stateBar.go +++ b/go/ui/stateBar.go @@ -7,16 +7,21 @@ import ( "muhq.space/muhqs-game/go/game" ) +const ( + PHASE_VIEW_WIDTH = 250 +) + type StateBar struct { X, Y int Width, Height int gameState *game.State activePlayer *game.Player ressourceView *TextBox + phaseView *TextBox } func NewStateBar(x, y int, width, height int, s *game.State, p *game.Player) *StateBar { - sb := &StateBar{x, y, width, height, s, p, nil} + sb := &StateBar{x, y, width, height, s, p, nil, nil} return sb } @@ -24,12 +29,23 @@ func (sb *StateBar) ressourceLabel() string { return fmt.Sprintf("%d", sb.activePlayer.Ressource) } +func (sb *StateBar) phaseLabel() string { + return fmt.Sprintf("%s: %s", sb.gameState.Players[sb.gameState.ActivePlayerId].Name, + sb.gameState.ActivePhase.String()) +} + func (sb *StateBar) Draw(screen *ebiten.Image) { if sb.ressourceView == nil { - sb.ressourceView = NewFixedTextBox(sb.X, sb.Y+sb.Height-sb.Width, sb.Width, sb.Width, + sb.ressourceView = NewFixedTextBox(sb.X, sb.Y, sb.Height, sb.Height, sb.ressourceLabel()) } sb.ressourceView.Draw(screen) + + if sb.phaseView == nil { + sb.phaseView = NewFixedTextBox(sb.X+sb.ressourceView.Width, sb.Y, + PHASE_VIEW_WIDTH, sb.Height, sb.phaseLabel()) + } + sb.phaseView.Draw(screen) } func (sb *StateBar) FindObjectAt(x, y int) interface{} { @@ -39,4 +55,7 @@ func (sb *StateBar) FindObjectAt(x, y int) interface{} { func (sb *StateBar) ForceRedraw() { sb.ressourceView.Text = sb.ressourceLabel() sb.ressourceView = nil + + sb.phaseView.Text = sb.phaseLabel() + sb.phaseView = nil } diff --git a/go/ui/textBox.go b/go/ui/textBox.go index 5e2d140b..0cd09294 100644 --- a/go/ui/textBox.go +++ b/go/ui/textBox.go @@ -33,8 +33,8 @@ func NewAutoTextBox(x, y int, t string) *TextBox { } func NewUnitInfo(x, y int, u *game.Unit) *TextBox { - info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nMovement: %d\nAttack %s\nUpkeep: %d", - u.String(), u.GetDamage(), u.Health, u.Movement, u.Attack.String(), u.Upkeep) + info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nMovement: %s\nAttack %s\nUpkeep: %d", + u.String(), u.GetDamage(), u.Health, u.Movement.String(), u.Attack.String(), u.Upkeep) return NewAutoTextBox(x, y, info) } diff --git a/go/webtools/main.go b/go/webtools/main.go index 4d03c770..7cb0e318 100644 --- a/go/webtools/main.go +++ b/go/webtools/main.go @@ -1,11 +1,14 @@ package main import ( + "math/rand" "syscall/js" "muhq.space/muhqs-game/go/game" ) +var r rand.Rand + func jSRandomTileFromMap() js.Func { jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) any { if len(args) != 2 { @@ -34,10 +37,11 @@ func randomTileFromMap(mapData string, symbols []string) (game.Position, error) if err != nil { return game.INVALID_POSITION(), err } - return m.RandomTileFromSymbols(symbols) + return m.RandomTileFromSymbols(r, symbols) } func main() { + r = rand.New(rand.NewSource(time.Now().UnixNano())) js.Global().Set("randomTileFromSymbol", jSRandomTileFromMap()) <-make(chan bool) } |
