aboutsummaryrefslogtreecommitdiff
path: root/go/game
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-26 22:16:10 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:44 +0100
commit926ca5cdd20a39e3e05aaa3486cb166718dab5ca (patch)
tree3d7bc8a0e1e0be1ce8d92d5503bdede878e718e9 /go/game
parent8720ebf9c74d9e11001772b5c00c293fa3a8b9e5 (diff)
downloadmuhqs-game-926ca5cdd20a39e3e05aaa3486cb166718dab5ca.tar.gz
muhqs-game-926ca5cdd20a39e3e05aaa3486cb166718dab5ca.zip
intermeditae commit
Diffstat (limited to 'go/game')
-rw-r--r--go/game/action.go110
-rw-r--r--go/game/card.go3
-rw-r--r--go/game/deck.go14
-rw-r--r--go/game/kraken.go86
-rw-r--r--go/game/map.go57
-rw-r--r--go/game/map_test.go4
-rw-r--r--go/game/movement.go11
-rw-r--r--go/game/phases.go (renamed from go/game/phase.go)14
-rw-r--r--go/game/pileOfCards.go4
-rw-r--r--go/game/player.go45
-rw-r--r--go/game/playerControl.go33
-rw-r--r--go/game/stack.go14
-rw-r--r--go/game/state.go258
-rw-r--r--go/game/tile.go12
-rw-r--r--go/game/unit.go33
15 files changed, 532 insertions, 166 deletions
diff --git a/go/game/action.go b/go/game/action.go
index c121d0c8..f3b242b5 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -8,7 +8,6 @@ import (
type ActionPrototype interface {
Specialize(...interface{}) Action
String() string
- CanPayCosts(s *State) bool
}
type Action interface {
@@ -19,19 +18,15 @@ 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} }
-type FastAction interface {
- Action
-}
-
type ActionBase struct {
Source interface{}
Card *Card
+ Targets interface{}
resolveFunc func(s *State)
costFunc func(s *State) bool
desc string
@@ -59,16 +54,16 @@ func (a *ActionBase) String() string {
type PlayAction struct {
ActionBase
- targets interface{}
}
func NewPlayAction(p *Player, c *Card, targets interface{}) Action {
costFunc := func(*State) bool {
cost := c.GetPlayCosts()
- if p.Ressource < cost {
+
+ if p.Resource < cost {
return false
}
- p.Ressource -= cost
+ p.Resource -= cost
return true
}
return NewPlayActionWithSpecialCosts(p, c, targets, costFunc)
@@ -83,8 +78,12 @@ func NewPlayActionWithSpecialCosts(p *Player, c *Card, targets interface{},
s.ResolvePlay(p, c, targets)
}
- desc := fmt.Sprintf("p %s@%v", c.Name, targets)
- return &PlayAction{ActionBase{p, c, resolveFunc, costFunc, desc}, targets}
+ desc := fmt.Sprintf("play %s@%v", c.Name, targets)
+ return &PlayAction{ActionBase{p, c, targets, resolveFunc, costFunc, desc}}
+}
+
+type MoveAction struct {
+ ActionBase
}
type MoveActionPrototype struct {
@@ -95,10 +94,6 @@ 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 %s", proto.Unit.Movement.String())
}
@@ -112,7 +107,6 @@ func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action {
}
costFunc := func(*State) bool {
- log.Printf("Move Actions of %s: %d\n", u.GetCard().Name, u.AvailMoveActions)
if u.AvailMoveActions < 1 {
return false
}
@@ -121,15 +115,15 @@ func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action {
}
desc := fmt.Sprintf("-> %s", target.Position.String())
- return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc}
+ return &MoveAction{ActionBase{u, u.GetCard(), target, resolveFunc, costFunc, desc}}
}
-type AttackActionPrototype struct {
- Unit *Unit
+type AttackAction struct {
+ ActionBase
}
-func (proto *AttackActionPrototype) CanPayCosts(s *State) bool {
- return proto.Unit.AvailAttackActions > 0
+type AttackActionPrototype struct {
+ Unit *Unit
}
func NewAttackActionPrototype(u *Unit) *AttackActionPrototype {
@@ -157,7 +151,7 @@ func (proto *AttackActionPrototype) Specialize(args ...interface{}) Action {
}
desc := fmt.Sprintf("x %s", target.GetTile().Position.String())
- return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc}
+ return &AttackAction{ActionBase{u, u.GetCard(), target, resolveFunc, costFunc, desc}}
}
type FullActionPrototype struct {
@@ -168,10 +162,6 @@ 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"
}
@@ -188,7 +178,7 @@ func (proto *FullActionPrototype) Specialize(args ...interface{}) Action {
return true
}
desc := "full_action"
- return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc}
+ return &ActionBase{u, u.GetCard(), nil, resolveFunc, costFunc, desc}
}
type FreeActionPrototype struct {
@@ -196,10 +186,6 @@ type FreeActionPrototype struct {
canPayFunc func(s *State) bool
}
-func (proto *FreeActionPrototype) CanPayCosts(s *State) bool {
- return proto.canPayFunc(s)
-}
-
func (proto *FreeActionPrototype) String() string {
return "free_action"
}
@@ -209,9 +195,69 @@ func (proto *FreeActionPrototype) Specialize(args ...interface{}) Action {
resolveFunc := args[0].(func(s *State))
costFunc := args[1].(func(s *State) bool)
desc := "free_action"
- return &FreeAction{ActionBase{p, p.GetCard(), resolveFunc, costFunc, desc}}
+ return &FreeAction{ActionBase{p, p.GetCard(), nil, resolveFunc, costFunc, desc}}
}
type FreeAction struct {
ActionBase
}
+
+type BuyAction struct {
+ player *Player
+ card *Card
+}
+
+func (a *BuyAction) Resolve(s *State) {
+ log.Println("Move", a.card.Name, "into", a.player.Name, "discard pile")
+ a.player.Store.MoveCard(a.card, a.player.DiscardPile)
+}
+
+func (a *BuyAction) PayCosts(*State) bool {
+ if a.card.BuyCost < 0 || a.card.BuyCost > a.player.Resource {
+ return false
+ }
+
+ a.player.Resource -= a.card.BuyCost
+ return true
+}
+
+func (a *BuyAction) String() string { return fmt.Sprintf("%s buy %s", a.player.Name, a.card.Name) }
+
+func NewBuyAction(p *Player, c *Card) *BuyAction {
+ return &BuyAction{p, c}
+}
+
+type UpkeepAction struct {
+ player *Player
+ unit *Unit
+ pay bool
+}
+
+func (a *UpkeepAction) Resolve(s *State) {
+ if a.pay {
+ a.player.Resource -= a.unit.Upkeep
+ } else {
+ s.DestroyPermanent(a.unit)
+ }
+}
+
+func (a *UpkeepAction) PayCosts(*State) bool {
+ if a.unit.Upkeep > a.player.Resource {
+ return false
+ }
+
+ a.player.Resource -= a.unit.Upkeep
+ return true
+}
+
+func (a *UpkeepAction) String() string {
+ if a.pay {
+ return fmt.Sprintf("%s keep %s", a.player.Name, a.unit.Tile.Position.String())
+ }
+
+ return fmt.Sprintf("%s disband %s", a.player.Name, a.unit.Tile.Position.String())
+}
+
+func NewUpkeepAction(p *Player, u *Unit, pay bool) *UpkeepAction {
+ return &UpkeepAction{p, u, pay}
+}
diff --git a/go/game/card.go b/go/game/card.go
index 6f8a74a8..07ef033b 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -223,7 +223,8 @@ func NewCard(cardPath string) *Card {
}
func (c *Card) Path() string {
- return path.Join(c.Set.String(), strings.ToLower(c.Name))
+ fileName := strings.ReplaceAll(strings.ToLower(c.Name), " ", "_")
+ return path.Join(c.Set.String(), fileName)
}
func (c *Card) IsPermanent() bool {
diff --git a/go/game/deck.go b/go/game/deck.go
index f1290fa4..569a952d 100644
--- a/go/game/deck.go
+++ b/go/game/deck.go
@@ -34,6 +34,20 @@ func (d *Deck) Draw(amount int) []*Card {
return drawn
}
+func (d *Deck) DrawOne() *Card {
+ drawn := d.Draw(1)
+ if len(drawn) == 0 {
+ return nil
+ }
+ return drawn[0]
+}
+
+func NewDeckFrom(cards PileOfCards) *Deck {
+ d := NewDeck()
+ cards.MoveInto(d)
+ return d
+}
+
func NewDeckFromCardPaths(cardPaths []string) *Deck {
d := NewDeck()
for _, cardPath := range cardPaths {
diff --git a/go/game/kraken.go b/go/game/kraken.go
index b0108361..ef63bbd0 100644
--- a/go/game/kraken.go
+++ b/go/game/kraken.go
@@ -7,43 +7,42 @@ import (
"sync"
)
-// 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`
+const (
+ KRAKEN_NAME = "Kraken"
+)
+
+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`
func NewKraken(id int, s *State, c color.Color) *Player {
+ kraken := NewPlayer(id, KRAKEN_NAME, NewDeck(), s, c)
d := NewDeckFromDeckList(KrakenDecklist)
- kraken := NewPlayer(id, "The Kraken", d, NewStore(), s, c)
d.Shuffle(s.Rand)
+ kraken.Deck = d
kraken.DrawPerTurn = 0
- the_kraken := NewUnitFromPath("kraken/the_kraken", s.Map.TileAt(Position{3, 1}), kraken)
- s.AddPermanent(the_kraken)
+ kraken_tile := s.Map.FilterTiles(func(t *Tile) bool { return t.Raw == "the kraken" })
+ if len(kraken_tile) != 1 {
+ log.Fatal("No kraken tile found in Map")
+ }
- ctrl := &KrakenControl{kraken, make(chan Action), new(sync.WaitGroup)}
- ctrl.syncGameState.Add(1)
+ s.AddPermanent(NewUnitFromPath("kraken/the_kraken", kraken_tile[0], kraken))
- kraken.Ctrl = ctrl
- go func() {
- for len(s.Map.WinCondition(s)) == 0 {
- ctrl.KrakenTurn()
- }
- }()
+ addKrakenControl(kraken)
return kraken
}
@@ -54,12 +53,25 @@ type KrakenControl struct {
syncGameState *sync.WaitGroup
}
+func addKrakenControl(kraken *Player) {
+ ctrl := &KrakenControl{kraken, make(chan Action), new(sync.WaitGroup)}
+ kraken.Ctrl = ctrl
+
+ ctrl.syncGameState.Add(1)
+ go func() {
+ s := kraken.gameState
+ for len(s.Map.WinCondition(s)) == 0 {
+ ctrl.krakenTurn()
+ }
+ }()
+}
+
func (ctrl *KrakenControl) awaitGameStateSync() {
ctrl.syncGameState.Wait()
ctrl.syncGameState.Add(1)
}
-func (ctrl *KrakenControl) KrakenTurn() {
+func (ctrl *KrakenControl) krakenTurn() {
s := ctrl.kraken.gameState
kraken := ctrl.kraken
@@ -78,15 +90,15 @@ func (ctrl *KrakenControl) KrakenTurn() {
continue
}
- if c.BuyCost > kraken.Ressource {
- log.Println("Kraken discards", c.Name, "because ressource to low", kraken.Ressource)
+ if c.BuyCost > kraken.Resource {
+ log.Println("Kraken discards", c.Name, "because resource to low", kraken.Resource)
kraken.DiscardPile.AddCard(c)
break
}
kraken.Hand.AddCard(c)
costFunc := func(s *State) bool {
- kraken.Ressource -= c.BuyCost
+ kraken.Resource -= c.BuyCost
return true
}
@@ -130,8 +142,8 @@ func (ctrl *KrakenControl) KrakenTurn() {
func (ctrl *KrakenControl) RecvAction() Action {
s := ctrl.kraken.gameState
kraken := ctrl.kraken
- if s.ActivePlayerId != kraken.Id ||
- s.ActivePhase != actionPhase || !s.Stack.IsEmpty() {
+ if !s.IsActivePlayer(kraken) ||
+ s.ActivePhase != Phases.ActionPhase || !s.Stack.IsEmpty() {
return NewPassPriorityAction(ctrl.kraken)
}
@@ -143,9 +155,13 @@ func (ctrl *KrakenControl) RecvAction() Action {
func (ctrl *KrakenControl) SendNotification(n PlayerNotification) {
}
+func (p *Player) IsKraken() bool {
+ return p.Name == KRAKEN_NAME
+}
+
func (s *State) FindKraken() *Player {
for _, p := range s.Players {
- if p.Name == "The Kraken" {
+ if p.IsKraken() {
return p
}
}
diff --git a/go/game/map.go b/go/game/map.go
index 72e17ad4..cfabfaf9 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -19,8 +19,8 @@ const (
BASE_URL string = "localhost:8000/"
MAP_URL_PART string = "html/build/maps"
- DEFAULT_RESSOURCE_GAIN int = 5
- DEFAULT_START_DECK string = "3 misc/farmer"
+ DEFAULT_RESOURCE_GAIN int = 5
+ DEFAULT_START_DECK string = "3 misc/farmer"
)
type MapYml struct {
@@ -36,9 +36,10 @@ func DummyWinCondition(*State) []*Player {
type Map struct {
Tiles [][]Tile
symbols map[string]string
- RessourceGain int
+ ResourceGain int
StartDeckList string
WinCondition func(*State) []*Player
+ Stores map[Position]*Store
}
func readMap(r io.Reader) (*Map, error) {
@@ -59,7 +60,14 @@ func ParseMap(data []byte) (*Map, error) {
mapYml.Symbols[" "] = "neutral"
- m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK, DummyWinCondition}
+ m := &Map{
+ Tiles: [][]Tile{},
+ symbols: mapYml.Symbols,
+ ResourceGain: DEFAULT_RESOURCE_GAIN,
+ StartDeckList: DEFAULT_START_DECK,
+ WinCondition: DummyWinCondition,
+ Stores: make(map[Position]*Store),
+ }
if mapYml.StartDeckList != "" {
m.StartDeckList = mapYml.StartDeckList
@@ -70,11 +78,17 @@ func ParseMap(data []byte) (*Map, error) {
row := rows[y]
m.Tiles = append(m.Tiles, []Tile{})
for x := 0; x < len(rows[y]); x++ {
+ pos := Position{x, y}
s := string(row[x])
- tile, err := NewTileFromString(mapYml.Symbols[s], Position{x, y})
+ tile, err := NewTileFromString(mapYml.Symbols[s], pos)
if err != nil {
return nil, err
}
+
+ if tile.Type == TileTypes.Store {
+ m.Stores[pos] = NewStore()
+ }
+
m.Tiles[y] = append(m.Tiles[y], tile)
}
}
@@ -254,3 +268,36 @@ func (m *Map) FilterTiles(filter func(t *Tile) bool) []*Tile {
func (m *Map) AllTiles() []*Tile {
return m.FilterTiles(func(*Tile) bool { return true })
}
+
+func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) {
+ nStores := len(m.Stores)
+ stores := make([]*Store, 0, nStores)
+ for _, store := range m.Stores {
+ stores = append(stores, store)
+ }
+
+ d := NewDeckFrom(cards)
+ d.Shuffle(rand)
+ nCards := d.Size()
+
+ for i := 0; i < nCards; i++ {
+ c := d.DrawOne()
+ stores[i%nStores].AddCard(c)
+ }
+}
+
+func (m *Map) redistributeStoreCards(cards PileOfCards, rand *rand.Rand) {
+ poc := NewPileOfCards()
+ for _, store := range m.Stores {
+ store.MoveInto(poc)
+ }
+ m.distributeStoreCards(poc, rand)
+}
+
+func (m *Map) HasStores() bool {
+ return len(m.Stores) > 0
+}
+
+func (m *Map) GetStoreOn(p Position) *Store {
+ return m.Stores[p]
+}
diff --git a/go/game/map_test.go b/go/game/map_test.go
index 96efc459..6972f867 100644
--- a/go/game/map_test.go
+++ b/go/game/map_test.go
@@ -68,8 +68,8 @@ kings:
}
}
- if m.RessourceGain != DEFAULT_RESSOURCE_GAIN {
- t.Fatalf("ressource gain is not the default one")
+ if m.ResourceGain != DEFAULT_RESOURCE_GAIN {
+ t.Fatalf("resource gain is not the default one")
}
}
diff --git a/go/game/movement.go b/go/game/movement.go
index df7e4138..1a3f03c4 100644
--- a/go/game/movement.go
+++ b/go/game/movement.go
@@ -17,11 +17,16 @@ func INVALID_MOVEMENT() Movement {
}
func parseMovement(desc interface{}) Movement {
- if r, ok := desc.(int); ok {
- return Movement{r, false}
+ var movementStr string
+ switch typedDesc := desc.(type) {
+ case int:
+ return Movement{typedDesc, false}
+ case string:
+ movementStr = typedDesc
+ case map[string]interface{}:
+ movementStr = typedDesc["en"].(string)
}
- movementStr := desc.(string)
tokens := strings.Split(movementStr, " ")
r, err := strconv.Atoi(tokens[0])
if err != nil {
diff --git a/go/game/phase.go b/go/game/phases.go
index efcd430a..ea239278 100644
--- a/go/game/phase.go
+++ b/go/game/phases.go
@@ -26,3 +26,17 @@ func (p PhaseType) String() string {
return ""
}
+
+var Phases = struct {
+ DrawPhase PhaseType
+ UpkeepPhase PhaseType
+ ActionPhase PhaseType
+ BuyPhase PhaseType
+ DiscardPhase PhaseType
+}{
+ DrawPhase: drawPhase,
+ UpkeepPhase: upkeepPhase,
+ ActionPhase: actionPhase,
+ BuyPhase: buyPhase,
+ DiscardPhase: discardPhase,
+}
diff --git a/go/game/pileOfCards.go b/go/game/pileOfCards.go
index 807235a5..457f804a 100644
--- a/go/game/pileOfCards.go
+++ b/go/game/pileOfCards.go
@@ -15,6 +15,10 @@ type PileOfCardsBase struct {
cards []*Card
}
+func NewPileOfCards() *PileOfCardsBase {
+ return &PileOfCardsBase{[]*Card{}}
+}
+
func (poc *PileOfCardsBase) Size() int {
return len(poc.cards)
}
diff --git a/go/game/player.go b/go/game/player.go
index 017e8407..608fc7e9 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -2,7 +2,6 @@ package game
import (
"image/color"
- "log"
)
const (
@@ -13,7 +12,7 @@ type Player struct {
Id int
Name string
Turn int
- Ressource int
+ Resource int
DrawPerTurn int
Hand *Hand
DiscardPile *DiscardPile
@@ -24,29 +23,39 @@ type Player struct {
Ctrl PlayerControl
}
-func NewPlayer(id int, name string,
- deck *Deck, store *Store,
+func NewPlayer(id int, name string, deck *Deck,
gameState *State, color color.Color,
) *Player {
+ store := NewStore()
+ deck.MoveInto(store)
+
p := Player{
- id, name,
- 0, 0, MAX_DRAW,
- NewHand(), NewDiscardPile(), deck, store,
- gameState, color, nil,
+ Id: id,
+ Name: name,
+ Turn: 0,
+ Resource: 0,
+ DrawPerTurn: MAX_DRAW,
+ Hand: NewHand(),
+ DiscardPile: NewDiscardPile(),
+ Deck: NewDeckFromDeckList(gameState.Map.StartDeckList),
+ Store: store,
+ gameState: gameState,
+ Color: color,
+ Ctrl: nil,
}
return &p
}
func (p *Player) resourceGain() int {
if p.Name == "The kraken" {
- return p.gameState.Map.RessourceGain * (len(p.gameState.Players) - 1)
+ return p.gameState.Map.ResourceGain * (len(p.gameState.Players) - 1)
} else {
- return p.gameState.Map.RessourceGain
+ return p.gameState.Map.ResourceGain
}
}
func (p *Player) Upkeep() {
- p.Ressource += p.resourceGain()
+ p.Resource += p.resourceGain()
// TODO: handle upkeep triggers
@@ -56,7 +65,8 @@ func (p *Player) Upkeep() {
}
// TODO: let players decide if they want to pay the upkeep
- p.Ressource -= unit.Upkeep
+ p.Resource -= unit.Upkeep
+
// TODO: better refresh unit actions
unit.AvailAttackActions = 1
unit.AvailMoveActions = 1
@@ -65,8 +75,7 @@ func (p *Player) Upkeep() {
func (p *Player) ActionPhase() {
for {
- a := PromptAction(p.Ctrl)
- log.Printf("Received %T action from %s\n", a, p.Name)
+ a := promptAction(p.Ctrl)
if _, ok := a.(*PassPriorityAction); ok {
return
}
@@ -76,11 +85,17 @@ func (p *Player) ActionPhase() {
}
func (p *Player) buy(c *Card) {
- p.Ressource -= c.BuyCost
+ p.Resource -= c.BuyCost
p.DiscardPile.AddCard(c)
}
func (p *Player) BuyPhase() {
+ a := promptBuy(p.Ctrl)
+ if _, ok := a.(*PassPriorityAction); ok {
+ return
+ }
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
// TODO: select card to buy
}
diff --git a/go/game/playerControl.go b/go/game/playerControl.go
index 6d5e8e14..5e1552f0 100644
--- a/go/game/playerControl.go
+++ b/go/game/playerControl.go
@@ -6,6 +6,8 @@ const (
DeclaredActionNotification = iota
ResolvedActionNotification
PriorityNotification
+ UpkeepPrompt
+ BuyPrompt
)
func (n PlayerNotification) IsPriorityNotification() bool {
@@ -14,19 +16,28 @@ func (n PlayerNotification) IsPriorityNotification() bool {
type PlayerNotification struct {
Notification PlayerNotificationType
- Action Action
+ Context interface{}
+ Error error
}
func NewPriorityNotification() PlayerNotification {
- return PlayerNotification{PriorityNotification, nil}
+ return PlayerNotification{PriorityNotification, nil, nil}
}
-func NewDeclaredActionNotification(a Action) PlayerNotification {
- return PlayerNotification{DeclaredActionNotification, a}
+func NewDeclaredActionNotification(a Action, err error) PlayerNotification {
+ return PlayerNotification{DeclaredActionNotification, a, err}
}
-func NewResolvedActionNotification(a Action) PlayerNotification {
- return PlayerNotification{ResolvedActionNotification, a}
+func NewResolvedActionNotification(a Action, err error) PlayerNotification {
+ return PlayerNotification{ResolvedActionNotification, a, err}
+}
+
+func NewUpkeepPrompt(u *Unit) PlayerNotification {
+ return PlayerNotification{ResolvedActionNotification, u, nil}
+}
+
+func NewBuyPrompt() PlayerNotification {
+ return PlayerNotification{BuyPrompt, nil, nil}
}
type PlayerControl interface {
@@ -62,7 +73,15 @@ func NewChanPlayerControl() *ChanPlayerControl {
return &ChanPlayerControl{a, n}
}
-func PromptAction(ctrl PlayerControl) Action {
+func prompt(ctrl PlayerControl, notification PlayerNotification) Action {
ctrl.SendNotification(NewPriorityNotification())
return ctrl.RecvAction()
}
+
+func promptBuy(ctrl PlayerControl) Action {
+ return prompt(ctrl, NewBuyPrompt())
+}
+
+func promptAction(ctrl PlayerControl) Action {
+ return prompt(ctrl, NewPriorityNotification())
+}
diff --git a/go/game/stack.go b/go/game/stack.go
index 45ba4072..9132ae13 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -18,7 +18,6 @@ func (s *Stack) IsEmpty() bool {
}
func (s *Stack) Push(a Action) {
- s.gameState.broadcastNotification(NewDeclaredActionNotification(a))
s.Actions = append(s.Actions, a)
}
@@ -27,16 +26,21 @@ func (s *Stack) Pop() {
if l == 0 {
log.Fatalf("Can not pop from empty stack")
}
+
a := s.Actions[l-1]
s.Actions = s.Actions[:l-1]
- a.Resolve(s.gameState)
- s.gameState.broadcastNotification(NewResolvedActionNotification(a))
+
+ err := s.gameState.ValidateAction(a)
+ if err == nil {
+ a.Resolve(s.gameState)
+ }
+ log.Println("Resolved", a, err)
+ s.gameState.broadcastNotification(NewResolvedActionNotification(a, err))
}
func (s *Stack) Resolve() {
for !s.IsEmpty() {
- log.Println("Resolve Stack")
- s.gameState.AllPassing()
+ s.gameState.AllPassing(false)
s.Pop()
}
}
diff --git a/go/game/state.go b/go/game/state.go
index 29267c28..97283be4 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -1,28 +1,31 @@
package game
import (
+ "fmt"
"image/color"
"log"
"math/rand"
"strconv"
"strings"
"time"
+
+ "golang.org/x/exp/slices"
)
type State struct {
- Decks []*Deck
- Stores []*Store
- Map *Map
- Stack *Stack
- Hands []*Hand
- DiscardPiles []*DiscardPile
- Players []*Player
- ActivePlayerId int
- ActivePhase PhaseType
- Permanents []Permanent
- Units []*Unit
- Artifacts []*Artifact
- Rand *rand.Rand
+ Decks []*Deck
+ Stores []*Store
+ Map *Map
+ Stack *Stack
+ Hands []*Hand
+ DiscardPiles []*DiscardPile
+ Players []*Player
+ ActivePlayer *Player
+ ActivePhase PhaseType
+ Permanents []Permanent
+ Units []*Unit
+ Artifacts []*Artifact
+ Rand *rand.Rand
}
func NewState() *State {
@@ -32,16 +35,33 @@ func NewState() *State {
return s
}
-func (s *State) AddNewPlayer(name string, deck *Deck, store *Store, color color.Color) {
- p := NewPlayer(len(s.Players)+1, name, deck, store, s, color)
+func (s *State) AddNewPlayer(name string, deck *Deck, color color.Color) {
+ p := NewPlayer(len(s.Players)+1, name, deck, s, color)
s.Players = append(s.Players, p)
}
+func (s *State) IsActivePlayer(p *Player) bool {
+ return s.ActivePlayer == p
+}
+
func (s *State) Loop() []*Player {
winners := []*Player{}
+
+ if s.Map.HasStores() {
+ poc := NewPileOfCards()
+ for _, p := range s.Players {
+ if p.IsKraken() {
+ continue
+ }
+
+ p.Store.MoveInto(poc)
+ }
+ s.Map.distributeStoreCards(poc, s.Rand)
+ }
+
for len(winners) == 0 {
- for i, p := range s.Players {
- s.ActivePlayerId = i
+ for _, p := range s.Players {
+ s.ActivePlayer = p
p.Turn++
if p.Deck.IsEmpty() {
@@ -50,22 +70,22 @@ func (s *State) Loop() []*Player {
p.Deck.Shuffle(s.Rand)
}
- s.ActivePhase = drawPhase
+ s.ActivePhase = Phases.DrawPhase
p.Draw()
- s.ActivePhase = upkeepPhase
+ s.ActivePhase = Phases.UpkeepPhase
p.Upkeep()
- s.PhaseChange(i)
+ s.PhaseChange()
- s.ActivePhase = actionPhase
+ s.ActivePhase = Phases.ActionPhase
p.ActionPhase()
- s.PhaseChange(i)
+ s.PhaseChange()
- s.ActivePhase = buyPhase
+ s.ActivePhase = Phases.BuyPhase
p.BuyPhase()
- s.PhaseChange(i)
+ s.PhaseChange()
- s.ActivePhase = discardPhase
+ s.ActivePhase = Phases.DiscardPhase
p.DiscardPhase()
winners = s.Map.WinCondition(s)
@@ -75,38 +95,152 @@ func (s *State) Loop() []*Player {
return winners
}
+func (s *State) IsValidPlay(a *PlayAction) error {
+ if s.ActivePhase != Phases.ActionPhase {
+ log.Fatalf("Play actions can only be declared during one's actions phase")
+ }
+
+ if a.Card.IsPermanent() {
+ pos := a.Targets.(Position)
+ spawn := s.Map.TileAt(pos)
+ if !slices.Contains(s.AvailableSpawnTiles(a.Source.(*Player), a.Card), spawn) {
+ return fmt.Errorf("Spawn %v is not a valid spawn tile for %s",
+ pos.String(), a.Card.Name)
+ }
+ }
+
+ return nil
+}
+
+func (s *State) IsValidMove(a *MoveAction) error {
+ if s.ActivePhase != Phases.ActionPhase {
+ return fmt.Errorf("Move actions can only be declared during one's actions phase")
+ }
+
+ u := a.Source.(*Unit)
+ tile := a.Targets.(*Tile)
+
+ if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
+ return fmt.Errorf("Unit %s at %s can not move to %s",
+ u.Card.Name, u.Tile.Position.String(), tile.Position.String())
+ }
+
+ if tile.Permanent != nil {
+ return fmt.Errorf("Unit %s at %s can not move to occupied %s",
+ u.Card.Name, u.Tile.Position.String(), tile.Position.String())
+ }
+
+ return nil
+}
+
+func (s *State) IsValidAttack(a *AttackAction) error {
+ if s.ActivePhase != Phases.ActionPhase {
+ return fmt.Errorf("Attack actions can only be declared during one's actions phase")
+ }
+
+ unit := a.Source.(Unit)
+ target := a.Targets.(Permanent)
+ if !target.Attackable() {
+ return fmt.Errorf("Target %s is not attackable")
+ }
+
+ if !IsPositionInRange(unit.Tile.Position, target.GetTile().Position, unit.Attack.Range) {
+ return fmt.Errorf("Move actions can only be declared during one's actions phase")
+ }
+
+ // TODO: check move
+
+ return nil
+}
+
+func (s *State) IsValidBuy(a *BuyAction) error {
+ if s.Map.HasStores() {
+ var storeTiles []*Tile
+ for pos, store := range s.Map.Stores {
+ if slices.Contains(store.cards, a.card) {
+ storeTiles = append(storeTiles, s.Map.TileAt(pos))
+ }
+ }
+
+ if storeTiles == nil {
+ return fmt.Errorf("no store contains %s", a.card.Name)
+ }
+
+ controlsUnitOnStore := false
+ for _, tile := range storeTiles {
+ controlsUnitOnStore = controlsUnitOnStore || (tile.Permanent != nil &&
+ tile.Permanent.GetCard().Type == CardTypes.Unit &&
+ tile.Permanent.GetController() == a.player)
+ }
+ if !controlsUnitOnStore {
+ return fmt.Errorf("%s' controls no unit on a store containing %s",
+ a.player.Name, a.card.Name)
+ }
+
+ } else if !slices.Contains(a.player.Store.cards, a.card) {
+ return fmt.Errorf("%s's store does not contain %s", a.player.Name, a.card.Name)
+ }
+
+ return nil
+}
+
+func (s *State) ValidateAction(a Action) error {
+ var err error = nil
+ switch a := a.(type) {
+ case *PlayAction:
+ err = s.IsValidPlay(a)
+ case *MoveAction:
+ err = s.IsValidMove(a)
+ case *AttackAction:
+ err = s.IsValidAttack(a)
+ case *BuyAction:
+ err = s.IsValidBuy(a)
+ }
+ return err
+}
+
func (s *State) PushAction(a Action) {
if !a.PayCosts(s) {
log.Fatalf("Cost for %s could not be paid", a)
}
+
s.Stack.Push(a)
}
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")
+ var err error = nil
+ switch a.(type) {
+ case *BuyAction:
+ if s.ActivePhase != Phases.BuyPhase {
+ err = fmt.Errorf("Cards can only be bought during one's buy phase")
+ }
+ case *FreeAction:
+ default:
+ if !s.Stack.IsEmpty() {
+ err = fmt.Errorf("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")
+ if err == nil {
+ s.PushAction(a)
}
- s.PushAction(a)
+ s.broadcastNotification(NewDeclaredActionNotification(a, err))
}
-func (s *State) DeclareFastAction(a Action) {
- s.PushAction(a)
-}
-
-func (s *State) AllPassing() bool {
+func (s *State) AllPassing(skipFirst bool) bool {
nPlayers := len(s.Players)
- for i := 0; i < nPlayers; i++ {
- p := s.Players[(i+s.ActivePlayerId)%nPlayers]
+ i := 0
+ if skipFirst {
+ i++
+ }
+ for ; i < nPlayers; i++ {
+ p := s.Players[(i+s.ActivePlayer.Id-1)%nPlayers]
p.Ctrl.SendNotification(NewPriorityNotification())
a := p.Ctrl.RecvAction()
switch a.(type) {
case *PassPriorityAction:
- log.Printf("%s passed %d/%d\n", p.Name, i, nPlayers)
+ log.Printf("%s passed %d/%d\n", p.Name, i+1, nPlayers)
default:
s.DeclareAction(a)
return false
@@ -115,15 +249,15 @@ func (s *State) AllPassing() bool {
return true
}
-func (s *State) PhaseChange(activePlayerId int) {
- log.Println("Phase change")
- for !s.AllPassing() {
+func (s *State) PhaseChange() {
+ skipFirst := true
+ for !s.AllPassing(skipFirst) {
s.Stack.Resolve()
+ skipFirst = false
}
}
func (s *State) broadcastNotification(n PlayerNotification) {
- log.Printf("broadcasting %v\n", n)
for _, p := range s.Players {
p.Ctrl.SendNotification(n)
}
@@ -236,16 +370,13 @@ func (s *State) ResolvePlay(p *Player, c *Card, targets interface{}) {
log.Printf("Resolving unhandled card type: %s\n", c.Type.String())
}
- if !c.IsPermanent() {
+ if c.IsPermanent() {
+ } else {
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
@@ -257,12 +388,43 @@ func (s *State) SpawnTiles(p *Player) []*Tile {
return spawns
}
+func (s *State) WaterSpawnTiles(p *Player) []*Tile {
+ if p.IsKraken() {
+ return s.KrakenSpawnTiles()
+ }
+
+ docks := s.Map.FilterTiles(func(t *Tile) bool {
+ if t.Type != TileTypes.Docks {
+ return false
+ }
+
+ return t.Raw == "docks" || strings.HasSuffix(t.Raw, strconv.Itoa(p.Id))
+ })
+
+ log.Println(docks)
+ spawns := []*Tile{}
+ for _, dock := range docks {
+ for _, candidate := range TilesInRangeFromOrigin(s.Map, dock.Position, 1) {
+ if candidate.Water {
+ spawns = append(spawns, candidate)
+ }
+ }
+ }
+
+ 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)
+ var candidates []*Tile
+ if c.hasMovementEffect("swimming") {
+ candidates = s.WaterSpawnTiles(p)
+ } else {
+ candidates = s.SpawnTiles(p)
+ }
tiles := []*Tile{}
for _, candidate := range candidates {
diff --git a/go/game/tile.go b/go/game/tile.go
index 55e00b8f..9adb1eae 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -13,6 +13,8 @@ const (
farm
street
spawn
+ docks
+ store
)
func (t TileType) String() string {
@@ -29,6 +31,10 @@ func (t TileType) String() string {
return "street"
case spawn:
return "spawn"
+ case docks:
+ return "docks"
+ case store:
+ return "store"
}
return ""
}
@@ -40,6 +46,8 @@ var TileTypes = struct {
Farm TileType
Street TileType
Spawn TileType
+ Docks TileType
+ Store TileType
}{
Neutral: neutral,
Tower: tower,
@@ -47,6 +55,8 @@ var TileTypes = struct {
Farm: farm,
Street: street,
Spawn: spawn,
+ Docks: docks,
+ Store: store,
}
var TileNames = map[string]TileType{
@@ -56,6 +66,8 @@ var TileNames = map[string]TileType{
"farm": farm,
"street": street,
"spawn": spawn,
+ "docks": docks,
+ "store": store,
}
type Tile struct {
diff --git a/go/game/unit.go b/go/game/unit.go
index 0d39e904..aa535086 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -2,8 +2,6 @@ package game
import (
"log"
-
- "golang.org/x/exp/slices"
)
type UnitState int
@@ -74,13 +72,6 @@ func NewUnitFromPath(cardPath string, tile *Tile, owner *Player) *Unit {
}
func (u *Unit) move(s *State, tile *Tile) {
- if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
- log.Panicf("Unit %s at %v can not move to %v", u.Card.Name, u.Tile.Position, tile.Position)
- }
- if tile.Permanent != nil {
- log.Panicf("Unit %s at %v can not move to occupied %v", u.Card.Name, u.Tile.Position, tile.Position)
- }
-
u.Tile.Permanent = nil
tile.Permanent = u
u.Tile = tile
@@ -88,11 +79,24 @@ func (u *Unit) move(s *State, tile *Tile) {
func (u *Unit) MoveRangeTiles(m *Map) []*Tile {
tiles := []*Tile{}
+ // TODO: build graph only containing possible tile in movement range
+ graph := m.generateMapGraphFor(u)
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)
+ if tile == nil || !u.IsAvailableTile(tile) {
+ continue
+ }
+
+ path, err := findPathTo(graph, u, pos)
+ if err != nil {
+ continue
+ }
+
+ if path.Distance/2 > int64(u.Movement.Range) {
+ continue
}
+
+ tiles = append(tiles, tile)
}
return tiles
}
@@ -149,11 +153,14 @@ func (u *Unit) FullAction() {
func (u *Unit) GetAvailActionPrototypes() []ActionPrototype {
prototypes := []ActionPrototype{}
- if u.AvailMoveActions > 0 {
+ if u.AvailMoveActions > 0 && u.Movement != INVALID_MOVEMENT() {
prototypes = append(prototypes, NewMoveActionPrototype(u))
}
- if u.AvailAttackActions > 0 {
+ m := u.GetController().gameState.Map
+ if u.AvailAttackActions > 0 && u.Attack != INVALID_ATTACK() &&
+ len(u.AttackableEnemyPermanents(m)) > 0 {
+
prototypes = append(prototypes, NewAttackActionPrototype(u))
}