From d76117cc913c738002c7562189ada75748645ff1 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Tue, 31 Jan 2023 00:16:53 +0100 Subject: intermediate commit --- go/client/main.go | 60 +++++++++++++--- go/game/action.go | 59 +++++++++------- go/game/ai.go | 36 +++++----- go/game/artifact.go | 4 +- go/game/card.go | 157 ++++++++++++++++++++++++++++++----------- go/game/cardImplementations.go | 31 ++++++++ go/game/cardParsing.go | 140 ++++++++++++++++++++++++++++++++++++ go/game/kraken.go | 42 +++++------ go/game/map.go | 2 +- go/game/permanent.go | 114 +++++++++++++++++++++--------- go/game/pileOfCards.go | 4 +- go/game/player.go | 37 +++++++--- go/game/playerControl.go | 7 +- go/game/range.go | 2 +- go/game/state.go | 60 +++++++++------- go/game/tile.go | 4 +- go/game/unit.go | 44 ++++++------ go/game/winCondition.go | 4 +- go/ui/hand.go | 8 ++- go/ui/mapView.go | 24 ++++--- go/ui/prompt.go | 78 ++++++++++++++++++++ go/ui/textBox.go | 9 ++- 22 files changed, 692 insertions(+), 234 deletions(-) create mode 100644 go/game/cardImplementations.go create mode 100644 go/game/cardParsing.go create mode 100644 go/ui/prompt.go diff --git a/go/client/main.go b/go/client/main.go index b8f1bf7a..62fa52c1 100644 --- a/go/client/main.go +++ b/go/client/main.go @@ -85,6 +85,7 @@ type Game struct { storeView *ui.PocList storeButton *ui.SimpleButton stackBuffer *ui.Buffer + prompt *ui.Prompt widgets []ui.Widget activePlayer *game.Player @@ -107,7 +108,14 @@ func NewGame() *Game { DEFAULT_BUTTON_HEIGHT, "Pass", func(*ui.SimpleButton) { - if g.hasPriority { + if g.prompt != nil { + + a := g.prompt.Confirm() + log.Println("Prompt confirmation returned", a) + g.declareAction(a) + g.prompt = nil + g.removeWidget(g.prompt) + } else if g.hasPriority { g.passPriority() } }) @@ -168,9 +176,9 @@ func (g *Game) initPlayerUi(player *game.Player) { var x, y int g.handLayer = ui.NewHandView(0, - g.mapView.GetHeight(), + g.mapView.Height(), HAND_VIEW_WIDTH, - screenHeight-g.mapView.GetHeight()-DEFAULT_BUTTON_HEIGHT, + screenHeight-g.mapView.Height()-DEFAULT_BUTTON_HEIGHT, player.Hand, true) g.addWidget(g.handLayer) @@ -261,7 +269,7 @@ func (g *Game) removeWidgetAt(idx int) { } func (g *Game) addActionChoice(perm game.Permanent, x, y int) { - actions := perm.GetAvailActionPrototypes() + actions := perm.AvailActionPrototypes() if len(actions) > 0 { g.selectedObject = perm onClick := func(c *ui.Choice, x, y int) { @@ -278,6 +286,15 @@ func (g *Game) removeActionChoice() { g.actionChoice = nil } +func (g *Game) AddHighlight(obj interface{}) { + switch obj := obj.(type) { + case ui.HandCard: + g.handLayer.AddHighlightCard(obj.C) + case *game.Tile: + g.mapView.AddHighlightTile(obj) + } +} + func (g *Game) clearMapHighlights() { g.mapView.ClearPermanentsHighlights() g.mapView.ClearTileHighlights() @@ -289,12 +306,20 @@ func (g *Game) declareAction(a game.Action) { g.clearMapHighlights() g.handLayer.ClearHighlights() g.hasPriority = false - g.removeWidget(g.passButton) + g.hidePassButton() g.stateBar.ForceRedraw() } +func (g *Game) showPassButton() { + g.addWidget(g.passButton) +} + +func (g *Game) hidePassButton() { + g.removeWidget(g.passButton) +} + func (g *Game) passPriority() { - g.declareAction(game.NewPassPriorityAction(g.activePlayer)) + g.declareAction(game.NewPassPriority(g.activePlayer)) } func (g *Game) handlePlayerNotifications() { @@ -302,7 +327,7 @@ func (g *Game) handlePlayerNotifications() { for n != nil { switch n.Notification { case game.PriorityNotification: - g.addWidget(g.passButton) + g.showPassButton() g.hasPriority = true case game.DeclaredActionNotification: @@ -321,6 +346,14 @@ func (g *Game) handlePlayerNotifications() { if !g.storesOnMap { g.showStore() } + + case game.HandCardSelectionPrompt: + ctx := n.Context.([]int) + min, max := ctx[0], ctx[1] + promptTxt := fmt.Sprintf("Select between %d and %d hand cards", min, max) + g.prompt = ui.NewHandCardPrompt(screenHeight/2, screenWidth, promptTxt) + g.addWidget(g.prompt) + g.showPassButton() } g.stateBar.ForceRedraw() @@ -362,7 +395,7 @@ func (g *Game) handleSelection(obj interface{}, x, y int) { var action game.Action switch obj := g.selectedObject.(type) { case *game.MoveActionPrototype: - action = obj.Specialize(tile) + action = obj.Specialize(g.gameState, tile) case *game.Card: if obj.IsPermanent() { action = game.NewPlayAction(g.activePlayer, obj, tile.Position) @@ -384,7 +417,7 @@ func (g *Game) handleSelection(obj interface{}, x, y int) { } else { switch obj := g.selectedObject.(type) { case *game.AttackActionPrototype: - action := obj.Specialize(perm) + action := obj.Specialize(g.gameState, perm) g.selectedObject = nil g.declareAction(action) g.mapView.HighlightPermanent(perm) @@ -437,7 +470,7 @@ func (g *Game) handleHover(obj interface{}, x, y int) { hoverHint = ui.NewPermInfo(x+ui.TILE_WIDTH, y, obj) case *game.Tile: if g.storesOnMap && obj.Type == game.TileTypes.Store { - poc := g.gameState.Map.GetStoreOn(obj.Position) + poc := g.gameState.Map.StoreOn(obj.Position) hoverHint = ui.NewPocList(x, y, poc) // Set a special hoverDetector resetFunc keeping the hovering store // around and allow card selection until the cursor leaves the store widget. @@ -473,7 +506,12 @@ func (g *Game) Update() error { obj := g.findObjectAt(x, y) if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { - g.handleSelection(obj, x, y) + if g.prompt != nil { + g.prompt.Add(obj) + g.AddHighlight(obj) + } else { + g.handleSelection(obj, x, y) + } g.hightlightSelection(obj) } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil { g.handleHover(obj, x, y) diff --git a/go/game/action.go b/go/game/action.go index f3b242b5..a2ca038e 100644 --- a/go/game/action.go +++ b/go/game/action.go @@ -6,7 +6,7 @@ import ( ) type ActionPrototype interface { - Specialize(...interface{}) Action + Specialize(*State, ...interface{}) Action String() string } @@ -16,12 +16,21 @@ type Action interface { String() string } -type PassPriorityAction struct{ Source *Player } +type PassPriority struct{ Source *Player } -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 (*PassPriority) Resolve(*State) {} +func (*PassPriority) PayCosts(*State) bool { return true } +func (*PassPriority) String() string { return "pass" } +func NewPassPriority(p *Player) Action { return &PassPriority{p} } + +type HandCardSelection struct { + cards []*Card +} + +func (*HandCardSelection) Resolve(*State) {} +func (*HandCardSelection) PayCosts(*State) bool { return true } +func (*HandCardSelection) String() string { return "hand card selection" } +func NewHandCardSelection(cards []*Card) Action { return &HandCardSelection{cards} } type ActionBase struct { Source interface{} @@ -44,7 +53,7 @@ func (a *ActionBase) String() string { switch source := a.Source.(type) { case Permanent: p := source - return fmt.Sprintf("%s: %s %s", p.GetController().Name, p.GetTile().Position.String(), a.desc) + return fmt.Sprintf("%s: %s %s", p.Controller().Name, p.Tile().Position.String(), a.desc) case *Player: return fmt.Sprintf("%s %s", source.Name, a.desc) default: @@ -58,7 +67,7 @@ type PlayAction struct { func NewPlayAction(p *Player, c *Card, targets interface{}) Action { costFunc := func(*State) bool { - cost := c.GetPlayCosts() + cost := c.PlayCosts() if p.Resource < cost { return false @@ -98,7 +107,7 @@ func (proto *MoveActionPrototype) String() string { return fmt.Sprintf("move %s", proto.Unit.Movement.String()) } -func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action { +func (proto *MoveActionPrototype) Specialize(s *State, args ...interface{}) Action { target := args[0].(*Tile) u := proto.Unit @@ -115,7 +124,7 @@ func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action { } desc := fmt.Sprintf("-> %s", target.Position.String()) - return &MoveAction{ActionBase{u, u.GetCard(), target, resolveFunc, costFunc, desc}} + return &MoveAction{ActionBase{u, u.Card(), target, resolveFunc, costFunc, desc}} } type AttackAction struct { @@ -134,7 +143,7 @@ func (proto *AttackActionPrototype) String() string { return fmt.Sprintf("attack %s", proto.Unit.Attack.String()) } -func (proto *AttackActionPrototype) Specialize(args ...interface{}) Action { +func (proto *AttackActionPrototype) Specialize(s *State, args ...interface{}) Action { target := args[0].(Permanent) u := proto.Unit @@ -150,25 +159,26 @@ func (proto *AttackActionPrototype) Specialize(args ...interface{}) Action { return true } - desc := fmt.Sprintf("x %s", target.GetTile().Position.String()) - return &AttackAction{ActionBase{u, u.GetCard(), target, resolveFunc, costFunc, desc}} + desc := fmt.Sprintf("x %s", target.Tile().Position.String()) + return &AttackAction{ActionBase{u, u.Card(), target, resolveFunc, costFunc, desc}} } type FullActionPrototype struct { - Unit *Unit + Unit *Unit + resolveFuncPrototype func(*State, *Unit) func(*State) } -func NewFullActionPrototype(u *Unit) *FullActionPrototype { - return &FullActionPrototype{u} +func NewFullActionPrototype(u *Unit, proto func(*State, *Unit) func(*State)) *FullActionPrototype { + return &FullActionPrototype{u, proto} } func (proto *FullActionPrototype) String() string { return "full_action" } -func (proto *FullActionPrototype) Specialize(args ...interface{}) Action { +func (proto *FullActionPrototype) Specialize(s *State, args ...interface{}) Action { u := proto.Unit - resolveFunc := args[0].(func(s *State)) + resolveFunc := proto.resolveFuncPrototype(s, u) costFunc := func(*State) bool { if u.AvailAttackActions < 1 && u.AvailMoveActions < 1 { return false @@ -178,24 +188,23 @@ func (proto *FullActionPrototype) Specialize(args ...interface{}) Action { return true } desc := "full_action" - return &ActionBase{u, u.GetCard(), nil, resolveFunc, costFunc, desc} + return &ActionBase{u, u.Card(), nil, resolveFunc, costFunc, desc} } type FreeActionPrototype struct { - Permanent Permanent - canPayFunc func(s *State) bool + Permanent Permanent } func (proto *FreeActionPrototype) String() string { return "free_action" } -func (proto *FreeActionPrototype) Specialize(args ...interface{}) Action { +func (proto *FreeActionPrototype) Specialize(s *State, args ...interface{}) Action { p := proto.Permanent resolveFunc := args[0].(func(s *State)) costFunc := args[1].(func(s *State) bool) desc := "free_action" - return &FreeAction{ActionBase{p, p.GetCard(), nil, resolveFunc, costFunc, desc}} + return &FreeAction{ActionBase{p, p.Card(), nil, resolveFunc, costFunc, desc}} } type FreeAction struct { @@ -252,10 +261,10 @@ func (a *UpkeepAction) PayCosts(*State) bool { 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 keep %s", a.player.Name, a.unit.Tile().Position.String()) } - return fmt.Sprintf("%s disband %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 { diff --git a/go/game/ai.go b/go/game/ai.go index 1b4b4e79..01820a19 100644 --- a/go/game/ai.go +++ b/go/game/ai.go @@ -48,7 +48,7 @@ func (ai *UnitAI) Execute(f func(*UnitAI)) { } func NewUnitAI(s *State, u *Unit) *UnitAI { - aiDesc := u.Card.getAI() + aiDesc := u.Card().getAI() if aiDesc == "" { return nil } @@ -64,8 +64,8 @@ func NewUnitAI(s *State, u *Unit) *UnitAI { case "shy": go ai.Execute(ShyAI) case "wandering": - aiDesc := u.Card.getAI() - x, err := strconv.Atoi(strings.Split(u.Card.getAI(), " ")[1]) + 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) } @@ -102,9 +102,9 @@ func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph { } func findPathTo(graph *dijkstra.Graph, u *Unit, pos Position) (dijkstra.BestPath, error) { - srcId, err := graph.GetMapping(u.GetTile().Position.String()) + srcId, err := graph.GetMapping(u.Tile().Position.String()) if err != nil { - log.Fatalf("No mapping %s in Graph: %s", u.GetTile().Position.String(), err) + log.Fatalf("No mapping %s in Graph: %s", u.Tile().Position.String(), err) } destId, err := graph.GetMapping(pos.String()) @@ -120,7 +120,7 @@ func findPathsToUnits(graph *dijkstra.Graph, m *Map, u *Unit, units []*Unit) []* for _, unit := range units { var bestPathToUnit *dijkstra.BestPath = nil - for _, adjacentTile := range TilesInRangeFromOrigin(m, unit.GetTile().Position, 1) { + for _, adjacentTile := range TilesInRangeFromOrigin(m, unit.Tile().Position, 1) { path, err := findPathTo(graph, u, adjacentTile.Position) if err != nil { continue @@ -148,7 +148,7 @@ func moveToRandomTile(ai *UnitAI) Action { idx = ai.s.Rand.Intn(len(moveTargets) - 1) } moveTarget := moveTargets[idx] - a := NewMoveActionPrototype(ai.u).Specialize(moveTarget) + a := NewMoveActionPrototype(ai.u).Specialize(ai.s, moveTarget) return a } @@ -158,7 +158,7 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { } graph := ai.s.Map.generateMapGraphFor(ai.u) - enemyUnits := ai.s.EnemyUnits(ai.u.GetController()) + enemyUnits := ai.s.EnemyUnits(ai.u.Controller()) if len(enemyUnits) == 0 { return nil } @@ -185,13 +185,13 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { 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) { + if IsPositionInRange(ai.u.Tile().Position, nextStep, ai.u.Movement.Range) { target = nextStep } else { break } } - a := NewMoveActionPrototype(ai.u).Specialize(ai.s.Map.TileAt(target)) + a := NewMoveActionPrototype(ai.u).Specialize(ai.s, ai.s.Map.TileAt(target)) return a } @@ -206,7 +206,7 @@ func attackAttackableEnemyPerm(ai *UnitAI) Action { idx = ai.s.Rand.Intn(len(attackablePerms) - 1) } attackablePerm := attackablePerms[idx] - a := NewAttackActionPrototype(ai.u).Specialize(attackablePerm) + a := NewAttackActionPrototype(ai.u).Specialize(ai.s, attackablePerm) return a } @@ -254,7 +254,7 @@ func ShyAI(ai *UnitAI) { // 1. inEnemyAttackRange := false out: - for _, enemyUnit := range ai.s.EnemyUnits(ai.u.GetController()) { + for _, enemyUnit := range ai.s.EnemyUnits(ai.u.Controller()) { for _, p := range enemyUnit.AttackablePermanents(ai.s.Map) { if p == ai.u { inEnemyAttackRange = true @@ -263,8 +263,12 @@ out: } } if !inEnemyAttackRange && ai.u.HasFullAction() { - // return NewFullActionPrototype(u).Specialize(u.FullActionResolveFunc()) - a := NewFullActionPrototype(ai.u).Specialize() + fullActions := ai.u.Card().Impl.fullActionPrototypes(ai.u) + idx := 0 + if len(fullActions) > 1 { + idx = ai.s.Rand.Intn(len(fullActions) - 1) + } + a := fullActions[idx].Specialize(ai.s, ai.u) ai.actions <- a } @@ -287,8 +291,8 @@ out: // proceed at 1. func WanderingAI(ai *UnitAI, x int) { // 1. - for _, enemyUnit := range ai.s.EnemyUnits(ai.u.GetController()) { - if IsPositionInRange(ai.u.GetTile().Position, enemyUnit.GetTile().Position, x) { + for _, enemyUnit := range ai.s.EnemyUnits(ai.u.Controller()) { + if IsPositionInRange(ai.u.Tile().Position, enemyUnit.Tile().Position, x) { AggressiveAI(ai) break } diff --git a/go/game/artifact.go b/go/game/artifact.go index 92a0361a..23b653a6 100644 --- a/go/game/artifact.go +++ b/go/game/artifact.go @@ -39,13 +39,13 @@ func (a *Artifact) Fight(p Permanent) { } func (a *Artifact) IsDestroyed() bool { - return a.Damage >= a.Solid + return a.Damage() >= a.Solid } func (a *Artifact) Attackable() bool { return a.Solid > 0 } -func (a *Artifact) GetAvailActionPrototypes() []ActionPrototype { +func (a *Artifact) AvailActionPrototypes() []ActionPrototype { return []ActionPrototype{} } diff --git a/go/game/card.go b/go/game/card.go index 07ef033b..d4515676 100644 --- a/go/game/card.go +++ b/go/game/card.go @@ -1,6 +1,7 @@ package game import ( + "fmt" "io" "log" "net/http" @@ -15,30 +16,30 @@ import ( type CardType int const ( - UndefinedCardType CardType = iota - UnitType - SpellType - ArtifactType - EquipmentType - PotionType - IntentionType + undefinedCardType CardType = iota + unitType + spellType + artifactType + equipmentType + potionType + intentionType ) func (ct CardType) String() string { switch ct { - case UndefinedCardType: + case undefinedCardType: return "undefined" - case UnitType: + case unitType: return "unit" - case SpellType: + case spellType: return "spell" - case ArtifactType: + case artifactType: return "artifact" - case EquipmentType: + case equipmentType: return "equipment" - case PotionType: + case potionType: return "potion" - case IntentionType: + case intentionType: return "intention" } log.Panicf("Switch not exhausting %d", ct) @@ -53,26 +54,63 @@ var CardTypes = struct { Potion CardType Intention CardType }{ - Unit: UnitType, - Spell: SpellType, - Artifact: ArtifactType, - Equipment: EquipmentType, - Potion: PotionType, - Intention: IntentionType, + Unit: unitType, + Spell: spellType, + Artifact: artifactType, + Equipment: equipmentType, + Potion: potionType, + Intention: intentionType, } var CardTypeNames = map[string]CardType{ - "unit": UnitType, - "spell": SpellType, - "artifact": ArtifactType, - "equipment": EquipmentType, - "potion": PotionType, - "intention": IntentionType, + "unit": unitType, + "spell": spellType, + "artifact": artifactType, + "equipment": equipmentType, + "potion": potionType, + "intention": intentionType, +} + +type cardImplementation interface { + spawnTiles(*State, *Player) []*Tile + fullActionPrototypes(*Unit) []*FullActionPrototype + freeActionPrototypes(Permanent) []*FreeActionPrototype +} + +// Default implementation of the cardImplementation interface. +// +// Card Implementations should embed this struct to "inherit" the +// default behavior. +type cardImplementationBase struct{} + +func (impl *cardImplementationBase) spawnTiles(*State, *Player) []*Tile { + return nil +} + +func (impl *cardImplementationBase) fullActionPrototypes(*Unit) []*FullActionPrototype { + return nil +} + +func (impl *cardImplementationBase) freeActionPrototypes(Permanent) []*FreeActionPrototype { + return nil +} + +// Map of all card implementations +var cardImplementations map[string]cardImplementation + +func getCardImplementation(c *Card) cardImplementation { + if impl, found := cardImplementations[c.Path()]; found { + return impl + } + + impl := newParsedCardImplementation(c) + cardImplementations[c.Path()] = impl + return impl } const ( CARD_DATA_URL string = "data/cards" - NOT_BUYABLE int = -1 + NOT_BUYABLE int = -1 // Maybe this needs to be changed some day to allow more design space ) type Card struct { @@ -81,9 +119,22 @@ type Card struct { Type CardType BuyCost int Values map[string]interface{} + Impl cardImplementation } -var cards = map[string]map[string][]byte{} +var cardDefinitions = map[string]map[string][]byte{} + +func getCardDefinition(set, cardName string) []byte { + cardDefinition, found := cardDefinitions[set][cardName] + if !found { + cardDefinition = retrieveCardDefinition(path.Join(set, cardName)) + if cardDefinitions[set] == nil { + cardDefinitions[set] = map[string][]byte{} + } + cardDefinitions[set][cardName] = cardDefinition + } + return cardDefinition +} func retrieveCardDefinition(cardPath string) []byte { url := PROTOCOL + path.Join(BASE_URL, CARD_DATA_URL, cardPath+".yml") @@ -135,12 +186,16 @@ func (card *Card) parseDefinition(definition []byte) { bc, found := data["buy"] if found { card.BuyCost = bc.(int) + } else { + card.BuyCost = NOT_BUYABLE } delete(data, "buy") for k, v := range data { card.Values[k.(string)] = v } + + card.Impl = getCardImplementation(card) } func (card *Card) getCanonicalValues(key string) []string { @@ -187,9 +242,23 @@ func (card *Card) hasEffect(effect string) bool { return slices.Contains(card.getEffects(), effect) } -func (card *Card) hasMovementEffect(effect string) bool { +func (card *Card) getXEffect(effect string) (int, error) { + for _, e := range card.getEffects() { + if !strings.Contains(e, effect) { + continue + } + + tokens := strings.SplitN(e, " ", 2) + return strconv.Atoi(tokens[1]) + } + return 0, fmt.Errorf("Crad %s has no effect %s", card.Name, effect) +} + +func (card *Card) hasPlacementConstrain(effect string) bool { if slices.Contains(card.getEffects(), effect) { return true + } else if card.Type != CardTypes.Unit { + return false } movementDesc := card.getCanonicalValues("movement")[0] @@ -208,16 +277,15 @@ func NewCard(cardPath string) *Card { cardName := path.Base(cardPath) set := path.Dir(cardPath) - cardDefinition, found := cards[set][cardName] - if !found { - cardDefinition = retrieveCardDefinition(cardPath) - if cards[set] == nil { - cards[set] = map[string][]byte{} - } - cards[set][cardName] = cardDefinition - } + cardDefinition := getCardDefinition(set, cardName) - c := Card{"", SetIdentifierNames[set], UndefinedCardType, -1, map[string]interface{}{}} + c := Card{ + Name: "", + Set: SetIdentifierNames[set], + Type: undefinedCardType, + BuyCost: NOT_BUYABLE, + Values: map[string]interface{}{}, + } c.parseDefinition(cardDefinition) return &c } @@ -228,12 +296,19 @@ func (c *Card) Path() string { } func (c *Card) IsPermanent() bool { - return c.Type != UndefinedCardType && c.Type != SpellType && c.Type != IntentionType + return c.Type == CardTypes.Unit || + c.Type != CardTypes.Artifact || + c.Type != CardTypes.Equipment || + c.Type != CardTypes.Potion +} + +func (c *Card) IsBuyable() bool { + return c.BuyCost != NOT_BUYABLE } -func (c *Card) GetPlayCosts() int { +func (c *Card) PlayCosts() int { switch c.Type { - case UnitType: + case CardTypes.Unit: return c.Values["upkeep"].(int) default: return c.Values["play"].(int) diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go new file mode 100644 index 00000000..d1f8de70 --- /dev/null +++ b/go/game/cardImplementations.go @@ -0,0 +1,31 @@ +package game + +type farmerImplementation struct { + cardImplementationBase +} + +type advisorImplementation struct { + cardImplementationBase +} + +func (impl *advisorImplementation) fullActionPrototypes(u *Unit) []*FullActionPrototype { + resolvePrototype := func(s *State, u *Unit) func(*State) { + return func(s *State) { + controller := u.Controller() + controller.DrawN(1) + cards := controller.PromptHandCardSelection(0, 1) + if cards != nil { + controller.Deck.MoveCard(cards[0], s.Exile) + } + } + } + return []*FullActionPrototype{NewFullActionPrototype(u, resolvePrototype)} +} + +func init() { + cardImplementations = map[string]cardImplementation{ + "base/advisor": &advisorImplementation{}, + + "misc/farmer": &farmerImplementation{}, + } +} diff --git a/go/game/cardParsing.go b/go/game/cardParsing.go new file mode 100644 index 00000000..af8e1560 --- /dev/null +++ b/go/game/cardParsing.go @@ -0,0 +1,140 @@ +package game + +import ( + "log" + "strconv" + "strings" +) + +type parsedCardImplementation struct { + card *Card + spawnTilesFunc func(*State, *Player) []*Tile + genFullActionPrototypes func(*Unit) []*FullActionPrototype + genFreeActionPrototypes func(Permanent) []*FreeActionPrototype +} + +func (impl *parsedCardImplementation) spawnTiles(s *State, p *Player) []*Tile { + if impl.spawnTilesFunc != nil { + return impl.spawnTilesFunc(s, p) + } + return nil +} + +func (impl *parsedCardImplementation) fullActionPrototypes(u *Unit) []*FullActionPrototype { + if impl.genFullActionPrototypes != nil { + return impl.genFullActionPrototypes(u) + } + return nil +} + +func (impl *parsedCardImplementation) freeActionPrototypes(p Permanent) []*FreeActionPrototype { + if impl.genFreeActionPrototypes != nil { + return impl.genFreeActionPrototypes(p) + } + return nil +} + +func newParsedCardImplementation(c *Card) *parsedCardImplementation { + impl := &parsedCardImplementation{card: c} + for _, effect := range c.getEffects() { + impl.parseEffect(effect) + } + return impl +} + +func (impl *parsedCardImplementation) parseEffect(effect string) { + if strings.Contains(effect, "can be played") { + impl.parsePlayModification(effect) + } +} + +func parseUnitConstrain(constrain string) func(ctx interface{}, u *Unit) bool { + enemy := strings.Contains(constrain, "enemy") + if enemy { + return func(ctx interface{}, e *Unit) bool { + p := ctx.(*Player) + return p.IsEnemy(e.Controller()) + } + } else { + log.Println("Only enemy unit constrains are implemented yet") + } + + return nil +} + +func parseLocation(location string) func(interface{}, *State) []*Tile { + r := -1 + var what string + if strings.Contains(location, "on") { + r = 0 + tokens := strings.SplitN(location, "on ", 2) + what = tokens[1] + + } else if strings.Contains(location, "next to") { + r = 1 + tokens := strings.SplitN(location, "next to ", 2) + what = tokens[1] + + } else if strings.Contains(location, "in range") { + tokens := strings.SplitN(location, "in range ", 2) + afterRangePattern := tokens[1] + + var rangeStr string + tokens = strings.SplitN(afterRangePattern, " ", 2) + rangeStr, what = tokens[0], tokens[1] + + var err error + r, err = strconv.Atoi(rangeStr) + if err != nil { + log.Fatalf("Unsupported range pattern \"%s\" in \"%s\"", "range "+rangeStr, location) + } + } + + if r == -1 || what == "" { + return nil + } + + if strings.Contains(what, "unit") { + constrainFunc := parseUnitConstrain(what) + if constrainFunc != nil { + return func(ctx interface{}, s *State) []*Tile { + origins := s.Map.FilterTiles(func(t *Tile) bool { + if t.Permanent != nil && + t.Permanent.Card().Type == CardTypes.Unit && + constrainFunc(ctx, t.Permanent.(*Unit)) { + return true + } + return false + }) + + if r == 0 { + return origins + } else { + tiles := []*Tile{} + for _, origin := range origins { + tiles = append(tiles, TilesInRangeFromOrigin(s.Map, origin.Position, r)...) + } + return tiles + } + } + } + } + log.Println("Only unit location constrains are implemented yet") + return nil +} + +func (impl *parsedCardImplementation) parsePlayModification(effect string) { + tokens := strings.SplitN(effect, " can be played ", 2) + log.Println(tokens) + what, how := tokens[0], tokens[1] + if what == strings.ToLower(impl.card.Name) { + if locationFunc := parseLocation(how); locationFunc != nil { + impl.spawnTilesFunc = func(p *State, s *Player) []*Tile { return locationFunc(s, p) } + } else { + log.Println("Play modification other than locations are not implemented ") + } + } else { + log.Println(effect) + log.Println("Play modification for other cards are not implemented") + } +} diff --git a/go/game/kraken.go b/go/game/kraken.go index ef63bbd0..177423ff 100644 --- a/go/game/kraken.go +++ b/go/game/kraken.go @@ -11,21 +11,23 @@ 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` +// 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 = `2 kraken/frost_pylon` func NewKraken(id int, s *State, c color.Color) *Player { kraken := NewPlayer(id, KRAKEN_NAME, NewDeck(), s, c) @@ -84,12 +86,6 @@ func (ctrl *KrakenControl) krakenTurn() { } c := drawn[0] - if c.Name == "Frost Pylon" { - // TODO: implement frost pylon placement - kraken.DiscardPile.AddCard(c) - continue - } - if c.BuyCost > kraken.Resource { log.Println("Kraken discards", c.Name, "because resource to low", kraken.Resource) kraken.DiscardPile.AddCard(c) @@ -136,7 +132,7 @@ func (ctrl *KrakenControl) krakenTurn() { } } - ctrl.actions <- NewPassPriorityAction(ctrl.kraken) + ctrl.actions <- NewPassPriority(ctrl.kraken) } func (ctrl *KrakenControl) RecvAction() Action { @@ -145,7 +141,7 @@ func (ctrl *KrakenControl) RecvAction() Action { if !s.IsActivePlayer(kraken) || s.ActivePhase != Phases.ActionPhase || !s.Stack.IsEmpty() { - return NewPassPriorityAction(ctrl.kraken) + return NewPassPriority(ctrl.kraken) } ctrl.syncGameState.Done() diff --git a/go/game/map.go b/go/game/map.go index cfabfaf9..cab18f26 100644 --- a/go/game/map.go +++ b/go/game/map.go @@ -298,6 +298,6 @@ func (m *Map) HasStores() bool { return len(m.Stores) > 0 } -func (m *Map) GetStoreOn(p Position) *Store { +func (m *Map) StoreOn(p Position) *Store { return m.Stores[p] } diff --git a/go/game/permanent.go b/go/game/permanent.go index acdcd85f..21655d71 100644 --- a/go/game/permanent.go +++ b/go/game/permanent.go @@ -3,78 +3,124 @@ package game import ( "fmt" "log" + "strconv" + "strings" + + "golang.org/x/exp/slices" ) type Permanent interface { - GetCard() *Card - GetTile() *Tile - GetDamage() int - GetPile() []Permanent - GetController() *Player - GetOwner() *Player + Card() *Card + Tile() *Tile + Damage() int + Pile() []Permanent + Controller() *Player + Owner() *Player + Effects() []string + HasEffect(string) bool + XEffect(string) (int, error) String() string Fight(p Permanent) AddDamage(damage int) IsDestroyed() bool Attackable() bool - GetAvailActionPrototypes() []ActionPrototype - Swimming() bool + AvailActionPrototypes() []ActionPrototype } type PermanentBase struct { - Card *Card - Tile *Tile - Damage int - Pile []Permanent - Controller *Player - Owner *Player + card *Card + tile *Tile + damage int + pile []Permanent + controller *Player + owner *Player + tmpEffects []string } func NewPermanentBase(card *Card, tile *Tile, owner *Player) PermanentBase { if tile.Permanent != nil { log.Fatalf("Tile at %v already occupied by %v", tile.Position, tile.Permanent) } - return PermanentBase{card, tile, 0, []Permanent{}, owner, owner} + + return PermanentBase{ + card: card, + tile: tile, + controller: owner, + owner: owner, + } } -func (p *PermanentBase) GetCard() *Card { - return p.Card +func (p *PermanentBase) Card() *Card { + return p.card } -func (p *PermanentBase) GetTile() *Tile { - return p.Tile +func (p *PermanentBase) Tile() *Tile { + return p.tile } -func (p *PermanentBase) GetDamage() int { - return p.Damage +func (p *PermanentBase) Damage() int { + return p.damage } -func (p *PermanentBase) GetPile() []Permanent { - return p.Pile +func (p *PermanentBase) Pile() []Permanent { + return p.pile } -func (p *PermanentBase) GetController() *Player { - return p.Controller +func (p *PermanentBase) Controller() *Player { + return p.controller } -func (p *PermanentBase) GetOwner() *Player { - return p.Owner +func (p *PermanentBase) Owner() *Player { + return p.owner } func (p *PermanentBase) String() string { return fmt.Sprintf("%s's %s@%s", - p.GetController().Name, p.GetCard().Name, p.GetTile().Position.String()) + p.Controller().Name, p.Card().Name, p.Tile().Position.String()) +} + +func DealDamage(src, dest Permanent, damage int) { + actualDamage := damage + shield, err := dest.XEffect("shield") + if err != nil { + // TODO: implement piercing + + actualDamage -= shield + if actualDamage < 0 { + actualDamage = 0 + } + } + dest.AddDamage(actualDamage) } func (p *PermanentBase) AddDamage(damage int) { - p.Damage += damage + 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") +func (p *PermanentBase) Effects() []string { + return append(p.card.getEffects(), p.tmpEffects...) +} + +func (p *PermanentBase) HasEffect(effect string) bool { + return slices.Contains(p.tmpEffects, effect) || p.card.hasEffect(effect) +} + +func (p *PermanentBase) XEffect(effect string) (int, error) { + x, err := p.card.getXEffect(effect) + + for _, e := range p.tmpEffects { + if !strings.Contains(e, effect) { + continue + } + + tokens := strings.SplitN(e, " ", 2) + tmpX, err := strconv.Atoi(tokens[1]) + if err != nil { + return 0, err + } + x += tmpX } + + return x, err } diff --git a/go/game/pileOfCards.go b/go/game/pileOfCards.go index 457f804a..c8b81c87 100644 --- a/go/game/pileOfCards.go +++ b/go/game/pileOfCards.go @@ -3,7 +3,7 @@ package game type PileOfCards interface { Size() int IsEmpty() bool - GetCards() []*Card + Cards() []*Card AddCards(cards []*Card) AddCard(card *Card) RemoveCard(card *Card) @@ -27,7 +27,7 @@ func (poc *PileOfCardsBase) IsEmpty() bool { return poc.Size() == 0 } -func (poc *PileOfCardsBase) GetCards() []*Card { +func (poc *PileOfCardsBase) Cards() []*Card { return poc.cards } diff --git a/go/game/player.go b/go/game/player.go index 608fc7e9..7a72e111 100644 --- a/go/game/player.go +++ b/go/game/player.go @@ -2,6 +2,7 @@ package game import ( "image/color" + "log" ) const ( @@ -60,7 +61,7 @@ func (p *Player) Upkeep() { // TODO: handle upkeep triggers for _, unit := range p.gameState.Units { - if unit.Controller != p { + if unit.controller != p { continue } @@ -76,7 +77,7 @@ func (p *Player) Upkeep() { func (p *Player) ActionPhase() { for { a := promptAction(p.Ctrl) - if _, ok := a.(*PassPriorityAction); ok { + if _, ok := a.(*PassPriority); ok { return } p.gameState.DeclareAction(a) @@ -84,19 +85,13 @@ func (p *Player) ActionPhase() { } } -func (p *Player) buy(c *Card) { - p.Resource -= c.BuyCost - p.DiscardPile.AddCard(c) -} - func (p *Player) BuyPhase() { a := promptBuy(p.Ctrl) - if _, ok := a.(*PassPriorityAction); ok { + if _, ok := a.(*PassPriority); ok { return } p.gameState.DeclareAction(a) p.gameState.Stack.Resolve() - // TODO: select card to buy } func (p *Player) DiscardCard(c *Card) { @@ -104,7 +99,24 @@ func (p *Player) DiscardCard(c *Card) { } func (p *Player) DiscardPhase() { - // TODO: select cards to discard + cards := p.PromptHandCardSelection(0, p.Deck.Size()) + for _, c := range cards { + p.DiscardCard(c) + } +} + +func (p *Player) PromptHandCardSelection(min, max int) []*Card { + p.Ctrl.SendNotification(NewHandCardSelectionPrompt(min, max)) + switch a := p.Ctrl.RecvAction().(type) { + case *HandCardSelection: + // TODO: validate cards + return a.cards + case *PassPriority: + return nil + default: + log.Fatalf("Unexpected response type %T for hand card selection", a) + } + return nil } func (p *Player) Draw() { @@ -120,3 +132,8 @@ func (p *Player) DrawN(n int) { drawnCards := p.Deck.Draw(n) p.Hand.AddCards(drawnCards) } + +func (p *Player) IsEnemy(other *Player) bool { + // TODO: support coop / teams + return p != other +} diff --git a/go/game/playerControl.go b/go/game/playerControl.go index 5e1552f0..e62d91b3 100644 --- a/go/game/playerControl.go +++ b/go/game/playerControl.go @@ -8,6 +8,7 @@ const ( PriorityNotification UpkeepPrompt BuyPrompt + HandCardSelectionPrompt ) func (n PlayerNotification) IsPriorityNotification() bool { @@ -40,6 +41,10 @@ func NewBuyPrompt() PlayerNotification { return PlayerNotification{BuyPrompt, nil, nil} } +func NewHandCardSelectionPrompt(min, max int) PlayerNotification { + return PlayerNotification{HandCardSelectionPrompt, []int{min, max}, nil} +} + type PlayerControl interface { RecvAction() Action SendNotification(PlayerNotification) @@ -47,7 +52,7 @@ type PlayerControl interface { type DummyPlayerControl struct{ P *Player } -func (c *DummyPlayerControl) RecvAction() Action { return NewPassPriorityAction(c.P) } +func (c *DummyPlayerControl) RecvAction() Action { return NewPassPriority(c.P) } func (*DummyPlayerControl) SendNotification(PlayerNotification) {} type ChanPlayerControl struct { diff --git a/go/game/range.go b/go/game/range.go index af1e8fd0..ab86c44e 100644 --- a/go/game/range.go +++ b/go/game/range.go @@ -54,5 +54,5 @@ func TilesInRangeFromOrigin(m *Map, origin Position, r int) []*Tile { } func TilesInRange(m *Map, p Permanent, r int) []*Tile { - return TilesInRangeFromOrigin(m, p.GetTile().Position, r) + return TilesInRangeFromOrigin(m, p.Tile().Position, r) } diff --git a/go/game/state.go b/go/game/state.go index 97283be4..d0a516c9 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -15,6 +15,7 @@ import ( type State struct { Decks []*Deck Stores []*Store + Exile *PileOfCardsBase Map *Map Stack *Stack Hands []*Hand @@ -31,6 +32,7 @@ type State struct { func NewState() *State { s := &State{} s.Stack = NewStack(s) + s.Exile = NewPileOfCards() s.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) return s } @@ -122,12 +124,12 @@ func (s *State) IsValidMove(a *MoveAction) error { 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()) + 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()) + u.card.Name, u.tile.Position.String(), tile.Position.String()) } return nil @@ -139,17 +141,21 @@ func (s *State) IsValidAttack(a *AttackAction) error { } unit := a.Source.(Unit) + if unit.Attack == INVALID_ATTACK() { + return fmt.Errorf("%s can not attack", unit.card.Name) + } + target := a.Targets.(Permanent) if !target.Attackable() { - return fmt.Errorf("Target %s is not attackable") + return fmt.Errorf("Target %s is not attackable", target.Tile().Position.String()) } - 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") + if !IsPositionInRange(unit.tile.Position, target.Tile().Position, unit.Attack.Range) { + return fmt.Errorf("Attack target on %s is not in attack range %d of %s on %s", + target.Tile().Position.String(), unit.Attack.Range, unit.card.Name, + unit.tile.Position.String()) } - // TODO: check move - return nil } @@ -169,8 +175,8 @@ func (s *State) IsValidBuy(a *BuyAction) error { controlsUnitOnStore := false for _, tile := range storeTiles { controlsUnitOnStore = controlsUnitOnStore || (tile.Permanent != nil && - tile.Permanent.GetCard().Type == CardTypes.Unit && - tile.Permanent.GetController() == a.player) + tile.Permanent.Card().Type == CardTypes.Unit && + tile.Permanent.Controller() == a.player) } if !controlsUnitOnStore { return fmt.Errorf("%s' controls no unit on a store containing %s", @@ -209,11 +215,14 @@ func (s *State) PushAction(a Action) { func (s *State) DeclareAction(a Action) { var err error = nil - switch a.(type) { + switch a := a.(type) { case *BuyAction: if s.ActivePhase != Phases.BuyPhase { err = fmt.Errorf("Cards can only be bought during one's buy phase") } + if !a.card.IsBuyable() { + err = fmt.Errorf("Card %s is not buyable", a.card.Name) + } case *FreeAction: default: if !s.Stack.IsEmpty() { @@ -239,7 +248,7 @@ func (s *State) AllPassing(skipFirst bool) bool { p.Ctrl.SendNotification(NewPriorityNotification()) a := p.Ctrl.RecvAction() switch a.(type) { - case *PassPriorityAction: + case *PassPriority: log.Printf("%s passed %d/%d\n", p.Name, i+1, nPlayers) default: s.DeclareAction(a) @@ -284,9 +293,9 @@ func (s *State) AddNewArtifact(card *Card, pos Position, owner *Player) { func (s *State) AddNewPermFromPath(cardPath string, pos Position, owner *Player) { card := NewCard(cardPath) switch card.Type { - case UnitType: + case CardTypes.Unit: s.AddNewUnit(card, pos, owner) - case ArtifactType: + case CardTypes.Artifact: s.AddNewArtifact(card, pos, owner) } } @@ -319,7 +328,7 @@ func (s *State) DestroyPermanent(p Permanent) { case *Artifact: s.Artifacts = removePtr(s.Artifacts, p) } - p.GetOwner().DiscardPile.AddCard(p.GetCard()) + p.Owner().DiscardPile.AddCard(p.Card()) } func (s *State) Fight(p1, p2 Permanent) { @@ -349,22 +358,21 @@ func (s *State) FilterUnits(filter func(*Unit) bool) []*Unit { func (s *State) OwnUnits(player *Player) []*Unit { return s.FilterUnits(func(u *Unit) bool { - return u.GetController() == player + return u.Controller() == player }) } func (s *State) EnemyUnits(player *Player) []*Unit { return s.FilterUnits(func(u *Unit) bool { - // TODO: support coop / teams - return u.GetController() != player + return u.Controller().IsEnemy(player) }) } func (s *State) ResolvePlay(p *Player, c *Card, targets interface{}) { switch c.Type { - case UnitType: + case CardTypes.Unit: s.AddNewUnit(c, targets.(Position), p) - case ArtifactType: + case CardTypes.Artifact: s.AddNewArtifact(c, targets.(Position), p) default: log.Printf("Resolving unhandled card type: %s\n", c.Type.String()) @@ -419,12 +427,16 @@ func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile { log.Fatalf("AvailableSpawnTiles called for %s", c.Type.String()) } - var candidates []*Tile - if c.hasMovementEffect("swimming") { - candidates = s.WaterSpawnTiles(p) - } else { - candidates = s.SpawnTiles(p) + candidates := c.Impl.spawnTiles(s, p) + if candidates == nil { + // Default spawn tiles + if c.hasPlacementConstrain("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 9adb1eae..06af795c 100644 --- a/go/game/tile.go +++ b/go/game/tile.go @@ -100,10 +100,10 @@ func (t *Tile) IsFree() bool { } func (t *Tile) IsAvailableForCard(card *Card) bool { - if t.Water && !card.hasMovementEffect("swimming") { + if t.Water && !card.hasPlacementConstrain("swimming") { return false } - if !t.Water && card.hasMovementEffect("swimming") { + if !t.Water && card.hasPlacementConstrain("swimming") { return false } return t.IsFree() diff --git a/go/game/unit.go b/go/game/unit.go index aa535086..f4f7d9cc 100644 --- a/go/game/unit.go +++ b/go/game/unit.go @@ -1,9 +1,5 @@ package game -import ( - "log" -) - type UnitState int const ( @@ -51,18 +47,18 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit { } u := &Unit{ - NewPermanentBase(card, tile, owner), - card.Values["health"].(int), - movement, - attack, - upkeep, - []*FullActionPrototype{}, - []*FreeActionPrototype{}, - []UnitState{}, - 1, - 1, + PermanentBase: NewPermanentBase(card, tile, owner), + Health: card.Values["health"].(int), + Movement: movement, + Attack: attack, + Upkeep: upkeep, + AvailMoveActions: 1, + AvailAttackActions: 1, } + u.FullActions = card.Impl.fullActionPrototypes(u) + u.FreeActions = card.Impl.freeActionPrototypes(u) + tile.Permanent = u return u } @@ -72,16 +68,16 @@ func NewUnitFromPath(cardPath string, tile *Tile, owner *Player) *Unit { } func (u *Unit) move(s *State, tile *Tile) { - u.Tile.Permanent = nil + u.tile.Permanent = nil tile.Permanent = u - u.Tile = tile + u.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) { + for _, pos := range PositionsInRange(u.tile.Position, u.Movement.Range, false) { tile := m.TileAt(pos) if tile == nil || !u.IsAvailableTile(tile) { continue @@ -116,11 +112,11 @@ func (u *Unit) AttackablePermanents(m *Map) []Permanent { } func (u *Unit) AttackableEnemyPermanents(m *Map) []Permanent { - controller := u.GetController() + controller := u.Controller() permanents := []Permanent{} for _, t := range u.AttackableTiles(m) { if t.Permanent != nil && t.Permanent.Attackable() && - t.Permanent.GetController() != controller { + t.Permanent.Controller() != controller { permanents = append(permanents, t.Permanent) } @@ -129,15 +125,15 @@ func (u *Unit) AttackableEnemyPermanents(m *Map) []Permanent { } func (u *Unit) IsAvailableTile(tile *Tile) bool { - return tile.IsAvailableForCard(u.Card) + return tile.IsAvailableForCard(u.card) } func (u *Unit) Fight(p Permanent) { - p.AddDamage(u.Attack.Damage) + DealDamage(u, p, u.Attack.Damage) } func (u *Unit) IsDestroyed() bool { - return u.PermanentBase.Damage >= u.Health + return u.damage >= u.Health } func (u *Unit) Attackable() bool { @@ -151,13 +147,13 @@ func (u *Unit) HasFullAction() bool { func (u *Unit) FullAction() { } -func (u *Unit) GetAvailActionPrototypes() []ActionPrototype { +func (u *Unit) AvailActionPrototypes() []ActionPrototype { prototypes := []ActionPrototype{} if u.AvailMoveActions > 0 && u.Movement != INVALID_MOVEMENT() { prototypes = append(prototypes, NewMoveActionPrototype(u)) } - m := u.GetController().gameState.Map + m := u.Controller().gameState.Map if u.AvailAttackActions > 0 && u.Attack != INVALID_ATTACK() && len(u.AttackableEnemyPermanents(m)) > 0 { diff --git a/go/game/winCondition.go b/go/game/winCondition.go index fa07257f..4db36e1d 100644 --- a/go/game/winCondition.go +++ b/go/game/winCondition.go @@ -7,11 +7,11 @@ import ( func KingGame(s *State) []*Player { foundKings := map[*Player]struct{}{} for _, u := range s.Units { - if u.Card.Name != "King" { + if u.card.Name != "King" { continue } - foundKings[u.Owner] = struct{}{} + foundKings[u.owner] = struct{}{} } if len(foundKings) == len(s.Players) { diff --git a/go/ui/hand.go b/go/ui/hand.go index 5421f6fb..f7157232 100644 --- a/go/ui/hand.go +++ b/go/ui/hand.go @@ -40,7 +40,7 @@ func NewHandView(x, y, width, height int, h *game.Hand, bottomAligned bool) *Han func (hv *HandView) render() *ebiten.Image { img := ebiten.NewImage(hv.Width, hv.Height) - handCards := hv.hand.GetCards() + handCards := hv.hand.Cards() nHandCards := len(handCards) for i, card := range handCards { cardImg := assets.GetCard(card.Path(), "en") @@ -92,7 +92,7 @@ func (hv *HandView) FindObjectAt(x, y int) interface{} { return nil } - return HandCard{hv.hand.GetCards()[i]} + return HandCard{hv.hand.Cards()[i]} } func (hv *HandView) setHighlights(cards []*game.Card) { @@ -104,6 +104,10 @@ func (hv *HandView) HighlightCard(card *game.Card) { hv.setHighlights([]*game.Card{card}) } +func (hv *HandView) AddHighlightCard(card *game.Card) { + hv.setHighlights(append(hv.highlights, card)) +} + func (hv *HandView) ClearHighlights() { hv.setHighlights([]*game.Card{}) } diff --git a/go/ui/mapView.go b/go/ui/mapView.go index c966f1a8..8c83bd41 100644 --- a/go/ui/mapView.go +++ b/go/ui/mapView.go @@ -35,7 +35,7 @@ func NewMapView(g *game.State) *MapView { return &vw } -func (mv *MapView) GetHeight() int { +func (mv *MapView) Height() int { return len(mv.gameState.Map.Tiles) * TILE_HEIGHT } @@ -133,9 +133,9 @@ func (vw *MapView) drawMapLayer(screen *ebiten.Image) { } func getPermanentSymbol(p game.Permanent, i int) *ebiten.Image { - permanentSymbol := assets.GetSymbol(strings.ToLower(p.GetCard().Name)) + permanentSymbol := assets.GetSymbol(strings.ToLower(p.Card().Name)) if permanentSymbol == nil { - symbol := fmt.Sprintf("%c%d", unicode.ToUpper(rune(p.GetCard().Type.String()[0])), i) + symbol := fmt.Sprintf("%c%d", unicode.ToUpper(rune(p.Card().Type.String()[0])), i) permanentSymbol = assets.GetGenericSymbol(symbol) if permanentSymbol == nil { log.Fatalf("Failed to generate generic symbol %s", symbol) @@ -151,13 +151,13 @@ func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) { for i, p := range vw.gameState.Permanents { permanentSymbol := getPermanentSymbol(p, i) - t := p.GetTile() + t := p.Tile() x_px := t.Position.X*TILE_WIDTH + (TILE_WIDTH-PERMANENT_WIDTH)/2 y_px := t.Position.Y*TILE_HEIGHT + (TILE_HEIGHT-PERMANENT_HEIGHT)/2 op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(x_px), float64(y_px)) - op.ColorM.ScaleWithColor(p.GetController().Color) + op.ColorM.ScaleWithColor(p.Controller().Color) for _, h := range vw.permanentsHighlights { if p == h { @@ -206,7 +206,7 @@ func (vw *MapView) FindObjectAt(screenX, screenY int) interface{} { relativeY >= yMargin && relativeY <= TILE_HEIGHT-yMargin { for _, p := range vw.gameState.Permanents { - pos := p.GetTile().Position + pos := p.Tile().Position if pos.X == x && pos.Y == y { if isSymbolTransparentAt(p, relativeX, relativeY) { break @@ -225,8 +225,12 @@ func (vw *MapView) FindObjectAt(screenX, screenY int) interface{} { } func (vw *MapView) HighlightPositions(pos []game.Position) { - vw.ForceRedraw() vw.tileHighlights = pos + vw.ForceRedraw() +} + +func (vw *MapView) AddHighlightPosition(pos game.Position) { + vw.HighlightPositions(append(vw.tileHighlights, pos)) } func (vw *MapView) HighlightTiles(tiles []*game.Tile) { @@ -241,13 +245,17 @@ func (vw *MapView) HighlightTile(t *game.Tile) { vw.HighlightTiles([]*game.Tile{t}) } +func (vw *MapView) AddHighlightTile(t *game.Tile) { + vw.AddHighlightPosition(t.Position) +} + func (vw *MapView) ClearTileHighlights() { vw.HighlightTiles([]*game.Tile{}) } func (vw *MapView) HighlightPermanents(permanents []game.Permanent) { - vw.ForceRedraw() vw.permanentsHighlights = permanents + vw.ForceRedraw() } func (vw *MapView) HighlightPermanent(p game.Permanent) { diff --git a/go/ui/prompt.go b/go/ui/prompt.go new file mode 100644 index 00000000..bfcd5d14 --- /dev/null +++ b/go/ui/prompt.go @@ -0,0 +1,78 @@ +package ui + +import ( + "log" + + "github.com/hajimehoshi/ebiten/v2" + + "muhq.space/muhqs-game/go/game" +) + +type promptType int + +const ( + handCard promptType = iota + + PROMPT_HEIGHT int = 50 +) + +type Prompt struct { + promptType promptType + y, width int + sel []interface{} + components []Widget +} + +func NewHandCardPrompt(y, width int, promptText string) *Prompt { + p := &Prompt{ + y: y, + width: width, + promptType: handCard, + components: []Widget{NewCenteringFixedTextBox(0, y, width, PROMPT_HEIGHT, promptText)}, + } + return p +} + +func (p *Prompt) Add(obj interface{}) { + var ok bool + switch p.promptType { + case handCard: + _, ok = obj.(HandCard) + } + + if !ok { + log.Fatalf("Invalid type %T for prompt %d", obj, p.promptType) + } + + p.sel = append(p.sel, obj) +} + +func (p *Prompt) Confirm() game.Action { + switch p.promptType { + case handCard: + cards := make([]*game.Card, 0, len(p.sel)) + for _, c := range p.sel { + cards = append(cards, c.(HandCard).C) + } + return game.NewHandCardSelection(cards) + } + + return nil +} + +func (p *Prompt) Height() int { + return PROMPT_HEIGHT +} + +func (p *Prompt) Draw(screen *ebiten.Image) { + for _, comp := range p.components { + comp.Draw(screen) + } +} + +func (p *Prompt) Contains(x, y int) bool { + return x >= 0 && y >= 0 && x <= p.width && y >= p.y && y <= p.y+PROMPT_HEIGHT +} + +func (p *Prompt) FindObjectAt(screenX, screenY int) interface{} { return nil } +func (*Prompt) Render(Widget) *ebiten.Image { return nil } diff --git a/go/ui/textBox.go b/go/ui/textBox.go index dd10e8e2..78199410 100644 --- a/go/ui/textBox.go +++ b/go/ui/textBox.go @@ -55,7 +55,7 @@ func NewCenteringAutoTextBox(x, y int, t string) *TextBox { func NewUnitInfo(x, y int, u *game.Unit) *TextBox { info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nUpkeep: %d", - u.String(), u.GetDamage(), u.Health, u.Upkeep) + u.String(), u.Damage(), u.Health, u.Upkeep) if u.Movement != game.INVALID_MOVEMENT() { info = fmt.Sprintf("%s\nMovement: %s", info, u.Movement.String()) @@ -69,8 +69,7 @@ func NewUnitInfo(x, y int, u *game.Unit) *TextBox { } func NewPermInfo(x, y int, p game.Permanent) *TextBox { - info := fmt.Sprintf("%s\nDamage: %d", - p.String(), p.GetDamage()) + info := fmt.Sprintf("%s\nDamage: %d", p.String(), p.Damage()) return NewAutoTextBox(x, y, info) } @@ -135,7 +134,7 @@ func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList { func (w *PocList) setText() { t := "" - for _, c := range w.poc.GetCards() { + for _, c := range w.poc.Cards() { t = fmt.Sprintf("%s%s\n", t, c.Name) } @@ -151,7 +150,7 @@ func (w *PocList) setText() { } func (w *PocList) FindObjectAt(x, y int) interface{} { - cards := w.poc.GetCards() + cards := w.poc.Cards() if len(cards) == 0 { return nil } -- cgit v1.2.3