diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-26 22:16:10 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:44 +0100 |
| commit | 926ca5cdd20a39e3e05aaa3486cb166718dab5ca (patch) | |
| tree | 3d7bc8a0e1e0be1ce8d92d5503bdede878e718e9 | |
| parent | 8720ebf9c74d9e11001772b5c00c293fa3a8b9e5 (diff) | |
| download | muhqs-game-926ca5cdd20a39e3e05aaa3486cb166718dab5ca.tar.gz muhqs-game-926ca5cdd20a39e3e05aaa3486cb166718dab5ca.zip | |
intermeditae commit
| -rw-r--r-- | go/assets/retrieve.go | 2 | ||||
| -rw-r--r-- | go/client/main.go | 385 | ||||
| -rw-r--r-- | go/font/font.go | 18 | ||||
| -rw-r--r-- | go/game/action.go | 110 | ||||
| -rw-r--r-- | go/game/card.go | 3 | ||||
| -rw-r--r-- | go/game/deck.go | 14 | ||||
| -rw-r--r-- | go/game/kraken.go | 86 | ||||
| -rw-r--r-- | go/game/map.go | 57 | ||||
| -rw-r--r-- | go/game/map_test.go | 4 | ||||
| -rw-r--r-- | go/game/movement.go | 11 | ||||
| -rw-r--r-- | go/game/phases.go (renamed from go/game/phase.go) | 14 | ||||
| -rw-r--r-- | go/game/pileOfCards.go | 4 | ||||
| -rw-r--r-- | go/game/player.go | 45 | ||||
| -rw-r--r-- | go/game/playerControl.go | 33 | ||||
| -rw-r--r-- | go/game/stack.go | 14 | ||||
| -rw-r--r-- | go/game/state.go | 258 | ||||
| -rw-r--r-- | go/game/tile.go | 12 | ||||
| -rw-r--r-- | go/game/unit.go | 33 | ||||
| -rw-r--r-- | go/ui/buffer.go | 4 | ||||
| -rw-r--r-- | go/ui/button.go | 14 | ||||
| -rw-r--r-- | go/ui/cardView.go | 46 | ||||
| -rw-r--r-- | go/ui/hand.go | 53 | ||||
| -rw-r--r-- | go/ui/mapView.go | 57 | ||||
| -rw-r--r-- | go/ui/stateBar.go | 40 | ||||
| -rw-r--r-- | go/ui/textBox.go | 133 | ||||
| -rw-r--r-- | go/ui/widget.go | 1 |
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 { |
