aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-31 00:16:53 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2023-01-31 00:16:53 +0100
commitd76117cc913c738002c7562189ada75748645ff1 (patch)
tree0b0acd248870eb57eee272fa60905fdfc578dd14
parent37204e799318a8140372b1fa8e5265e02af3fe96 (diff)
downloadmuhqs-game-d76117cc913c738002c7562189ada75748645ff1.tar.gz
muhqs-game-d76117cc913c738002c7562189ada75748645ff1.zip
intermediate commit
-rw-r--r--go/client/main.go60
-rw-r--r--go/game/action.go59
-rw-r--r--go/game/ai.go36
-rw-r--r--go/game/artifact.go4
-rw-r--r--go/game/card.go157
-rw-r--r--go/game/cardImplementations.go31
-rw-r--r--go/game/cardParsing.go140
-rw-r--r--go/game/kraken.go42
-rw-r--r--go/game/map.go2
-rw-r--r--go/game/permanent.go114
-rw-r--r--go/game/pileOfCards.go4
-rw-r--r--go/game/player.go37
-rw-r--r--go/game/playerControl.go7
-rw-r--r--go/game/range.go2
-rw-r--r--go/game/state.go60
-rw-r--r--go/game/tile.go4
-rw-r--r--go/game/unit.go44
-rw-r--r--go/game/winCondition.go4
-rw-r--r--go/ui/hand.go8
-rw-r--r--go/ui/mapView.go24
-rw-r--r--go/ui/prompt.go78
-rw-r--r--go/ui/textBox.go9
22 files changed, 692 insertions, 234 deletions
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
}