From 926ca5cdd20a39e3e05aaa3486cb166718dab5ca Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 26 Jan 2023 22:16:10 +0100 Subject: intermeditae commit --- go/game/action.go | 110 ++++++++++++++------ go/game/card.go | 3 +- go/game/deck.go | 14 +++ go/game/kraken.go | 86 +++++++++------- go/game/map.go | 57 ++++++++++- go/game/map_test.go | 4 +- go/game/movement.go | 11 +- go/game/phase.go | 28 ----- go/game/phases.go | 42 ++++++++ go/game/pileOfCards.go | 4 + go/game/player.go | 45 ++++++--- go/game/playerControl.go | 33 ++++-- go/game/stack.go | 14 ++- go/game/state.go | 258 ++++++++++++++++++++++++++++++++++++++--------- go/game/tile.go | 12 +++ go/game/unit.go | 33 +++--- 16 files changed, 560 insertions(+), 194 deletions(-) delete mode 100644 go/game/phase.go create mode 100644 go/game/phases.go (limited to 'go/game') 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/phase.go deleted file mode 100644 index efcd430a..00000000 --- a/go/game/phase.go +++ /dev/null @@ -1,28 +0,0 @@ -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/phases.go b/go/game/phases.go new file mode 100644 index 00000000..ea239278 --- /dev/null +++ b/go/game/phases.go @@ -0,0 +1,42 @@ +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 "" +} + +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)) } -- cgit v1.2.3