aboutsummaryrefslogtreecommitdiff
path: root/go
diff options
context:
space:
mode:
Diffstat (limited to 'go')
-rw-r--r--go/assets/retrieve.go2
-rw-r--r--go/client/main.go385
-rw-r--r--go/font/font.go18
-rw-r--r--go/game/action.go110
-rw-r--r--go/game/card.go3
-rw-r--r--go/game/deck.go14
-rw-r--r--go/game/kraken.go86
-rw-r--r--go/game/map.go57
-rw-r--r--go/game/map_test.go4
-rw-r--r--go/game/movement.go11
-rw-r--r--go/game/phases.go (renamed from go/game/phase.go)14
-rw-r--r--go/game/pileOfCards.go4
-rw-r--r--go/game/player.go45
-rw-r--r--go/game/playerControl.go33
-rw-r--r--go/game/stack.go14
-rw-r--r--go/game/state.go258
-rw-r--r--go/game/tile.go12
-rw-r--r--go/game/unit.go33
-rw-r--r--go/ui/buffer.go4
-rw-r--r--go/ui/button.go14
-rw-r--r--go/ui/cardView.go46
-rw-r--r--go/ui/hand.go53
-rw-r--r--go/ui/mapView.go57
-rw-r--r--go/ui/stateBar.go40
-rw-r--r--go/ui/textBox.go133
-rw-r--r--go/ui/widget.go1
26 files changed, 1109 insertions, 342 deletions
diff --git a/go/assets/retrieve.go b/go/assets/retrieve.go
index cb062196..d9b40643 100644
--- a/go/assets/retrieve.go
+++ b/go/assets/retrieve.go
@@ -94,7 +94,7 @@ func GetGenericSymbol(symbol string) *ebiten.Image {
if !found {
genericBase := GetSymbol("generic_base")
img = ebiten.NewImageFromImage(genericBase)
- text.Draw(img, symbol, font.Font, 5, 27, color.Black)
+ text.Draw(img, symbol, font.Font24, 5, 27, color.Black)
}
return img
}
diff --git a/go/client/main.go b/go/client/main.go
index a3dbeeec..b8f1bf7a 100644
--- a/go/client/main.go
+++ b/go/client/main.go
@@ -19,6 +19,7 @@ const (
screenHeight = 1024
DEFAULT_BUTTON_HEIGHT = 40
+ PASS_BUTTON_WIDTH = 100
RESOLVE_BUTTON_X = 500
STACK_BUFFER_WIDTH = 300
@@ -27,6 +28,8 @@ const (
HAND_VIEW_HEIGHT = 500
HAND_VIEW_WIDTH = 500
+
+ STATE_BAR_WIDTH = 300
)
var playerColors = []color.RGBA{
@@ -34,10 +37,17 @@ var playerColors = []color.RGBA{
{1, 1, 0xff, 0xff},
}
+// A simple structure to detect if the cursor has not moved for HOVER_THRESHOLD
+// ticks.
+// hoverDetector.update must be called during each Update invocation to track the
+// frame and the current cursor position.
type hoverDetector struct {
last_x, last_y int
ticks int
- resetFunc func()
+ // ResetFunc is called when the cursor has moved.
+ // If resetFunc returns true the hoverDetector is reset
+ // otherwise the reset is skipped.
+ resetFunc func() bool
}
func (h *hoverDetector) update(x, y int) {
@@ -50,11 +60,11 @@ func (h *hoverDetector) update(x, y int) {
}
func (h *hoverDetector) reset() {
- h.ticks = 0
- if h.resetFunc != nil {
- h.resetFunc()
- h.resetFunc = nil
+ if h.resetFunc != nil && !h.resetFunc() {
+ return
}
+ h.ticks = 0
+ h.resetFunc = nil
}
func (h *hoverDetector) startedHovering() bool {
@@ -62,19 +72,28 @@ func (h *hoverDetector) startedHovering() bool {
}
type Game struct {
- gameState *game.State
- activePlayer *game.Player
- playerCtrl *game.ChanPlayerControl
- hasPriority bool
- handLayer *ui.HandView
- stateBar *ui.StateBar
- mapView *ui.MapView
- actionChoice *ui.Choice
- passButton *ui.SimpleButton
- stackBuffer *ui.Buffer
- widgets []ui.Widget
selectedObject interface{}
hoverDetector hoverDetector
+
+ handLayer *ui.HandView
+ stateBar *ui.StateBar
+ mapView *ui.MapView
+ actionChoice *ui.Choice
+ passButton *ui.SimpleButton
+ discardPileView *ui.PocList
+ discardPileButton *ui.SimpleButton
+ storeView *ui.PocList
+ storeButton *ui.SimpleButton
+ stackBuffer *ui.Buffer
+ widgets []ui.Widget
+
+ activePlayer *game.Player
+
+ playerCtrl *game.ChanPlayerControl
+ hasPriority bool
+
+ gameState *game.State
+ storesOnMap bool
}
func NewGame() *Game {
@@ -82,8 +101,12 @@ func NewGame() *Game {
g.widgets = []ui.Widget{}
g.gameState = game.NewState()
- g.passButton = ui.NewSimpleButton(screenWidth-100, screenHeight-DEFAULT_BUTTON_HEIGHT,
- 100, DEFAULT_BUTTON_HEIGHT, "Pass", func(*ui.SimpleButton) {
+ g.passButton = ui.NewSimpleButton(screenWidth-PASS_BUTTON_WIDTH,
+ screenHeight-DEFAULT_BUTTON_HEIGHT,
+ PASS_BUTTON_WIDTH,
+ DEFAULT_BUTTON_HEIGHT,
+ "Pass",
+ func(*ui.SimpleButton) {
if g.hasPriority {
g.passPriority()
}
@@ -107,6 +130,9 @@ func (g *Game) loadMap(mapName string) {
}
g.gameState.Map = m
g.mapView = ui.NewMapView(g.gameState)
+
+ storeTiles := m.FilterTiles(func(t *game.Tile) bool { return t.Type == game.TileTypes.Store })
+ g.storesOnMap = len(storeTiles) > 0
}
// func NewGameFromMapWithPlayers(mapName string, players []string, decks []*game.Deck) *Game {
@@ -120,9 +146,9 @@ func (g *Game) loadMap(mapName string) {
// return g
// }
-func (g *Game) addPlayer(name string, deck *game.Deck, store *game.Store) *game.Player {
+func (g *Game) addPlayer(name string, deck *game.Deck) *game.Player {
color := playerColors[len(g.gameState.Players)]
- g.gameState.AddNewPlayer(name, deck, game.NewStore(), color)
+ g.gameState.AddNewPlayer(name, deck, color)
return g.getPlayer(name)
}
@@ -139,33 +165,101 @@ func (g *Game) initPlayerUi(player *game.Player) {
g.activePlayer = player
g.playerCtrl = game.NewChanPlayerControl()
player.Ctrl = g.playerCtrl
+ var x, y int
+
+ g.handLayer = ui.NewHandView(0,
+ g.mapView.GetHeight(),
+ HAND_VIEW_WIDTH,
+ screenHeight-g.mapView.GetHeight()-DEFAULT_BUTTON_HEIGHT,
+ player.Hand,
+ true)
+ g.addWidget(g.handLayer)
+
+ g.stateBar = ui.NewStateBar(0,
+ screenHeight-DEFAULT_BUTTON_HEIGHT,
+ STATE_BAR_WIDTH,
+ DEFAULT_BUTTON_HEIGHT,
+ g.gameState,
+ player)
+ g.addWidget(g.stateBar)
+
+ g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width,
+ screenHeight-DEFAULT_BUTTON_HEIGHT,
+ 150,
+ DEFAULT_BUTTON_HEIGHT,
+ "DiscardPile",
+ func(*ui.SimpleButton) {
+ if idx := g.findWidget(g.discardPileView); idx == -1 {
+ g.discardPileView.ForceRedraw()
+ g.addWidget(g.discardPileView)
+ } else {
+ g.removeWidget(g.discardPileView)
+ }
+ })
+ g.addWidget(g.discardPileButton)
+
+ x = g.discardPileButton.X + g.discardPileButton.Width/2
+ y = screenHeight - DEFAULT_BUTTON_HEIGHT
+ g.discardPileView = ui.NewPocList(x, y, g.activePlayer.DiscardPile)
+
+ if !g.storesOnMap {
+ x = g.stateBar.Width + g.discardPileButton.Width
+ y = screenHeight - DEFAULT_BUTTON_HEIGHT
+ g.storeButton = ui.NewSimpleButton(x, y,
+ 150,
+ DEFAULT_BUTTON_HEIGHT,
+ "Store",
+ func(*ui.SimpleButton) {
+ if idx := g.findWidget(g.storeView); idx == -1 {
+ g.showStore()
+ } else {
+ g.hideStore()
+ }
+ })
+ g.addWidget(g.storeView)
- g.handLayer = ui.NewHandView(0, g.mapView.GetHeight(),
- HAND_VIEW_WIDTH, screenHeight-g.mapView.GetHeight()-DEFAULT_BUTTON_HEIGHT, player.Hand)
- g.widgets = append(g.widgets, g.handLayer)
+ x = g.storeButton.X + g.storeButton.Width/2
+ y = screenHeight - DEFAULT_BUTTON_HEIGHT
+ g.storeView = ui.NewPocList(x, y, g.activePlayer.Store)
+ }
+}
+
+func (g *Game) showStore() {
+ g.storeView.ForceRedraw()
+ g.addWidget(g.storeView)
+}
- g.stateBar = ui.NewStateBar(0, screenHeight-DEFAULT_BUTTON_HEIGHT,
- screenWidth-g.passButton.Width, DEFAULT_BUTTON_HEIGHT, g.gameState, player)
- g.widgets = append(g.widgets, g.stateBar)
+func (g *Game) hideStore() {
+ g.removeWidget(g.storeView)
}
func (g *Game) addWidget(w ui.Widget) {
g.widgets = append(g.widgets, w)
}
-func (g *Game) removeWidget(widget ui.Widget) {
- nwidgets := len(g.widgets)
+func (g *Game) findWidget(widget ui.Widget) int {
for i, w := range g.widgets {
if w != widget {
continue
}
+ return i
+ }
+ return -1
+}
- g.widgets[i] = g.widgets[nwidgets-1]
- g.widgets = g.widgets[:nwidgets-1]
- return
+func (g *Game) removeWidget(widget ui.Widget) {
+ idx := g.findWidget(widget)
+ if idx != -1 {
+ g.removeWidgetAt(idx)
}
}
+func (g *Game) removeWidgetAt(idx int) {
+ nwidgets := len(g.widgets)
+ g.widgets[idx] = g.widgets[nwidgets-1]
+ g.widgets = g.widgets[:nwidgets-1]
+}
+
func (g *Game) addActionChoice(perm game.Permanent, x, y int) {
actions := perm.GetAvailActionPrototypes()
if len(actions) > 0 {
@@ -193,6 +287,7 @@ func (g *Game) clearMapHighlights() {
func (g *Game) declareAction(a game.Action) {
g.playerCtrl.SendAction(a)
g.clearMapHighlights()
+ g.handLayer.ClearHighlights()
g.hasPriority = false
g.removeWidget(g.passButton)
g.stateBar.ForceRedraw()
@@ -209,20 +304,38 @@ func (g *Game) handlePlayerNotifications() {
case game.PriorityNotification:
g.addWidget(g.passButton)
g.hasPriority = true
+
case game.DeclaredActionNotification:
- g.stackBuffer.AddLine(n.Action.String())
+ if n.Error != nil {
+ log.Fatal(n.Error)
+ }
+ a := n.Context.(game.Action)
+ g.stackBuffer.AddLine(a.String())
+
case game.ResolvedActionNotification:
g.clearMapHighlights()
g.stackBuffer.RemoveLast()
g.mapView.ForceRedraw()
+
+ case game.BuyPrompt:
+ if !g.storesOnMap {
+ g.showStore()
+ }
}
+
+ g.stateBar.ForceRedraw()
+ g.handLayer.ForceRedraw()
+
n = g.playerCtrl.RecvNotification()
}
}
func (g *Game) findObjectAt(x, y int) interface{} {
- for _, b := range g.widgets {
- if obj := b.FindObjectAt(x, y); obj != nil {
+ // Iterate the widget in reverse order to ensure that objects are found
+ // in the newer widget possibly drawn over older ones
+ for i := len(g.widgets) - 1; i >= 0; i-- {
+ w := g.widgets[i]
+ if obj := w.FindObjectAt(x, y); obj != nil {
return obj
}
}
@@ -233,96 +346,137 @@ func (g *Game) findObjectAt(x, y int) interface{} {
return nil
}
-func (g *Game) Update() error {
- g.handlePlayerNotifications()
-
- x, y := ebiten.CursorPosition()
- g.hoverDetector.update(x, y)
-
- obj := g.findObjectAt(x, y)
-
- if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
- if obj == nil {
- fmt.Printf("No object found at cursor position (%d, %d)\n", x, y)
- return nil
- }
+func (g *Game) handleSelection(obj interface{}, x, y int) {
+ if obj == nil {
+ fmt.Printf("No object found at cursor position (%d, %d)\n", x, y)
+ return
+ }
- log.Printf("Active object is of type %T\n", g.selectedObject)
-
- switch obj := obj.(type) {
- case ui.Button:
- obj.Click(x, y)
-
- case *game.Tile:
- tile := obj
- if g.selectedObject != nil {
- var action game.Action
- switch obj := g.selectedObject.(type) {
- case *game.MoveActionPrototype:
- action = obj.Specialize(tile)
- case *game.Card:
- if obj.IsPermanent() {
- action = game.NewPlayAction(g.activePlayer, obj, tile.Position)
- g.handLayer.ForceRedraw()
- }
- }
- if action != nil {
- g.declareAction(action)
- g.selectedObject = nil
- g.mapView.HighlightTile(tile)
+ switch obj := obj.(type) {
+ case ui.Button:
+ obj.Click(x, y)
+
+ case *game.Tile:
+ tile := obj
+ if g.selectedObject != nil {
+ var action game.Action
+ switch obj := g.selectedObject.(type) {
+ case *game.MoveActionPrototype:
+ action = obj.Specialize(tile)
+ case *game.Card:
+ if obj.IsPermanent() {
+ action = game.NewPlayAction(g.activePlayer, obj, tile.Position)
}
- } else {
+ }
+ if action != nil {
+ g.declareAction(action)
+ g.selectedObject = nil
g.mapView.HighlightTile(tile)
}
+ } else {
+ g.mapView.HighlightTile(tile)
+ }
- case game.Permanent:
- perm := obj
- if g.selectedObject == nil {
- g.addActionChoice(perm, x, y)
- } else {
- switch obj := g.selectedObject.(type) {
- case *game.AttackActionPrototype:
- action := obj.Specialize(perm)
- g.selectedObject = nil
- g.declareAction(action)
- g.mapView.HighlightPermanent(perm)
- }
+ case game.Permanent:
+ perm := obj
+ if g.selectedObject == nil {
+ g.addActionChoice(perm, x, y)
+ } else {
+ switch obj := g.selectedObject.(type) {
+ case *game.AttackActionPrototype:
+ action := obj.Specialize(perm)
+ g.selectedObject = nil
+ g.declareAction(action)
+ g.mapView.HighlightPermanent(perm)
}
+ }
- case ui.HandCard:
- g.selectedObject = obj.C
+ case ui.HandCard:
+ g.selectedObject = obj.C
- default:
- log.Fatalf("Object of type %T not handled", obj)
+ case *game.Card:
+ if g.gameState.IsActivePlayer(g.activePlayer) &&
+ g.gameState.ActivePhase == game.Phases.BuyPhase {
+ g.declareAction(game.NewBuyAction(g.activePlayer, obj))
}
- switch obj := g.selectedObject.(type) {
- case *game.MoveActionPrototype:
- g.mapView.HighlightTiles(obj.Unit.MoveRangeTiles(g.gameState.Map))
- case *game.AttackActionPrototype:
- perms := obj.Unit.AttackablePermanents(g.gameState.Map)
- g.mapView.HighlightPermanents(perms)
- case *game.Card:
- if obj.IsPermanent() {
- spawnTiles := g.gameState.AvailableSpawnTiles(g.activePlayer, obj)
- g.mapView.HighlightTiles(spawnTiles)
- }
+ default:
+ log.Fatalf("Object of type %T not handled", obj)
+ }
+}
+
+func (g *Game) hightlightSelection(obj interface{}) {
+ switch obj := g.selectedObject.(type) {
+ case *game.MoveActionPrototype:
+ g.mapView.HighlightTiles(obj.Unit.MoveRangeTiles(g.gameState.Map))
+ case *game.AttackActionPrototype:
+ perms := obj.Unit.AttackablePermanents(g.gameState.Map)
+ g.mapView.HighlightPermanents(perms)
+ case *game.Card:
+ g.handLayer.HighlightCard(obj)
+ if obj.IsPermanent() {
+ spawnTiles := g.gameState.AvailableSpawnTiles(g.activePlayer, obj)
+ g.mapView.HighlightTiles(spawnTiles)
}
- } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil {
- var hoverHint *ui.TextBox
- switch obj := obj.(type) {
- case *game.Unit:
- hoverHint = ui.NewUnitInfo(x+ui.TILE_WIDTH, y, obj)
- case game.Permanent:
- hoverHint = ui.NewPermInfo(x+ui.TILE_WIDTH, y, obj)
+ }
+}
+
+func (g *Game) handleHover(obj interface{}, x, y int) {
+ var hoverHint ui.Widget
+ switch obj := obj.(type) {
+ case *game.Card:
+ wx, wy := x, y
+ if x+500 > screenWidth || y+700 > screenHeight {
+ wx = x - 500
+ wy = y - 700
+ }
+ hoverHint = ui.NewScaledCardView(wx, wy, 500, 700, obj.Path())
+ case *game.Unit:
+ hoverHint = ui.NewUnitInfo(x+ui.TILE_WIDTH, y, obj)
+ case game.Permanent:
+ 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)
+ 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.
+ g.hoverDetector.resetFunc = func() bool {
+ x, y := ebiten.CursorPosition()
+ if hoverHint.Contains(x, y) {
+ return false
+ }
+ g.removeWidget(hoverHint)
+ return true
+ }
}
+ }
- if hoverHint != nil {
- g.addWidget(hoverHint)
- g.hoverDetector.resetFunc = func() {
+ if hoverHint != nil {
+ g.addWidget(hoverHint)
+ // Set the default reset function removing the hoverHint
+ if g.hoverDetector.resetFunc == nil {
+ g.hoverDetector.resetFunc = func() bool {
g.removeWidget(hoverHint)
+ return true
}
}
+ }
+}
+
+func (g *Game) Update() error {
+ g.handlePlayerNotifications()
+
+ x, y := ebiten.CursorPosition()
+ g.hoverDetector.update(x, y)
+
+ obj := g.findObjectAt(x, y)
+
+ if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
+ g.handleSelection(obj, x, y)
+ g.hightlightSelection(obj)
+ } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil {
+ g.handleHover(obj, x, y)
} else if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
g.passButton.Click(0, 0)
} else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
@@ -330,18 +484,19 @@ func (g *Game) Update() error {
g.selectedObject = nil
g.hoverDetector.reset()
g.clearMapHighlights()
+ g.handLayer.ClearHighlights()
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
g.mapView.Draw(screen)
- for _, b := range g.widgets {
- b.Draw(screen)
- }
if !g.gameState.Stack.IsEmpty() {
g.stackBuffer.Draw(screen)
}
+ for _, b := range g.widgets {
+ b.Draw(screen)
+ }
ebitenutil.DebugPrint(screen,
fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()))
}
@@ -358,9 +513,11 @@ func main() {
g := NewGameFromMap("the-kraken")
- muhq := g.addPlayer("muhq",
- game.NewDeckFromDeckList(g.gameState.Map.StartDeckList),
- game.NewStore())
+ nautics := `1 nautics/barge
+1 nautics/captain
+1 nautics/fisher
+1 nautics/galley`
+ muhq := g.addPlayer("muhq", game.NewDeckFromDeckList(nautics))
g.initPlayerUi(muhq)
krakenId := len(g.gameState.Players)
diff --git a/go/font/font.go b/go/font/font.go
index 1ff9b64d..ae75cf8d 100644
--- a/go/font/font.go
+++ b/go/font/font.go
@@ -8,7 +8,9 @@ import (
"golang.org/x/image/font/opentype"
)
-var Font font.Face
+var Font24 font.Face
+var Font18 font.Face
+var Font12 font.Face
func InitFont() {
tt, err := opentype.Parse(goregular.TTF)
@@ -17,7 +19,19 @@ func InitFont() {
}
opts := &opentype.FaceOptions{Size: 24, DPI: 72}
- Font, err = opentype.NewFace(tt, opts)
+ Font24, err = opentype.NewFace(tt, opts)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ opts = &opentype.FaceOptions{Size: 18, DPI: 72}
+ Font18, err = opentype.NewFace(tt, opts)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ opts = &opentype.FaceOptions{Size: 12, DPI: 72}
+ Font12, err = opentype.NewFace(tt, opts)
if err != nil {
log.Fatal(err)
}
diff --git a/go/game/action.go b/go/game/action.go
index c121d0c8..f3b242b5 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -8,7 +8,6 @@ import (
type ActionPrototype interface {
Specialize(...interface{}) Action
String() string
- CanPayCosts(s *State) bool
}
type Action interface {
@@ -19,19 +18,15 @@ type Action interface {
type PassPriorityAction struct{ Source *Player }
-func (*PassPriorityAction) Declare(*State) {}
func (*PassPriorityAction) Resolve(*State) {}
func (*PassPriorityAction) PayCosts(*State) bool { return true }
func (*PassPriorityAction) String() string { return "pass" }
func NewPassPriorityAction(p *Player) Action { return &PassPriorityAction{p} }
-type FastAction interface {
- Action
-}
-
type ActionBase struct {
Source interface{}
Card *Card
+ Targets interface{}
resolveFunc func(s *State)
costFunc func(s *State) bool
desc string
@@ -59,16 +54,16 @@ func (a *ActionBase) String() string {
type PlayAction struct {
ActionBase
- targets interface{}
}
func NewPlayAction(p *Player, c *Card, targets interface{}) Action {
costFunc := func(*State) bool {
cost := c.GetPlayCosts()
- if p.Ressource < cost {
+
+ if p.Resource < cost {
return false
}
- p.Ressource -= cost
+ p.Resource -= cost
return true
}
return NewPlayActionWithSpecialCosts(p, c, targets, costFunc)
@@ -83,8 +78,12 @@ func NewPlayActionWithSpecialCosts(p *Player, c *Card, targets interface{},
s.ResolvePlay(p, c, targets)
}
- desc := fmt.Sprintf("p %s@%v", c.Name, targets)
- return &PlayAction{ActionBase{p, c, resolveFunc, costFunc, desc}, targets}
+ desc := fmt.Sprintf("play %s@%v", c.Name, targets)
+ return &PlayAction{ActionBase{p, c, targets, resolveFunc, costFunc, desc}}
+}
+
+type MoveAction struct {
+ ActionBase
}
type MoveActionPrototype struct {
@@ -95,10 +94,6 @@ func NewMoveActionPrototype(u *Unit) *MoveActionPrototype {
return &MoveActionPrototype{u}
}
-func (proto *MoveActionPrototype) CanPayCosts(s *State) bool {
- return proto.Unit.AvailMoveActions > 0
-}
-
func (proto *MoveActionPrototype) String() string {
return fmt.Sprintf("move %s", proto.Unit.Movement.String())
}
@@ -112,7 +107,6 @@ func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action {
}
costFunc := func(*State) bool {
- log.Printf("Move Actions of %s: %d\n", u.GetCard().Name, u.AvailMoveActions)
if u.AvailMoveActions < 1 {
return false
}
@@ -121,15 +115,15 @@ func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action {
}
desc := fmt.Sprintf("-> %s", target.Position.String())
- return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc}
+ return &MoveAction{ActionBase{u, u.GetCard(), target, resolveFunc, costFunc, desc}}
}
-type AttackActionPrototype struct {
- Unit *Unit
+type AttackAction struct {
+ ActionBase
}
-func (proto *AttackActionPrototype) CanPayCosts(s *State) bool {
- return proto.Unit.AvailAttackActions > 0
+type AttackActionPrototype struct {
+ Unit *Unit
}
func NewAttackActionPrototype(u *Unit) *AttackActionPrototype {
@@ -157,7 +151,7 @@ func (proto *AttackActionPrototype) Specialize(args ...interface{}) Action {
}
desc := fmt.Sprintf("x %s", target.GetTile().Position.String())
- return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc}
+ return &AttackAction{ActionBase{u, u.GetCard(), target, resolveFunc, costFunc, desc}}
}
type FullActionPrototype struct {
@@ -168,10 +162,6 @@ func NewFullActionPrototype(u *Unit) *FullActionPrototype {
return &FullActionPrototype{u}
}
-func (proto *FullActionPrototype) CanPayCosts(s *State) bool {
- return proto.Unit.AvailAttackActions > 0 && proto.Unit.AvailMoveActions > 0
-}
-
func (proto *FullActionPrototype) String() string {
return "full_action"
}
@@ -188,7 +178,7 @@ func (proto *FullActionPrototype) Specialize(args ...interface{}) Action {
return true
}
desc := "full_action"
- return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc}
+ return &ActionBase{u, u.GetCard(), nil, resolveFunc, costFunc, desc}
}
type FreeActionPrototype struct {
@@ -196,10 +186,6 @@ type FreeActionPrototype struct {
canPayFunc func(s *State) bool
}
-func (proto *FreeActionPrototype) CanPayCosts(s *State) bool {
- return proto.canPayFunc(s)
-}
-
func (proto *FreeActionPrototype) String() string {
return "free_action"
}
@@ -209,9 +195,69 @@ func (proto *FreeActionPrototype) Specialize(args ...interface{}) Action {
resolveFunc := args[0].(func(s *State))
costFunc := args[1].(func(s *State) bool)
desc := "free_action"
- return &FreeAction{ActionBase{p, p.GetCard(), resolveFunc, costFunc, desc}}
+ return &FreeAction{ActionBase{p, p.GetCard(), nil, resolveFunc, costFunc, desc}}
}
type FreeAction struct {
ActionBase
}
+
+type BuyAction struct {
+ player *Player
+ card *Card
+}
+
+func (a *BuyAction) Resolve(s *State) {
+ log.Println("Move", a.card.Name, "into", a.player.Name, "discard pile")
+ a.player.Store.MoveCard(a.card, a.player.DiscardPile)
+}
+
+func (a *BuyAction) PayCosts(*State) bool {
+ if a.card.BuyCost < 0 || a.card.BuyCost > a.player.Resource {
+ return false
+ }
+
+ a.player.Resource -= a.card.BuyCost
+ return true
+}
+
+func (a *BuyAction) String() string { return fmt.Sprintf("%s buy %s", a.player.Name, a.card.Name) }
+
+func NewBuyAction(p *Player, c *Card) *BuyAction {
+ return &BuyAction{p, c}
+}
+
+type UpkeepAction struct {
+ player *Player
+ unit *Unit
+ pay bool
+}
+
+func (a *UpkeepAction) Resolve(s *State) {
+ if a.pay {
+ a.player.Resource -= a.unit.Upkeep
+ } else {
+ s.DestroyPermanent(a.unit)
+ }
+}
+
+func (a *UpkeepAction) PayCosts(*State) bool {
+ if a.unit.Upkeep > a.player.Resource {
+ return false
+ }
+
+ a.player.Resource -= a.unit.Upkeep
+ return true
+}
+
+func (a *UpkeepAction) String() string {
+ if a.pay {
+ return fmt.Sprintf("%s keep %s", a.player.Name, a.unit.Tile.Position.String())
+ }
+
+ return fmt.Sprintf("%s disband %s", a.player.Name, a.unit.Tile.Position.String())
+}
+
+func NewUpkeepAction(p *Player, u *Unit, pay bool) *UpkeepAction {
+ return &UpkeepAction{p, u, pay}
+}
diff --git a/go/game/card.go b/go/game/card.go
index 6f8a74a8..07ef033b 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -223,7 +223,8 @@ func NewCard(cardPath string) *Card {
}
func (c *Card) Path() string {
- return path.Join(c.Set.String(), strings.ToLower(c.Name))
+ fileName := strings.ReplaceAll(strings.ToLower(c.Name), " ", "_")
+ return path.Join(c.Set.String(), fileName)
}
func (c *Card) IsPermanent() bool {
diff --git a/go/game/deck.go b/go/game/deck.go
index f1290fa4..569a952d 100644
--- a/go/game/deck.go
+++ b/go/game/deck.go
@@ -34,6 +34,20 @@ func (d *Deck) Draw(amount int) []*Card {
return drawn
}
+func (d *Deck) DrawOne() *Card {
+ drawn := d.Draw(1)
+ if len(drawn) == 0 {
+ return nil
+ }
+ return drawn[0]
+}
+
+func NewDeckFrom(cards PileOfCards) *Deck {
+ d := NewDeck()
+ cards.MoveInto(d)
+ return d
+}
+
func NewDeckFromCardPaths(cardPaths []string) *Deck {
d := NewDeck()
for _, cardPath := range cardPaths {
diff --git a/go/game/kraken.go b/go/game/kraken.go
index b0108361..ef63bbd0 100644
--- a/go/game/kraken.go
+++ b/go/game/kraken.go
@@ -7,43 +7,42 @@ import (
"sync"
)
-// var KrakenDecklist = `2 kraken/angry_squid
-// 2 kraken/chachalot
-// 2 kraken/clownfish
-// 2 kraken/deja_vu!
-// 2 kraken/devour_the_poor!
-// 2 kraken/dolphin
-// 2 kraken/flying_dutchmen
-// 2 kraken/frost_pylon
-// 2 kraken/gigantic_hail
-// 2 kraken/sailfish
-// 2 kraken/sold_out!
-// 2 kraken/suprise!
-// 2 kraken/tentacle_slap
-// 2 kraken/tides_change!
-// 2 kraken/unholy_cannonball`
-
-var KrakenDecklist = `1 kraken/angry_squid`
+const (
+ KRAKEN_NAME = "Kraken"
+)
+
+var KrakenDecklist = `2 kraken/angry_squid
+2 kraken/chachalot
+2 kraken/clownfish
+2 kraken/deja_vu!
+2 kraken/devour_the_poor!
+2 kraken/dolphin
+2 kraken/flying_dutchmen
+2 kraken/frost_pylon
+2 kraken/gigantic_hail
+2 kraken/sailfish
+2 kraken/sold_out!
+2 kraken/suprise!
+2 kraken/tentacle_slap
+2 kraken/tides_change!
+2 kraken/unholy_cannonball`
func NewKraken(id int, s *State, c color.Color) *Player {
+ kraken := NewPlayer(id, KRAKEN_NAME, NewDeck(), s, c)
d := NewDeckFromDeckList(KrakenDecklist)
- kraken := NewPlayer(id, "The Kraken", d, NewStore(), s, c)
d.Shuffle(s.Rand)
+ kraken.Deck = d
kraken.DrawPerTurn = 0
- the_kraken := NewUnitFromPath("kraken/the_kraken", s.Map.TileAt(Position{3, 1}), kraken)
- s.AddPermanent(the_kraken)
+ kraken_tile := s.Map.FilterTiles(func(t *Tile) bool { return t.Raw == "the kraken" })
+ if len(kraken_tile) != 1 {
+ log.Fatal("No kraken tile found in Map")
+ }
- ctrl := &KrakenControl{kraken, make(chan Action), new(sync.WaitGroup)}
- ctrl.syncGameState.Add(1)
+ s.AddPermanent(NewUnitFromPath("kraken/the_kraken", kraken_tile[0], kraken))
- kraken.Ctrl = ctrl
- go func() {
- for len(s.Map.WinCondition(s)) == 0 {
- ctrl.KrakenTurn()
- }
- }()
+ addKrakenControl(kraken)
return kraken
}
@@ -54,12 +53,25 @@ type KrakenControl struct {
syncGameState *sync.WaitGroup
}
+func addKrakenControl(kraken *Player) {
+ ctrl := &KrakenControl{kraken, make(chan Action), new(sync.WaitGroup)}
+ kraken.Ctrl = ctrl
+
+ ctrl.syncGameState.Add(1)
+ go func() {
+ s := kraken.gameState
+ for len(s.Map.WinCondition(s)) == 0 {
+ ctrl.krakenTurn()
+ }
+ }()
+}
+
func (ctrl *KrakenControl) awaitGameStateSync() {
ctrl.syncGameState.Wait()
ctrl.syncGameState.Add(1)
}
-func (ctrl *KrakenControl) KrakenTurn() {
+func (ctrl *KrakenControl) krakenTurn() {
s := ctrl.kraken.gameState
kraken := ctrl.kraken
@@ -78,15 +90,15 @@ func (ctrl *KrakenControl) KrakenTurn() {
continue
}
- if c.BuyCost > kraken.Ressource {
- log.Println("Kraken discards", c.Name, "because ressource to low", kraken.Ressource)
+ if c.BuyCost > kraken.Resource {
+ log.Println("Kraken discards", c.Name, "because resource to low", kraken.Resource)
kraken.DiscardPile.AddCard(c)
break
}
kraken.Hand.AddCard(c)
costFunc := func(s *State) bool {
- kraken.Ressource -= c.BuyCost
+ kraken.Resource -= c.BuyCost
return true
}
@@ -130,8 +142,8 @@ func (ctrl *KrakenControl) KrakenTurn() {
func (ctrl *KrakenControl) RecvAction() Action {
s := ctrl.kraken.gameState
kraken := ctrl.kraken
- if s.ActivePlayerId != kraken.Id ||
- s.ActivePhase != actionPhase || !s.Stack.IsEmpty() {
+ if !s.IsActivePlayer(kraken) ||
+ s.ActivePhase != Phases.ActionPhase || !s.Stack.IsEmpty() {
return NewPassPriorityAction(ctrl.kraken)
}
@@ -143,9 +155,13 @@ func (ctrl *KrakenControl) RecvAction() Action {
func (ctrl *KrakenControl) SendNotification(n PlayerNotification) {
}
+func (p *Player) IsKraken() bool {
+ return p.Name == KRAKEN_NAME
+}
+
func (s *State) FindKraken() *Player {
for _, p := range s.Players {
- if p.Name == "The Kraken" {
+ if p.IsKraken() {
return p
}
}
diff --git a/go/game/map.go b/go/game/map.go
index 72e17ad4..cfabfaf9 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -19,8 +19,8 @@ const (
BASE_URL string = "localhost:8000/"
MAP_URL_PART string = "html/build/maps"
- DEFAULT_RESSOURCE_GAIN int = 5
- DEFAULT_START_DECK string = "3 misc/farmer"
+ DEFAULT_RESOURCE_GAIN int = 5
+ DEFAULT_START_DECK string = "3 misc/farmer"
)
type MapYml struct {
@@ -36,9 +36,10 @@ func DummyWinCondition(*State) []*Player {
type Map struct {
Tiles [][]Tile
symbols map[string]string
- RessourceGain int
+ ResourceGain int
StartDeckList string
WinCondition func(*State) []*Player
+ Stores map[Position]*Store
}
func readMap(r io.Reader) (*Map, error) {
@@ -59,7 +60,14 @@ func ParseMap(data []byte) (*Map, error) {
mapYml.Symbols[" "] = "neutral"
- m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK, DummyWinCondition}
+ m := &Map{
+ Tiles: [][]Tile{},
+ symbols: mapYml.Symbols,
+ ResourceGain: DEFAULT_RESOURCE_GAIN,
+ StartDeckList: DEFAULT_START_DECK,
+ WinCondition: DummyWinCondition,
+ Stores: make(map[Position]*Store),
+ }
if mapYml.StartDeckList != "" {
m.StartDeckList = mapYml.StartDeckList
@@ -70,11 +78,17 @@ func ParseMap(data []byte) (*Map, error) {
row := rows[y]
m.Tiles = append(m.Tiles, []Tile{})
for x := 0; x < len(rows[y]); x++ {
+ pos := Position{x, y}
s := string(row[x])
- tile, err := NewTileFromString(mapYml.Symbols[s], Position{x, y})
+ tile, err := NewTileFromString(mapYml.Symbols[s], pos)
if err != nil {
return nil, err
}
+
+ if tile.Type == TileTypes.Store {
+ m.Stores[pos] = NewStore()
+ }
+
m.Tiles[y] = append(m.Tiles[y], tile)
}
}
@@ -254,3 +268,36 @@ func (m *Map) FilterTiles(filter func(t *Tile) bool) []*Tile {
func (m *Map) AllTiles() []*Tile {
return m.FilterTiles(func(*Tile) bool { return true })
}
+
+func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) {
+ nStores := len(m.Stores)
+ stores := make([]*Store, 0, nStores)
+ for _, store := range m.Stores {
+ stores = append(stores, store)
+ }
+
+ d := NewDeckFrom(cards)
+ d.Shuffle(rand)
+ nCards := d.Size()
+
+ for i := 0; i < nCards; i++ {
+ c := d.DrawOne()
+ stores[i%nStores].AddCard(c)
+ }
+}
+
+func (m *Map) redistributeStoreCards(cards PileOfCards, rand *rand.Rand) {
+ poc := NewPileOfCards()
+ for _, store := range m.Stores {
+ store.MoveInto(poc)
+ }
+ m.distributeStoreCards(poc, rand)
+}
+
+func (m *Map) HasStores() bool {
+ return len(m.Stores) > 0
+}
+
+func (m *Map) GetStoreOn(p Position) *Store {
+ return m.Stores[p]
+}
diff --git a/go/game/map_test.go b/go/game/map_test.go
index 96efc459..6972f867 100644
--- a/go/game/map_test.go
+++ b/go/game/map_test.go
@@ -68,8 +68,8 @@ kings:
}
}
- if m.RessourceGain != DEFAULT_RESSOURCE_GAIN {
- t.Fatalf("ressource gain is not the default one")
+ if m.ResourceGain != DEFAULT_RESOURCE_GAIN {
+ t.Fatalf("resource gain is not the default one")
}
}
diff --git a/go/game/movement.go b/go/game/movement.go
index df7e4138..1a3f03c4 100644
--- a/go/game/movement.go
+++ b/go/game/movement.go
@@ -17,11 +17,16 @@ func INVALID_MOVEMENT() Movement {
}
func parseMovement(desc interface{}) Movement {
- if r, ok := desc.(int); ok {
- return Movement{r, false}
+ var movementStr string
+ switch typedDesc := desc.(type) {
+ case int:
+ return Movement{typedDesc, false}
+ case string:
+ movementStr = typedDesc
+ case map[string]interface{}:
+ movementStr = typedDesc["en"].(string)
}
- movementStr := desc.(string)
tokens := strings.Split(movementStr, " ")
r, err := strconv.Atoi(tokens[0])
if err != nil {
diff --git a/go/game/phase.go b/go/game/phases.go
index efcd430a..ea239278 100644
--- a/go/game/phase.go
+++ b/go/game/phases.go
@@ -26,3 +26,17 @@ func (p PhaseType) String() string {
return ""
}
+
+var Phases = struct {
+ DrawPhase PhaseType
+ UpkeepPhase PhaseType
+ ActionPhase PhaseType
+ BuyPhase PhaseType
+ DiscardPhase PhaseType
+}{
+ DrawPhase: drawPhase,
+ UpkeepPhase: upkeepPhase,
+ ActionPhase: actionPhase,
+ BuyPhase: buyPhase,
+ DiscardPhase: discardPhase,
+}
diff --git a/go/game/pileOfCards.go b/go/game/pileOfCards.go
index 807235a5..457f804a 100644
--- a/go/game/pileOfCards.go
+++ b/go/game/pileOfCards.go
@@ -15,6 +15,10 @@ type PileOfCardsBase struct {
cards []*Card
}
+func NewPileOfCards() *PileOfCardsBase {
+ return &PileOfCardsBase{[]*Card{}}
+}
+
func (poc *PileOfCardsBase) Size() int {
return len(poc.cards)
}
diff --git a/go/game/player.go b/go/game/player.go
index 017e8407..608fc7e9 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -2,7 +2,6 @@ package game
import (
"image/color"
- "log"
)
const (
@@ -13,7 +12,7 @@ type Player struct {
Id int
Name string
Turn int
- Ressource int
+ Resource int
DrawPerTurn int
Hand *Hand
DiscardPile *DiscardPile
@@ -24,29 +23,39 @@ type Player struct {
Ctrl PlayerControl
}
-func NewPlayer(id int, name string,
- deck *Deck, store *Store,
+func NewPlayer(id int, name string, deck *Deck,
gameState *State, color color.Color,
) *Player {
+ store := NewStore()
+ deck.MoveInto(store)
+
p := Player{
- id, name,
- 0, 0, MAX_DRAW,
- NewHand(), NewDiscardPile(), deck, store,
- gameState, color, nil,
+ Id: id,
+ Name: name,
+ Turn: 0,
+ Resource: 0,
+ DrawPerTurn: MAX_DRAW,
+ Hand: NewHand(),
+ DiscardPile: NewDiscardPile(),
+ Deck: NewDeckFromDeckList(gameState.Map.StartDeckList),
+ Store: store,
+ gameState: gameState,
+ Color: color,
+ Ctrl: nil,
}
return &p
}
func (p *Player) resourceGain() int {
if p.Name == "The kraken" {
- return p.gameState.Map.RessourceGain * (len(p.gameState.Players) - 1)
+ return p.gameState.Map.ResourceGain * (len(p.gameState.Players) - 1)
} else {
- return p.gameState.Map.RessourceGain
+ return p.gameState.Map.ResourceGain
}
}
func (p *Player) Upkeep() {
- p.Ressource += p.resourceGain()
+ p.Resource += p.resourceGain()
// TODO: handle upkeep triggers
@@ -56,7 +65,8 @@ func (p *Player) Upkeep() {
}
// TODO: let players decide if they want to pay the upkeep
- p.Ressource -= unit.Upkeep
+ p.Resource -= unit.Upkeep
+
// TODO: better refresh unit actions
unit.AvailAttackActions = 1
unit.AvailMoveActions = 1
@@ -65,8 +75,7 @@ func (p *Player) Upkeep() {
func (p *Player) ActionPhase() {
for {
- a := PromptAction(p.Ctrl)
- log.Printf("Received %T action from %s\n", a, p.Name)
+ a := promptAction(p.Ctrl)
if _, ok := a.(*PassPriorityAction); ok {
return
}
@@ -76,11 +85,17 @@ func (p *Player) ActionPhase() {
}
func (p *Player) buy(c *Card) {
- p.Ressource -= c.BuyCost
+ p.Resource -= c.BuyCost
p.DiscardPile.AddCard(c)
}
func (p *Player) BuyPhase() {
+ a := promptBuy(p.Ctrl)
+ if _, ok := a.(*PassPriorityAction); ok {
+ return
+ }
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
// TODO: select card to buy
}
diff --git a/go/game/playerControl.go b/go/game/playerControl.go
index 6d5e8e14..5e1552f0 100644
--- a/go/game/playerControl.go
+++ b/go/game/playerControl.go
@@ -6,6 +6,8 @@ const (
DeclaredActionNotification = iota
ResolvedActionNotification
PriorityNotification
+ UpkeepPrompt
+ BuyPrompt
)
func (n PlayerNotification) IsPriorityNotification() bool {
@@ -14,19 +16,28 @@ func (n PlayerNotification) IsPriorityNotification() bool {
type PlayerNotification struct {
Notification PlayerNotificationType
- Action Action
+ Context interface{}
+ Error error
}
func NewPriorityNotification() PlayerNotification {
- return PlayerNotification{PriorityNotification, nil}
+ return PlayerNotification{PriorityNotification, nil, nil}
}
-func NewDeclaredActionNotification(a Action) PlayerNotification {
- return PlayerNotification{DeclaredActionNotification, a}
+func NewDeclaredActionNotification(a Action, err error) PlayerNotification {
+ return PlayerNotification{DeclaredActionNotification, a, err}
}
-func NewResolvedActionNotification(a Action) PlayerNotification {
- return PlayerNotification{ResolvedActionNotification, a}
+func NewResolvedActionNotification(a Action, err error) PlayerNotification {
+ return PlayerNotification{ResolvedActionNotification, a, err}
+}
+
+func NewUpkeepPrompt(u *Unit) PlayerNotification {
+ return PlayerNotification{ResolvedActionNotification, u, nil}
+}
+
+func NewBuyPrompt() PlayerNotification {
+ return PlayerNotification{BuyPrompt, nil, nil}
}
type PlayerControl interface {
@@ -62,7 +73,15 @@ func NewChanPlayerControl() *ChanPlayerControl {
return &ChanPlayerControl{a, n}
}
-func PromptAction(ctrl PlayerControl) Action {
+func prompt(ctrl PlayerControl, notification PlayerNotification) Action {
ctrl.SendNotification(NewPriorityNotification())
return ctrl.RecvAction()
}
+
+func promptBuy(ctrl PlayerControl) Action {
+ return prompt(ctrl, NewBuyPrompt())
+}
+
+func promptAction(ctrl PlayerControl) Action {
+ return prompt(ctrl, NewPriorityNotification())
+}
diff --git a/go/game/stack.go b/go/game/stack.go
index 45ba4072..9132ae13 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -18,7 +18,6 @@ func (s *Stack) IsEmpty() bool {
}
func (s *Stack) Push(a Action) {
- s.gameState.broadcastNotification(NewDeclaredActionNotification(a))
s.Actions = append(s.Actions, a)
}
@@ -27,16 +26,21 @@ func (s *Stack) Pop() {
if l == 0 {
log.Fatalf("Can not pop from empty stack")
}
+
a := s.Actions[l-1]
s.Actions = s.Actions[:l-1]
- a.Resolve(s.gameState)
- s.gameState.broadcastNotification(NewResolvedActionNotification(a))
+
+ err := s.gameState.ValidateAction(a)
+ if err == nil {
+ a.Resolve(s.gameState)
+ }
+ log.Println("Resolved", a, err)
+ s.gameState.broadcastNotification(NewResolvedActionNotification(a, err))
}
func (s *Stack) Resolve() {
for !s.IsEmpty() {
- log.Println("Resolve Stack")
- s.gameState.AllPassing()
+ s.gameState.AllPassing(false)
s.Pop()
}
}
diff --git a/go/game/state.go b/go/game/state.go
index 29267c28..97283be4 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -1,28 +1,31 @@
package game
import (
+ "fmt"
"image/color"
"log"
"math/rand"
"strconv"
"strings"
"time"
+
+ "golang.org/x/exp/slices"
)
type State struct {
- Decks []*Deck
- Stores []*Store
- Map *Map
- Stack *Stack
- Hands []*Hand
- DiscardPiles []*DiscardPile
- Players []*Player
- ActivePlayerId int
- ActivePhase PhaseType
- Permanents []Permanent
- Units []*Unit
- Artifacts []*Artifact
- Rand *rand.Rand
+ Decks []*Deck
+ Stores []*Store
+ Map *Map
+ Stack *Stack
+ Hands []*Hand
+ DiscardPiles []*DiscardPile
+ Players []*Player
+ ActivePlayer *Player
+ ActivePhase PhaseType
+ Permanents []Permanent
+ Units []*Unit
+ Artifacts []*Artifact
+ Rand *rand.Rand
}
func NewState() *State {
@@ -32,16 +35,33 @@ func NewState() *State {
return s
}
-func (s *State) AddNewPlayer(name string, deck *Deck, store *Store, color color.Color) {
- p := NewPlayer(len(s.Players)+1, name, deck, store, s, color)
+func (s *State) AddNewPlayer(name string, deck *Deck, color color.Color) {
+ p := NewPlayer(len(s.Players)+1, name, deck, s, color)
s.Players = append(s.Players, p)
}
+func (s *State) IsActivePlayer(p *Player) bool {
+ return s.ActivePlayer == p
+}
+
func (s *State) Loop() []*Player {
winners := []*Player{}
+
+ if s.Map.HasStores() {
+ poc := NewPileOfCards()
+ for _, p := range s.Players {
+ if p.IsKraken() {
+ continue
+ }
+
+ p.Store.MoveInto(poc)
+ }
+ s.Map.distributeStoreCards(poc, s.Rand)
+ }
+
for len(winners) == 0 {
- for i, p := range s.Players {
- s.ActivePlayerId = i
+ for _, p := range s.Players {
+ s.ActivePlayer = p
p.Turn++
if p.Deck.IsEmpty() {
@@ -50,22 +70,22 @@ func (s *State) Loop() []*Player {
p.Deck.Shuffle(s.Rand)
}
- s.ActivePhase = drawPhase
+ s.ActivePhase = Phases.DrawPhase
p.Draw()
- s.ActivePhase = upkeepPhase
+ s.ActivePhase = Phases.UpkeepPhase
p.Upkeep()
- s.PhaseChange(i)
+ s.PhaseChange()
- s.ActivePhase = actionPhase
+ s.ActivePhase = Phases.ActionPhase
p.ActionPhase()
- s.PhaseChange(i)
+ s.PhaseChange()
- s.ActivePhase = buyPhase
+ s.ActivePhase = Phases.BuyPhase
p.BuyPhase()
- s.PhaseChange(i)
+ s.PhaseChange()
- s.ActivePhase = discardPhase
+ s.ActivePhase = Phases.DiscardPhase
p.DiscardPhase()
winners = s.Map.WinCondition(s)
@@ -75,38 +95,152 @@ func (s *State) Loop() []*Player {
return winners
}
+func (s *State) IsValidPlay(a *PlayAction) error {
+ if s.ActivePhase != Phases.ActionPhase {
+ log.Fatalf("Play actions can only be declared during one's actions phase")
+ }
+
+ if a.Card.IsPermanent() {
+ pos := a.Targets.(Position)
+ spawn := s.Map.TileAt(pos)
+ if !slices.Contains(s.AvailableSpawnTiles(a.Source.(*Player), a.Card), spawn) {
+ return fmt.Errorf("Spawn %v is not a valid spawn tile for %s",
+ pos.String(), a.Card.Name)
+ }
+ }
+
+ return nil
+}
+
+func (s *State) IsValidMove(a *MoveAction) error {
+ if s.ActivePhase != Phases.ActionPhase {
+ return fmt.Errorf("Move actions can only be declared during one's actions phase")
+ }
+
+ u := a.Source.(*Unit)
+ tile := a.Targets.(*Tile)
+
+ if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
+ return fmt.Errorf("Unit %s at %s can not move to %s",
+ u.Card.Name, u.Tile.Position.String(), tile.Position.String())
+ }
+
+ if tile.Permanent != nil {
+ return fmt.Errorf("Unit %s at %s can not move to occupied %s",
+ u.Card.Name, u.Tile.Position.String(), tile.Position.String())
+ }
+
+ return nil
+}
+
+func (s *State) IsValidAttack(a *AttackAction) error {
+ if s.ActivePhase != Phases.ActionPhase {
+ return fmt.Errorf("Attack actions can only be declared during one's actions phase")
+ }
+
+ unit := a.Source.(Unit)
+ target := a.Targets.(Permanent)
+ if !target.Attackable() {
+ return fmt.Errorf("Target %s is not attackable")
+ }
+
+ if !IsPositionInRange(unit.Tile.Position, target.GetTile().Position, unit.Attack.Range) {
+ return fmt.Errorf("Move actions can only be declared during one's actions phase")
+ }
+
+ // TODO: check move
+
+ return nil
+}
+
+func (s *State) IsValidBuy(a *BuyAction) error {
+ if s.Map.HasStores() {
+ var storeTiles []*Tile
+ for pos, store := range s.Map.Stores {
+ if slices.Contains(store.cards, a.card) {
+ storeTiles = append(storeTiles, s.Map.TileAt(pos))
+ }
+ }
+
+ if storeTiles == nil {
+ return fmt.Errorf("no store contains %s", a.card.Name)
+ }
+
+ controlsUnitOnStore := false
+ for _, tile := range storeTiles {
+ controlsUnitOnStore = controlsUnitOnStore || (tile.Permanent != nil &&
+ tile.Permanent.GetCard().Type == CardTypes.Unit &&
+ tile.Permanent.GetController() == a.player)
+ }
+ if !controlsUnitOnStore {
+ return fmt.Errorf("%s' controls no unit on a store containing %s",
+ a.player.Name, a.card.Name)
+ }
+
+ } else if !slices.Contains(a.player.Store.cards, a.card) {
+ return fmt.Errorf("%s's store does not contain %s", a.player.Name, a.card.Name)
+ }
+
+ return nil
+}
+
+func (s *State) ValidateAction(a Action) error {
+ var err error = nil
+ switch a := a.(type) {
+ case *PlayAction:
+ err = s.IsValidPlay(a)
+ case *MoveAction:
+ err = s.IsValidMove(a)
+ case *AttackAction:
+ err = s.IsValidAttack(a)
+ case *BuyAction:
+ err = s.IsValidBuy(a)
+ }
+ return err
+}
+
func (s *State) PushAction(a Action) {
if !a.PayCosts(s) {
log.Fatalf("Cost for %s could not be paid", a)
}
+
s.Stack.Push(a)
}
func (s *State) DeclareAction(a Action) {
- if !s.Stack.IsEmpty() {
- log.Fatalf("Slow action can not be declared while other actions are on the stack")
+ var err error = nil
+ switch a.(type) {
+ case *BuyAction:
+ if s.ActivePhase != Phases.BuyPhase {
+ err = fmt.Errorf("Cards can only be bought during one's buy phase")
+ }
+ case *FreeAction:
+ default:
+ if !s.Stack.IsEmpty() {
+ err = fmt.Errorf("Slow action can not be declared while other actions are on the stack")
+ }
}
- if s.ActivePhase != actionPhase {
- log.Fatalf("Slow action can not be declared during one's actions phase")
+ if err == nil {
+ s.PushAction(a)
}
- s.PushAction(a)
+ s.broadcastNotification(NewDeclaredActionNotification(a, err))
}
-func (s *State) DeclareFastAction(a Action) {
- s.PushAction(a)
-}
-
-func (s *State) AllPassing() bool {
+func (s *State) AllPassing(skipFirst bool) bool {
nPlayers := len(s.Players)
- for i := 0; i < nPlayers; i++ {
- p := s.Players[(i+s.ActivePlayerId)%nPlayers]
+ i := 0
+ if skipFirst {
+ i++
+ }
+ for ; i < nPlayers; i++ {
+ p := s.Players[(i+s.ActivePlayer.Id-1)%nPlayers]
p.Ctrl.SendNotification(NewPriorityNotification())
a := p.Ctrl.RecvAction()
switch a.(type) {
case *PassPriorityAction:
- log.Printf("%s passed %d/%d\n", p.Name, i, nPlayers)
+ log.Printf("%s passed %d/%d\n", p.Name, i+1, nPlayers)
default:
s.DeclareAction(a)
return false
@@ -115,15 +249,15 @@ func (s *State) AllPassing() bool {
return true
}
-func (s *State) PhaseChange(activePlayerId int) {
- log.Println("Phase change")
- for !s.AllPassing() {
+func (s *State) PhaseChange() {
+ skipFirst := true
+ for !s.AllPassing(skipFirst) {
s.Stack.Resolve()
+ skipFirst = false
}
}
func (s *State) broadcastNotification(n PlayerNotification) {
- log.Printf("broadcasting %v\n", n)
for _, p := range s.Players {
p.Ctrl.SendNotification(n)
}
@@ -236,16 +370,13 @@ func (s *State) ResolvePlay(p *Player, c *Card, targets interface{}) {
log.Printf("Resolving unhandled card type: %s\n", c.Type.String())
}
- if !c.IsPermanent() {
+ if c.IsPermanent() {
+ } else {
p.DiscardPile.AddCard(c)
}
}
func (s *State) SpawnTiles(p *Player) []*Tile {
- if p.Name == "The Kraken" {
- return s.KrakenSpawnTiles()
- }
-
spawns := s.Map.FilterTiles(func(t *Tile) bool {
if t.Type != TileTypes.Spawn {
return false
@@ -257,12 +388,43 @@ func (s *State) SpawnTiles(p *Player) []*Tile {
return spawns
}
+func (s *State) WaterSpawnTiles(p *Player) []*Tile {
+ if p.IsKraken() {
+ return s.KrakenSpawnTiles()
+ }
+
+ docks := s.Map.FilterTiles(func(t *Tile) bool {
+ if t.Type != TileTypes.Docks {
+ return false
+ }
+
+ return t.Raw == "docks" || strings.HasSuffix(t.Raw, strconv.Itoa(p.Id))
+ })
+
+ log.Println(docks)
+ spawns := []*Tile{}
+ for _, dock := range docks {
+ for _, candidate := range TilesInRangeFromOrigin(s.Map, dock.Position, 1) {
+ if candidate.Water {
+ spawns = append(spawns, candidate)
+ }
+ }
+ }
+
+ return spawns
+}
+
func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile {
if !c.IsPermanent() {
log.Fatalf("AvailableSpawnTiles called for %s", c.Type.String())
}
- candidates := s.SpawnTiles(p)
+ var candidates []*Tile
+ if c.hasMovementEffect("swimming") {
+ candidates = s.WaterSpawnTiles(p)
+ } else {
+ candidates = s.SpawnTiles(p)
+ }
tiles := []*Tile{}
for _, candidate := range candidates {
diff --git a/go/game/tile.go b/go/game/tile.go
index 55e00b8f..9adb1eae 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -13,6 +13,8 @@ const (
farm
street
spawn
+ docks
+ store
)
func (t TileType) String() string {
@@ -29,6 +31,10 @@ func (t TileType) String() string {
return "street"
case spawn:
return "spawn"
+ case docks:
+ return "docks"
+ case store:
+ return "store"
}
return ""
}
@@ -40,6 +46,8 @@ var TileTypes = struct {
Farm TileType
Street TileType
Spawn TileType
+ Docks TileType
+ Store TileType
}{
Neutral: neutral,
Tower: tower,
@@ -47,6 +55,8 @@ var TileTypes = struct {
Farm: farm,
Street: street,
Spawn: spawn,
+ Docks: docks,
+ Store: store,
}
var TileNames = map[string]TileType{
@@ -56,6 +66,8 @@ var TileNames = map[string]TileType{
"farm": farm,
"street": street,
"spawn": spawn,
+ "docks": docks,
+ "store": store,
}
type Tile struct {
diff --git a/go/game/unit.go b/go/game/unit.go
index 0d39e904..aa535086 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -2,8 +2,6 @@ package game
import (
"log"
-
- "golang.org/x/exp/slices"
)
type UnitState int
@@ -74,13 +72,6 @@ func NewUnitFromPath(cardPath string, tile *Tile, owner *Player) *Unit {
}
func (u *Unit) move(s *State, tile *Tile) {
- if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
- log.Panicf("Unit %s at %v can not move to %v", u.Card.Name, u.Tile.Position, tile.Position)
- }
- if tile.Permanent != nil {
- log.Panicf("Unit %s at %v can not move to occupied %v", u.Card.Name, u.Tile.Position, tile.Position)
- }
-
u.Tile.Permanent = nil
tile.Permanent = u
u.Tile = tile
@@ -88,11 +79,24 @@ func (u *Unit) move(s *State, tile *Tile) {
func (u *Unit) MoveRangeTiles(m *Map) []*Tile {
tiles := []*Tile{}
+ // TODO: build graph only containing possible tile in movement range
+ graph := m.generateMapGraphFor(u)
for _, pos := range PositionsInRange(u.Tile.Position, u.Movement.Range, false) {
tile := m.TileAt(pos)
- if tile != nil && u.IsAvailableTile(tile) {
- tiles = append(tiles, tile)
+ if tile == nil || !u.IsAvailableTile(tile) {
+ continue
+ }
+
+ path, err := findPathTo(graph, u, pos)
+ if err != nil {
+ continue
+ }
+
+ if path.Distance/2 > int64(u.Movement.Range) {
+ continue
}
+
+ tiles = append(tiles, tile)
}
return tiles
}
@@ -149,11 +153,14 @@ func (u *Unit) FullAction() {
func (u *Unit) GetAvailActionPrototypes() []ActionPrototype {
prototypes := []ActionPrototype{}
- if u.AvailMoveActions > 0 {
+ if u.AvailMoveActions > 0 && u.Movement != INVALID_MOVEMENT() {
prototypes = append(prototypes, NewMoveActionPrototype(u))
}
- if u.AvailAttackActions > 0 {
+ m := u.GetController().gameState.Map
+ if u.AvailAttackActions > 0 && u.Attack != INVALID_ATTACK() &&
+ len(u.AttackableEnemyPermanents(m)) > 0 {
+
prototypes = append(prototypes, NewAttackActionPrototype(u))
}
diff --git a/go/ui/buffer.go b/go/ui/buffer.go
index e0e6b001..c451d7fc 100644
--- a/go/ui/buffer.go
+++ b/go/ui/buffer.go
@@ -31,12 +31,12 @@ func (b *Buffer) render() *ebiten.Image {
img.Fill(BUFFER_BACKGROUND)
y := 0
for _, line := range b.lines[b.pos:] {
- bounds := text.BoundString(font.Font, line)
+ bounds := text.BoundString(font.Font18, line)
y += bounds.Dy()
if y > b.Height {
break
}
- text.Draw(img, line, font.Font, 0, y, BUFFER_FOREGROUND)
+ text.Draw(img, line, font.Font18, 0, y, BUFFER_FOREGROUND)
}
return img
}
diff --git a/go/ui/button.go b/go/ui/button.go
index 97dbad22..6454ccac 100644
--- a/go/ui/button.go
+++ b/go/ui/button.go
@@ -32,9 +32,19 @@ func NewSimpleButton(x, y, w, h int, label string, onClick func(b *SimpleButton)
func (b *SimpleButton) render() *ebiten.Image {
img := ebiten.NewImage(b.Width, b.Height)
- // b := text.BoundString(f, label)
ebitenutil.DrawRect(img, float64(0), float64(0), float64(b.Width), float64(b.Height), b.BG)
- text.Draw(img, b.label, font.Font, 20, 25, b.FG)
+
+ bounds := text.BoundString(font.Font24, b.label)
+ x := (b.Width - bounds.Dx()) / 2
+ if x < 0 {
+ x = 0
+ }
+
+ y := b.Height - bounds.Dy()/2
+ if y < 0 {
+ y = 0
+ }
+ text.Draw(img, b.label, font.Font24, x, y, b.FG)
return img
}
diff --git a/go/ui/cardView.go b/go/ui/cardView.go
new file mode 100644
index 00000000..a1c004c5
--- /dev/null
+++ b/go/ui/cardView.go
@@ -0,0 +1,46 @@
+package ui
+
+import (
+ "github.com/hajimehoshi/ebiten/v2"
+ "muhq.space/muhqs-game/go/assets"
+)
+
+type CardView struct {
+ WidgetBase
+ cardPath string
+}
+
+func NewScaledCardView(x, y int, width, height int, cardPath string) *CardView {
+ cv := &CardView{NewWidgetBase(x, y, width, height), cardPath}
+ cv.renderImpl = func() *ebiten.Image { return cv.render() }
+ return cv
+}
+
+func NewAutoCardView(x, y int, cardPath string) *CardView {
+ cv := &CardView{NewWidgetBase(x, y, -1, -1), cardPath}
+ cv.renderImpl = func() *ebiten.Image { return cv.render() }
+ return cv
+}
+
+func (cv *CardView) render() *ebiten.Image {
+ card := assets.GetCard(cv.cardPath, "en")
+ op := &ebiten.DrawImageOptions{}
+
+ b := card.Bounds()
+ if cv.Width == -1 && cv.Height == -1 {
+ cv.Width = b.Dx()
+ cv.Height = b.Dy()
+ } else {
+ x_scale := float64(cv.Width) / float64(b.Dx())
+ y_scale := float64(cv.Height) / float64(b.Dy())
+ scale := x_scale
+ if y_scale < x_scale {
+ scale = y_scale
+ }
+ op.GeoM.Scale(scale, scale)
+ }
+
+ img := ebiten.NewImage(cv.Width, cv.Height)
+ img.DrawImage(card, op)
+ return img
+}
diff --git a/go/ui/hand.go b/go/ui/hand.go
index b07d177b..5421f6fb 100644
--- a/go/ui/hand.go
+++ b/go/ui/hand.go
@@ -6,6 +6,8 @@ import (
"github.com/hajimehoshi/ebiten/v2"
"muhq.space/muhqs-game/go/assets"
"muhq.space/muhqs-game/go/game"
+
+ "golang.org/x/exp/slices"
)
type HandCard struct {
@@ -14,13 +16,24 @@ type HandCard struct {
type HandView struct {
WidgetBase
- hand *game.Hand
- cardWidth float64
- scale float64
+ hand *game.Hand
+ cardWidth float64
+ cardHeight float64
+ scale float64
+ bottomAligned bool
+ highlights []*game.Card
}
-func NewHandView(x, y, width, height int, h *game.Hand) *HandView {
- hv := &HandView{NewWidgetBase(x, y, width, height), h, 0, 0}
+func NewHandView(x, y, width, height int, h *game.Hand, bottomAligned bool) *HandView {
+ hv := &HandView{
+ WidgetBase: NewWidgetBase(x, y, width, height),
+ hand: h,
+ cardWidth: 0,
+ cardHeight: 0,
+ scale: 0,
+ bottomAligned: bottomAligned,
+ highlights: []*game.Card{},
+ }
hv.renderImpl = func() *ebiten.Image { return hv.render() }
return hv
}
@@ -44,11 +57,22 @@ func (hv *HandView) render() *ebiten.Image {
hv.scale = y_scale
}
hv.cardWidth = float64(b.Dx()) * hv.scale
+ hv.cardHeight = float64(b.Dy()) * hv.scale
+ }
+
+ yOffset := 0.0
+ if hv.bottomAligned {
+ yOffset = float64(hv.Height) - hv.cardHeight
}
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(hv.scale, hv.scale)
- op.GeoM.Translate(float64(i)*hv.cardWidth, 0)
+ op.GeoM.Translate(float64(i)*hv.cardWidth, yOffset)
+
+ if slices.Contains(hv.highlights, card) {
+ op.ColorM.Scale(1, 0, 0, 0.5)
+ }
+
img.DrawImage(cardImg, op)
}
return img
@@ -59,6 +83,10 @@ func (hv *HandView) FindObjectAt(x, y int) interface{} {
return nil
}
+ if hv.bottomAligned && float64(y-hv.Y) < float64(hv.Height)-hv.cardHeight {
+ return nil
+ }
+
i := int(float64(x) / hv.cardWidth)
if i >= hv.hand.Size() {
return nil
@@ -66,3 +94,16 @@ func (hv *HandView) FindObjectAt(x, y int) interface{} {
return HandCard{hv.hand.GetCards()[i]}
}
+
+func (hv *HandView) setHighlights(cards []*game.Card) {
+ hv.highlights = cards
+ hv.ForceRedraw()
+}
+
+func (hv *HandView) HighlightCard(card *game.Card) {
+ hv.setHighlights([]*game.Card{card})
+}
+
+func (hv *HandView) ClearHighlights() {
+ hv.setHighlights([]*game.Card{})
+}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
index 6c887920..c966f1a8 100644
--- a/go/ui/mapView.go
+++ b/go/ui/mapView.go
@@ -132,20 +132,25 @@ func (vw *MapView) drawMapLayer(screen *ebiten.Image) {
screen.DrawImage(vw.mapLayer, &ebiten.DrawImageOptions{})
}
+func getPermanentSymbol(p game.Permanent, i int) *ebiten.Image {
+ permanentSymbol := assets.GetSymbol(strings.ToLower(p.GetCard().Name))
+ if permanentSymbol == nil {
+ symbol := fmt.Sprintf("%c%d", unicode.ToUpper(rune(p.GetCard().Type.String()[0])), i)
+ permanentSymbol = assets.GetGenericSymbol(symbol)
+ if permanentSymbol == nil {
+ log.Fatalf("Failed to generate generic symbol %s", symbol)
+ }
+ }
+
+ return permanentSymbol
+}
+
func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) {
if vw.permanentsLayer == nil {
vw.permanentsLayer = vw.newLayerImage()
for i, p := range vw.gameState.Permanents {
- permanentSymbol := assets.GetSymbol(strings.ToLower(p.GetCard().Name))
- if permanentSymbol == nil {
- symbol := fmt.Sprintf("%c%d", unicode.ToUpper(rune(p.GetCard().Type.String()[0])), i)
- permanentSymbol = assets.GetGenericSymbol(symbol)
- if permanentSymbol == nil {
- log.Fatalf("Failed to generate generic symbol %s", symbol)
- }
- }
-
+ permanentSymbol := getPermanentSymbol(p, i)
t := p.GetTile()
x_px := t.Position.X*TILE_WIDTH + (TILE_WIDTH-PERMANENT_WIDTH)/2
y_px := t.Position.Y*TILE_HEIGHT + (TILE_HEIGHT-PERMANENT_HEIGHT)/2
@@ -178,19 +183,37 @@ func (vw *MapView) ForceRedraw() {
vw.permanentsLayer = nil
}
-func (vw *MapView) FindObjectAt(x_px, y_px int) interface{} {
- x := x_px / TILE_WIDTH
- y := y_px / TILE_HEIGHT
+func isSymbolTransparentAt(p game.Permanent, relativeX, relativeY int) bool {
+ permanentSymbol := getPermanentSymbol(p, 0)
+ _, _, _, alpha := permanentSymbol.At(relativeX, relativeY).RGBA()
+ return alpha == 0
+}
+
+func (vw *MapView) FindObjectAt(screenX, screenY int) interface{} {
+ x := screenX / TILE_WIDTH
+ y := screenY / TILE_HEIGHT
if x < 0 || y < 0 {
return nil
}
- // TODO: detect if a permanent or the containing tile was selected
- for _, u := range vw.gameState.Permanents {
- pos := u.GetTile().Position
- if pos.X == x && pos.Y == y {
- return u
+ relativeX := screenX % TILE_WIDTH
+ relativeY := screenY % TILE_HEIGHT
+ xMargin := (TILE_WIDTH - PERMANENT_WIDTH) / 2
+ yMargin := (TILE_HEIGHT - PERMANENT_HEIGHT) / 2
+ // detect if a permanent or the containing tile was selected
+ if relativeX >= xMargin && relativeX <= TILE_WIDTH-xMargin &&
+ relativeY >= yMargin && relativeY <= TILE_HEIGHT-yMargin {
+
+ for _, p := range vw.gameState.Permanents {
+ pos := p.GetTile().Position
+ if pos.X == x && pos.Y == y {
+ if isSymbolTransparentAt(p, relativeX, relativeY) {
+ break
+ }
+
+ return p
+ }
}
}
diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go
index d7d86ea9..6914de6e 100644
--- a/go/ui/stateBar.go
+++ b/go/ui/stateBar.go
@@ -7,16 +7,12 @@ import (
"muhq.space/muhqs-game/go/game"
)
-const (
- PHASE_VIEW_WIDTH = 250
-)
-
type StateBar struct {
X, Y int
Width, Height int
gameState *game.State
activePlayer *game.Player
- ressourceView *TextBox
+ resourceView *TextBox
phaseView *TextBox
}
@@ -25,25 +21,25 @@ func NewStateBar(x, y int, width, height int, s *game.State, p *game.Player) *St
return sb
}
-func (sb *StateBar) ressourceLabel() string {
- return fmt.Sprintf("%d", sb.activePlayer.Ressource)
+func (sb *StateBar) resourceLabel() string {
+ return fmt.Sprintf("%d", sb.activePlayer.Resource)
}
func (sb *StateBar) phaseLabel() string {
- return fmt.Sprintf("%s: %s", sb.gameState.Players[sb.gameState.ActivePlayerId].Name,
+ return fmt.Sprintf("%s: %s", sb.gameState.ActivePlayer.Name,
sb.gameState.ActivePhase.String())
}
func (sb *StateBar) Draw(screen *ebiten.Image) {
- if sb.ressourceView == nil {
- sb.ressourceView = NewFixedTextBox(sb.X, sb.Y, sb.Height, sb.Height,
- sb.ressourceLabel())
+ if sb.resourceView == nil {
+ sb.resourceView = NewCenteringFixedTextBox(sb.X, sb.Y, sb.Height, sb.Height,
+ sb.resourceLabel())
}
- sb.ressourceView.Draw(screen)
+ sb.resourceView.Draw(screen)
if sb.phaseView == nil {
- sb.phaseView = NewFixedTextBox(sb.X+sb.ressourceView.Width, sb.Y,
- PHASE_VIEW_WIDTH, sb.Height, sb.phaseLabel())
+ sb.phaseView = NewFixedTextBox(sb.X+sb.resourceView.Width, sb.Y,
+ sb.Width-sb.resourceView.Width, sb.Height, sb.phaseLabel(), false)
}
sb.phaseView.Draw(screen)
}
@@ -53,9 +49,17 @@ func (sb *StateBar) FindObjectAt(x, y int) interface{} {
}
func (sb *StateBar) ForceRedraw() {
- sb.ressourceView.Text = sb.ressourceLabel()
- sb.ressourceView = nil
+ if sb.resourceView != nil {
+ sb.resourceView.Text = sb.resourceLabel()
+ sb.resourceView.ForceRedraw()
+ }
+
+ if sb.phaseView != nil {
+ sb.phaseView.Text = sb.phaseLabel()
+ sb.phaseView.ForceRedraw()
+ }
+}
- sb.phaseView.Text = sb.phaseLabel()
- sb.phaseView = nil
+func (sb *StateBar) Contains(x, y int) bool {
+ return sb.resourceView.Contains(x, y) || sb.phaseView.Contains(x, y)
}
diff --git a/go/ui/textBox.go b/go/ui/textBox.go
index 0cd09294..dd10e8e2 100644
--- a/go/ui/textBox.go
+++ b/go/ui/textBox.go
@@ -11,30 +11,60 @@ import (
)
var (
- TEXTBOX_BACKGROUND = gray
- TEXTBOX_FOREGROUND = color.White
+ TEXTBOX_DEFAULT_BACKGROUND = gray
+ TEXTBOX_DEFAULT_FOREGROUND = color.White
- TEXTBOX_AUTO_BORDER = 3
+ TEXTBOX_DEFAULT_MARGIN = 3
)
type TextBox struct {
WidgetBase
- Text string
+ Text string
+ Fg color.Color
+ Bg color.Color
+ XMargin int
+ YMargin int
+ Center bool
}
-func NewFixedTextBox(x, y int, width, height int, t string) *TextBox {
- tb := &TextBox{NewWidgetBase(x, y, width, height), t}
+func NewFixedTextBox(x, y int, width, height int, t string, center bool) *TextBox {
+ tb := &TextBox{
+ WidgetBase: NewWidgetBase(x, y, width, height),
+ Text: t,
+ Fg: TEXTBOX_DEFAULT_FOREGROUND,
+ Bg: TEXTBOX_DEFAULT_BACKGROUND,
+ XMargin: TEXTBOX_DEFAULT_MARGIN,
+ YMargin: TEXTBOX_DEFAULT_MARGIN,
+ Center: center,
+ }
tb.renderImpl = func() *ebiten.Image { return tb.render() }
return tb
}
+func NewCenteringFixedTextBox(x, y int, width, height int, t string) *TextBox {
+ return NewFixedTextBox(x, y, width, height, t, true)
+}
+
func NewAutoTextBox(x, y int, t string) *TextBox {
- return NewFixedTextBox(x, y, -1, -1, t)
+ return NewFixedTextBox(x, y, -1, -1, t, false)
+}
+
+func NewCenteringAutoTextBox(x, y int, t string) *TextBox {
+ return NewFixedTextBox(x, y, -1, -1, t, true)
}
func NewUnitInfo(x, y int, u *game.Unit) *TextBox {
- info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nMovement: %s\nAttack %s\nUpkeep: %d",
- u.String(), u.GetDamage(), u.Health, u.Movement.String(), u.Attack.String(), u.Upkeep)
+ info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nUpkeep: %d",
+ u.String(), u.GetDamage(), u.Health, u.Upkeep)
+
+ if u.Movement != game.INVALID_MOVEMENT() {
+ info = fmt.Sprintf("%s\nMovement: %s", info, u.Movement.String())
+ }
+
+ if u.Attack != game.INVALID_ATTACK() {
+ info = fmt.Sprintf("%s\nAttack: %s", info, u.Attack.String())
+ }
+
return NewAutoTextBox(x, y, info)
}
@@ -45,13 +75,88 @@ func NewPermInfo(x, y int, p game.Permanent) *TextBox {
}
func (tb *TextBox) render() *ebiten.Image {
- b := text.BoundString(font.Font, tb.Text)
+ var x, y int
+ b := text.BoundString(font.Font24, tb.Text)
if tb.Width == -1 || tb.Height == -1 {
- tb.Width = b.Dx() + 2*TEXTBOX_AUTO_BORDER
- tb.Height = b.Dy() + 2*TEXTBOX_AUTO_BORDER
+ tb.Width = b.Dx() + 2*tb.XMargin
+ tb.Height = b.Dy() + 2*tb.YMargin
+ x = tb.XMargin + b.Min.X
+ y = tb.YMargin - b.Min.Y
+ } else {
+ if !tb.Center {
+ x = tb.XMargin + b.Min.X
+ } else {
+ x = (tb.Width - b.Dx()) / 2
+ if x < 0 {
+ x = 0
+ }
+ }
+
+ y = tb.Height - b.Dy()/2
+ if y < 0 {
+ y = 0
+ }
}
+
img := ebiten.NewImage(tb.Width, tb.Height)
- img.Fill(TEXTBOX_BACKGROUND)
- text.Draw(img, tb.Text, font.Font, TEXTBOX_AUTO_BORDER+b.Min.X, TEXTBOX_AUTO_BORDER-b.Min.Y, TEXTBOX_FOREGROUND)
+ img.Fill(tb.Bg)
+ text.Draw(img, tb.Text, font.Font24, x, y, tb.Fg)
return img
}
+
+type PocList struct {
+ TextBox
+ centerX int
+ bottomY int
+ poc game.PileOfCards
+}
+
+func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList {
+ w := &PocList{
+ TextBox: TextBox{
+ WidgetBase: NewWidgetBase(-1, -1, -1, -1),
+ Text: "",
+ Fg: TEXTBOX_DEFAULT_FOREGROUND,
+ Bg: TEXTBOX_DEFAULT_BACKGROUND,
+ XMargin: TEXTBOX_DEFAULT_MARGIN,
+ YMargin: TEXTBOX_DEFAULT_MARGIN,
+ Center: true,
+ },
+ centerX: centerX,
+ bottomY: bottomY,
+ poc: poc,
+ }
+ w.renderImpl = func() *ebiten.Image {
+ w.setText()
+ return w.render()
+ }
+ return w
+}
+
+func (w *PocList) setText() {
+ t := ""
+ for _, c := range w.poc.GetCards() {
+ t = fmt.Sprintf("%s%s\n", t, c.Name)
+ }
+
+ if t != "" {
+ w.Text = t[:len(t)-1]
+ } else {
+ w.Text = "No cards"
+ }
+
+ b := text.BoundString(font.Font24, w.Text)
+ w.X = w.centerX - b.Dx()/2 - w.XMargin
+ w.Y = w.bottomY - b.Dy() - w.YMargin
+}
+
+func (w *PocList) FindObjectAt(x, y int) interface{} {
+ cards := w.poc.GetCards()
+ if len(cards) == 0 {
+ return nil
+ }
+
+ // b := text.BoundString(font.Font, w.Text)
+ // i := b.Dy() / w.poc.Size()
+ return cards[0]
+}
diff --git a/go/ui/widget.go b/go/ui/widget.go
index d783eb36..cfcdd766 100644
--- a/go/ui/widget.go
+++ b/go/ui/widget.go
@@ -7,6 +7,7 @@ import (
type Widget interface {
Draw(*ebiten.Image)
FindObjectAt(int, int) interface{}
+ Contains(int, int) bool
}
type WidgetBase struct {