aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-25 21:10:44 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:44 +0100
commit8720ebf9c74d9e11001772b5c00c293fa3a8b9e5 (patch)
tree70013771c425746318d290b5cadad0bbc1a48abb
parent73d40903aecad3efcf7c6055c133f0db86558203 (diff)
downloadmuhqs-game-8720ebf9c74d9e11001772b5c00c293fa3a8b9e5.tar.gz
muhqs-game-8720ebf9c74d9e11001772b5c00c293fa3a8b9e5.zip
intermediate commit
-rw-r--r--go/client/main.go30
-rw-r--r--go/game/action.go69
-rw-r--r--go/game/ai.go291
-rw-r--r--go/game/ai_test.go66
-rw-r--r--go/game/attack.go50
-rw-r--r--go/game/card.go74
-rw-r--r--go/game/deck.go14
-rw-r--r--go/game/kraken.go150
-rw-r--r--go/game/map.go27
-rw-r--r--go/game/map_test.go35
-rw-r--r--go/game/movement.go47
-rw-r--r--go/game/permanent.go9
-rw-r--r--go/game/phase.go28
-rw-r--r--go/game/player.go48
-rw-r--r--go/game/playerControl.go8
-rw-r--r--go/game/range.go12
-rw-r--r--go/game/range_test.go8
-rw-r--r--go/game/state.go101
-rw-r--r--go/game/tile.go24
-rw-r--r--go/game/unit.go84
-rw-r--r--go/go.mod1
-rw-r--r--go/go.sum8
-rw-r--r--go/ui/hand.go24
-rw-r--r--go/ui/stateBar.go23
-rw-r--r--go/ui/textBox.go4
-rw-r--r--go/webtools/main.go6
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) {
diff --git a/go/go.mod b/go/go.mod
index 6f97bdaf..24b6cb52 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -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
diff --git a/go/go.sum b/go/go.sum
index 1a81401d..f505893f 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -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)
}