aboutsummaryrefslogtreecommitdiff
path: root/go
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-31 22:19:28 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:45 +0100
commit3fd15c9b83fff074b588ff9688f9e49aab38cd32 (patch)
treefaf0710b755118dcd5ce7ae99fade3300e369292 /go
parent01b8c63effcdf862f6de2ec86ab47b5adf2a49bf (diff)
downloadmuhqs-game-3fd15c9b83fff074b588ff9688f9e49aab38cd32.tar.gz
muhqs-game-3fd15c9b83fff074b588ff9688f9e49aab38cd32.zip
intermediate commit
Diffstat (limited to 'go')
-rw-r--r--go/client/game.go537
-rw-r--r--go/client/main.go578
-rw-r--r--go/client/startMenu.go92
-rw-r--r--go/game/action.go2
-rw-r--r--go/game/cardParsing.go38
-rw-r--r--go/game/kraken.go19
-rw-r--r--go/game/player.go11
-rw-r--r--go/game/state.go6
8 files changed, 737 insertions, 546 deletions
diff --git a/go/client/game.go b/go/client/game.go
new file mode 100644
index 00000000..2f112593
--- /dev/null
+++ b/go/client/game.go
@@ -0,0 +1,537 @@
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/ebitenutil"
+ "github.com/hajimehoshi/ebiten/v2/inpututil"
+
+ "muhq.space/muhqs-game/go/game"
+ "muhq.space/muhqs-game/go/ui"
+)
+
+const (
+ DEFAULT_BUTTON_HEIGHT = 40
+ PASS_BUTTON_WIDTH = 100
+
+ RESOLVE_BUTTON_X = 500
+ STACK_BUFFER_WIDTH = 300
+
+ HOVER_THRESHOLD = 60
+
+ HAND_VIEW_HEIGHT = 500
+ HAND_VIEW_WIDTH = 500
+
+ STATE_BAR_WIDTH = 300
+)
+
+// 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 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) {
+ if x == h.last_x && y == h.last_y {
+ h.ticks++
+ } else {
+ h.last_x, h.last_y = x, y
+ h.reset()
+ }
+}
+
+func (h *hoverDetector) reset() {
+ if h.resetFunc != nil && !h.resetFunc() {
+ return
+ }
+ h.ticks = 0
+ h.resetFunc = nil
+}
+
+func (h *hoverDetector) startedHovering() bool {
+ return h.ticks == HOVER_THRESHOLD
+}
+
+type Game struct {
+ width, height int
+
+ 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
+ prompt *ui.Prompt
+ widgets []ui.Widget
+
+ activePlayer *game.Player
+
+ playerCtrl *game.ChanPlayerControl
+ hasPriority bool
+
+ gameState *game.State
+ storesOnMap bool
+}
+
+func newGame(app *app) *Game {
+ g := &Game{
+ width: app.windowWidth,
+ height: app.windowHeight,
+ }
+
+ g.widgets = []ui.Widget{}
+ g.gameState = game.NewState()
+
+ g.passButton = ui.NewSimpleButton(g.width-PASS_BUTTON_WIDTH,
+ g.height-DEFAULT_BUTTON_HEIGHT,
+ PASS_BUTTON_WIDTH,
+ DEFAULT_BUTTON_HEIGHT,
+ "Pass",
+ func(*ui.SimpleButton) {
+ if g.prompt != nil {
+ a := g.prompt.Confirm()
+ g.declareAction(a)
+ g.removeWidget(g.prompt)
+ g.prompt = nil
+
+ } else if g.hasPriority {
+ g.passPriority()
+ }
+ })
+
+ g.stackBuffer = ui.NewBuffer(g.width-STACK_BUFFER_WIDTH, g.passButton.Y-400,
+ STACK_BUFFER_WIDTH, 400)
+ return g
+}
+
+func (g *Game) loadMap(mapName string) *Game {
+ m, err := game.GetMap(mapName)
+ if err != nil {
+ log.Fatal(err)
+ }
+ 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
+ return g
+}
+
+func (g *Game) addActivePlayer(name string, deckList string) *Game {
+ deck := game.NewDeckFromDeckList(deckList)
+ color := playerColors[len(g.gameState.Players)]
+ g.gameState.AddNewPlayer(name, deck)
+ p := g.getPlayer(name)
+ p.Color = color
+ return g.initPlayerUi(p)
+}
+
+func (g *Game) getPlayer(name string) *game.Player {
+ for _, p := range g.gameState.Players {
+ if p.Name == name {
+ return p
+ }
+ }
+ return nil
+}
+
+func (g *Game) initPlayerUi(player *game.Player) *Game {
+ g.activePlayer = player
+ g.playerCtrl = game.NewChanPlayerControl()
+ player.Ctrl = g.playerCtrl
+ var x, y int
+
+ g.handLayer = ui.NewHandView(0,
+ g.mapView.Height(),
+ HAND_VIEW_WIDTH,
+ g.height-g.mapView.Height()-DEFAULT_BUTTON_HEIGHT,
+ player.Hand,
+ true)
+ g.addWidget(g.handLayer)
+
+ g.stateBar = ui.NewStateBar(0,
+ g.height-DEFAULT_BUTTON_HEIGHT,
+ STATE_BAR_WIDTH,
+ DEFAULT_BUTTON_HEIGHT,
+ g.gameState,
+ player)
+ g.addWidget(g.stateBar)
+
+ g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width,
+ g.height-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 = g.height - DEFAULT_BUTTON_HEIGHT
+ g.discardPileView = ui.NewPocList(x, y, g.activePlayer.DiscardPile)
+
+ if !g.storesOnMap {
+ x = g.stateBar.Width + g.discardPileButton.Width
+ y = g.height - 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)
+
+ x = g.storeButton.X + g.storeButton.Width/2
+ y = g.height - DEFAULT_BUTTON_HEIGHT
+ g.storeView = ui.NewPocList(x, y, g.activePlayer.Store)
+ }
+
+ return g
+}
+
+func (g *Game) showStore() {
+ g.storeView.ForceRedraw()
+ g.addWidget(g.storeView)
+}
+
+func (g *Game) hideStore() {
+ g.removeWidget(g.storeView)
+}
+
+func (g *Game) addWidget(w ui.Widget) {
+ g.widgets = append(g.widgets, w)
+}
+
+func (g *Game) findWidget(widget ui.Widget) int {
+ for i, w := range g.widgets {
+ if w != widget {
+ continue
+ }
+ return i
+ }
+ return -1
+}
+
+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.AvailActionPrototypes()
+ if len(actions) > 0 {
+ g.selectedObject = perm
+ onClick := func(c *ui.Choice, x, y int) {
+ g.removeActionChoice()
+ g.selectedObject = actions[c.GetChoosen(x, y)]
+ }
+ g.actionChoice = ui.NewActionChoice(x, y, actions, onClick)
+ g.addWidget(g.actionChoice)
+ }
+}
+
+func (g *Game) removeActionChoice() {
+ g.removeWidget(g.actionChoice)
+ g.actionChoice = nil
+}
+
+func (g *Game) AddHighlight(obj interface{}) {
+ switch obj := obj.(type) {
+ case ui.HandCard:
+ g.handLayer.AddHighlightCard(obj.C)
+ case *game.Tile:
+ g.mapView.AddHighlightTile(obj)
+ }
+}
+
+func (g *Game) clearMapHighlights() {
+ g.mapView.ClearPermanentsHighlights()
+ g.mapView.ClearTileHighlights()
+ g.mapView.ForceRedraw()
+}
+
+func (g *Game) declareAction(a game.Action) {
+ g.playerCtrl.SendAction(a)
+ g.clearMapHighlights()
+ g.handLayer.ClearHighlights()
+ g.hasPriority = false
+ g.hidePassButton()
+ g.stateBar.ForceRedraw()
+}
+
+func (g *Game) showPassButton() {
+ g.addWidget(g.passButton)
+}
+
+func (g *Game) hidePassButton() {
+ g.removeWidget(g.passButton)
+}
+
+func (g *Game) passPriority() {
+ g.declareAction(game.NewPassPriority(g.activePlayer))
+}
+
+func (g *Game) handlePlayerNotifications() {
+ n := g.playerCtrl.RecvNotification()
+ for n != nil {
+ switch n.Notification {
+ case game.PriorityNotification:
+ g.showPassButton()
+ g.hasPriority = true
+
+ case game.DeclaredActionNotification:
+ 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()
+ }
+
+ case game.HandCardSelectionPrompt:
+ ctx := n.Context.([]int)
+ min, max := ctx[0], ctx[1]
+ promptTxt := fmt.Sprintf("Select between %d and %d hand cards", min, max)
+ g.prompt = ui.NewHandCardPrompt(g.height/2, g.width, promptTxt)
+ g.addWidget(g.prompt)
+ g.showPassButton()
+ }
+
+ g.stateBar.ForceRedraw()
+ g.handLayer.ForceRedraw()
+
+ n = g.playerCtrl.RecvNotification()
+ }
+}
+
+func (g *Game) findObjectAt(x, y int) interface{} {
+ // 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
+ }
+ }
+ if obj := g.mapView.FindObjectAt(x, y); obj != nil {
+ return obj
+ }
+
+ 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
+ }
+
+ 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(g.gameState, tile)
+ case *game.Card:
+ if obj.IsPermanent() {
+ action = game.NewPlayAction(g.activePlayer, obj, tile.Position)
+ }
+ }
+ 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(g.gameState, perm)
+ g.selectedObject = nil
+ g.declareAction(action)
+ g.mapView.HighlightPermanent(perm)
+ }
+ }
+
+ case ui.HandCard:
+ g.selectedObject = obj.C
+
+ case *game.Card:
+ if g.gameState.IsActivePlayer(g.activePlayer) &&
+ g.gameState.ActivePhase == game.Phases.BuyPhase {
+ g.declareAction(game.NewBuyAction(g.activePlayer, obj))
+ }
+
+ 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)
+ }
+ }
+}
+
+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 > g.width || y+700 > g.height {
+ 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.StoreOn(obj.Position)
+ hoverHint = ui.NewPocList(x, y, poc)
+ // Set a special hoverDetector resetFunc keeping the hovering store
+ // around and allow card selection until the cursor leaves the store widget.
+ 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)
+ // 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) {
+ if g.prompt != nil {
+ g.prompt.Add(obj)
+ g.AddHighlight(obj)
+ } else {
+ g.handleSelection(obj, x, y)
+ }
+ g.hightlightSelection(obj)
+ } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil {
+ g.handleHover(obj, x, y)
+ } else if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
+ g.passButton.Click(0, 0)
+ } else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
+ g.removeActionChoice()
+ g.selectedObject = nil
+ g.hoverDetector.reset()
+ g.clearMapHighlights()
+ g.handLayer.ClearHighlights()
+ }
+ return nil
+}
+
+func (g *Game) Draw(screen *ebiten.Image) {
+ g.mapView.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()))
+}
+
+func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
+ return g.width, g.height
+}
+
+func (g *Game) Start() *Game {
+ go func() {
+ w := g.gameState.Loop()
+ log.Println("Winners are", w)
+ }()
+
+ return g
+}
diff --git a/go/client/main.go b/go/client/main.go
index 10279085..63fcab00 100644
--- a/go/client/main.go
+++ b/go/client/main.go
@@ -1,35 +1,20 @@
package main
import (
- "fmt"
+ "flag"
"image/color"
+ "io"
"log"
+ "os"
"github.com/hajimehoshi/ebiten/v2"
- "github.com/hajimehoshi/ebiten/v2/ebitenutil"
- "github.com/hajimehoshi/ebiten/v2/inpututil"
"muhq.space/muhqs-game/go/font"
- "muhq.space/muhqs-game/go/game"
- "muhq.space/muhqs-game/go/ui"
)
const (
- SCREEN_WIDTH = 800
- SCREEN_HEIGHT = 1024
-
- DEFAULT_BUTTON_HEIGHT = 40
- PASS_BUTTON_WIDTH = 100
-
- RESOLVE_BUTTON_X = 500
- STACK_BUFFER_WIDTH = 300
-
- HOVER_THRESHOLD = 60
-
- HAND_VIEW_HEIGHT = 500
- HAND_VIEW_WIDTH = 500
-
- STATE_BAR_WIDTH = 300
+ WINDOW_WIDTH = 800
+ WINDOW_HEIGHT = 1024
)
var playerColors = []color.RGBA{
@@ -37,542 +22,75 @@ 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 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) {
- if x == h.last_x && y == h.last_y {
- h.ticks++
- } else {
- h.last_x, h.last_y = x, y
- h.reset()
- }
-}
-
-func (h *hoverDetector) reset() {
- if h.resetFunc != nil && !h.resetFunc() {
- return
- }
- h.ticks = 0
- h.resetFunc = nil
-}
-
-func (h *hoverDetector) startedHovering() bool {
- return h.ticks == HOVER_THRESHOLD
-}
-
-type Game struct {
- screenWidth, screenHeight int
-
- 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
- prompt *ui.Prompt
- widgets []ui.Widget
-
- activePlayer *game.Player
-
- playerCtrl *game.ChanPlayerControl
- hasPriority bool
-
- gameState *game.State
- storesOnMap bool
-}
-
-func NewGame() *Game {
- g := &Game{
- screenWidth: SCREEN_WIDTH,
- screenHeight: SCREEN_HEIGHT,
- }
-
- g.widgets = []ui.Widget{}
- g.gameState = game.NewState()
-
- g.passButton = ui.NewSimpleButton(g.screenWidth-PASS_BUTTON_WIDTH,
- g.screenHeight-DEFAULT_BUTTON_HEIGHT,
- PASS_BUTTON_WIDTH,
- DEFAULT_BUTTON_HEIGHT,
- "Pass",
- func(*ui.SimpleButton) {
- if g.prompt != nil {
-
- a := g.prompt.Confirm()
- log.Println("Prompt confirmation returned", a)
- g.declareAction(a)
- g.prompt = nil
- g.removeWidget(g.prompt)
- } else if g.hasPriority {
- g.passPriority()
- }
- })
-
- g.stackBuffer = ui.NewBuffer(g.screenWidth-STACK_BUFFER_WIDTH, g.passButton.Y-400,
- STACK_BUFFER_WIDTH, 400)
- return g
-}
-
-func NewGameFromMap(mapName string) *Game {
- g := NewGame()
- g.loadMap(mapName)
- return g
-}
-
-func (g *Game) loadMap(mapName string) {
- m, err := game.GetMap(mapName)
- if err != nil {
- log.Fatal(err)
- }
- 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 {
-// g := NewGameFromMap(mapName)
-// activePlayer := g.addPlayer(players[0], decks[0], )
-// g.initPlayerUi(activePlayer)
-
-// for i, p := range players[1:] {
-// g.addPlayer(p, decks[i+1])
-// }
-// return g
-// }
-
-func (g *Game) addPlayer(name string, deck *game.Deck) *game.Player {
- color := playerColors[len(g.gameState.Players)]
- g.gameState.AddNewPlayer(name, deck, color)
- return g.getPlayer(name)
-}
-
-func (g *Game) getPlayer(name string) *game.Player {
- for _, p := range g.gameState.Players {
- if p.Name == name {
- return p
- }
- }
- return nil
-}
-
-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.Height(),
- HAND_VIEW_WIDTH,
- g.screenHeight-g.mapView.Height()-DEFAULT_BUTTON_HEIGHT,
- player.Hand,
- true)
- g.addWidget(g.handLayer)
-
- g.stateBar = ui.NewStateBar(0,
- g.screenHeight-DEFAULT_BUTTON_HEIGHT,
- STATE_BAR_WIDTH,
- DEFAULT_BUTTON_HEIGHT,
- g.gameState,
- player)
- g.addWidget(g.stateBar)
-
- g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width,
- g.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 = g.screenHeight - DEFAULT_BUTTON_HEIGHT
- g.discardPileView = ui.NewPocList(x, y, g.activePlayer.DiscardPile)
-
- if !g.storesOnMap {
- x = g.stateBar.Width + g.discardPileButton.Width
- y = g.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)
-
- x = g.storeButton.X + g.storeButton.Width/2
- y = g.screenHeight - DEFAULT_BUTTON_HEIGHT
- g.storeView = ui.NewPocList(x, y, g.activePlayer.Store)
- }
-}
-
-func (g *Game) showStore() {
- g.storeView.ForceRedraw()
- g.addWidget(g.storeView)
-}
-
-func (g *Game) hideStore() {
- g.removeWidget(g.storeView)
-}
-
-func (g *Game) addWidget(w ui.Widget) {
- g.widgets = append(g.widgets, w)
-}
-
-func (g *Game) findWidget(widget ui.Widget) int {
- for i, w := range g.widgets {
- if w != widget {
- continue
- }
- return i
- }
- return -1
-}
-
-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.AvailActionPrototypes()
- if len(actions) > 0 {
- g.selectedObject = perm
- onClick := func(c *ui.Choice, x, y int) {
- g.removeActionChoice()
- g.selectedObject = actions[c.GetChoosen(x, y)]
- }
- g.actionChoice = ui.NewActionChoice(x, y, actions, onClick)
- g.addWidget(g.actionChoice)
- }
-}
-
-func (g *Game) removeActionChoice() {
- g.removeWidget(g.actionChoice)
- g.actionChoice = nil
-}
-
-func (g *Game) AddHighlight(obj interface{}) {
- switch obj := obj.(type) {
- case ui.HandCard:
- g.handLayer.AddHighlightCard(obj.C)
- case *game.Tile:
- g.mapView.AddHighlightTile(obj)
- }
-}
+var (
+ playerName string
+ startDeckPath string
+)
-func (g *Game) clearMapHighlights() {
- g.mapView.ClearPermanentsHighlights()
- g.mapView.ClearTileHighlights()
- g.mapView.ForceRedraw()
-}
+type app struct {
+ windowWidth int
+ windowHeight int
-func (g *Game) declareAction(a game.Action) {
- g.playerCtrl.SendAction(a)
- g.clearMapHighlights()
- g.handLayer.ClearHighlights()
- g.hasPriority = false
- g.hidePassButton()
- g.stateBar.ForceRedraw()
+ active ebiten.Game
+ running bool
}
-func (g *Game) showPassButton() {
- g.addWidget(g.passButton)
+func (a *app) Update() error {
+ return a.active.Update()
}
-func (g *Game) hidePassButton() {
- g.removeWidget(g.passButton)
+func (a *app) Draw(screen *ebiten.Image) {
+ a.active.Draw(screen)
}
-func (g *Game) passPriority() {
- g.declareAction(game.NewPassPriority(g.activePlayer))
+func (a *app) Layout(outsideWidth, outsideHeight int) (int, int) {
+ return a.windowWidth, a.windowHeight
}
-func (g *Game) handlePlayerNotifications() {
- n := g.playerCtrl.RecvNotification()
- for n != nil {
- switch n.Notification {
- case game.PriorityNotification:
- g.showPassButton()
- g.hasPriority = true
-
- case game.DeclaredActionNotification:
- 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()
- }
-
- case game.HandCardSelectionPrompt:
- ctx := n.Context.([]int)
- min, max := ctx[0], ctx[1]
- promptTxt := fmt.Sprintf("Select between %d and %d hand cards", min, max)
- g.prompt = ui.NewHandCardPrompt(g.screenHeight/2, g.screenWidth, promptTxt)
- g.addWidget(g.prompt)
- g.showPassButton()
- }
-
- g.stateBar.ForceRedraw()
- g.handLayer.ForceRedraw()
-
- n = g.playerCtrl.RecvNotification()
- }
-}
-
-func (g *Game) findObjectAt(x, y int) interface{} {
- // 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
- }
- }
- if obj := g.mapView.FindObjectAt(x, y); obj != nil {
- return obj
- }
-
- 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
- }
-
- switch obj := obj.(type) {
- case ui.Button:
- obj.Click(x, y)
+func main() {
+ font.InitFont()
- case *game.Tile:
- tile := obj
- if g.selectedObject != nil {
- var action game.Action
- switch obj := g.selectedObject.(type) {
- case *game.MoveActionPrototype:
- action = obj.Specialize(g.gameState, tile)
- case *game.Card:
- if obj.IsPermanent() {
- action = game.NewPlayAction(g.activePlayer, obj, tile.Position)
- }
- }
- if action != nil {
- g.declareAction(action)
- g.selectedObject = nil
- g.mapView.HighlightTile(tile)
- }
- } else {
- g.mapView.HighlightTile(tile)
- }
+ app := &app{}
+ startMenu := NewStartMenu(app)
- 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(g.gameState, perm)
- g.selectedObject = nil
- g.declareAction(action)
- g.mapView.HighlightPermanent(perm)
- }
- }
+ flag.IntVar(&app.windowWidth, "windowWidth", WINDOW_WIDTH, "the window width")
+ flag.IntVar(&app.windowHeight, "windowHeight", WINDOW_HEIGHT, "the window height")
- case ui.HandCard:
- g.selectedObject = obj.C
+ flag.StringVar(&startMenu.playerName, "player", "muhq", "player name")
+ flag.StringVar(&startDeckPath, "deck", "", "deck list to load")
- case *game.Card:
- if g.gameState.IsActivePlayer(g.activePlayer) &&
- g.gameState.ActivePhase == game.Phases.BuyPhase {
- g.declareAction(game.NewBuyAction(g.activePlayer, obj))
- }
+ flag.StringVar(&startMenu.mapPath, "map", "the-kraken", "the map to load")
+ flag.StringVar(&startMenu.remote, "remote", "", "address of remote game to connect to")
+ flag.Parse()
- 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)
- }
- }
-}
+ ebiten.SetWindowSize(app.windowWidth, app.windowHeight)
+ ebiten.SetWindowTitle("Muhq's Game")
-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 > g.screenWidth || y+700 > g.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.StoreOn(obj.Position)
- hoverHint = ui.NewPocList(x, y, poc)
- // Set a special hoverDetector resetFunc keeping the hovering store
- // around and allow card selection until the cursor leaves the store widget.
- g.hoverDetector.resetFunc = func() bool {
- x, y := ebiten.CursorPosition()
- if hoverHint.Contains(x, y) {
- return false
- }
- g.removeWidget(hoverHint)
- return true
- }
- }
- }
+ startMenu.width, startMenu.height = app.windowWidth, app.windowHeight
- 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
- }
+ if startDeckPath != "" {
+ file, err := os.Open(startDeckPath)
+ if err != nil {
+ log.Fatal(err)
}
- }
-}
-func (g *Game) Update() error {
- g.handlePlayerNotifications()
+ defer file.Close()
- x, y := ebiten.CursorPosition()
- g.hoverDetector.update(x, y)
-
- obj := g.findObjectAt(x, y)
-
- if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
- if g.prompt != nil {
- g.prompt.Add(obj)
- g.AddHighlight(obj)
- } else {
- g.handleSelection(obj, x, y)
+ deckList, err := io.ReadAll(file)
+ if err != nil {
+ log.Fatal(err)
}
- 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) {
- g.removeActionChoice()
- g.selectedObject = nil
- g.hoverDetector.reset()
- g.clearMapHighlights()
- g.handLayer.ClearHighlights()
- }
- return nil
-}
-
-func (g *Game) Draw(screen *ebiten.Image) {
- g.mapView.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()))
-}
-
-func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
- return g.screenWidth, g.screenHeight
-}
-
-func main() {
- font.InitFont()
- g := NewGameFromMap("the-kraken")
-
- ebiten.SetWindowSize(g.screenWidth, g.screenHeight)
- ebiten.SetWindowTitle("Muhq's Game")
-
- nautics := `1 nautics/barge
+ startMenu.startDeck = string(deckList)
+ } else {
+ startMenu.startDeck = `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)
- kraken := game.NewKraken(krakenId, g.gameState, playerColors[krakenId])
- g.gameState.Players = append(g.gameState.Players, kraken)
-
- go func() {
- w := g.gameState.Loop()
- log.Println("Winners are", w)
- }()
+ }
- if err := ebiten.RunGame(g); err != nil {
+ app.active = startMenu
+ if err := ebiten.RunGame(app); err != nil {
log.Fatal(err)
}
}
diff --git a/go/client/startMenu.go b/go/client/startMenu.go
new file mode 100644
index 00000000..07c4f92b
--- /dev/null
+++ b/go/client/startMenu.go
@@ -0,0 +1,92 @@
+package main
+
+import (
+ "log"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/inpututil"
+ // "muhq.space/muhqs-game/go/game"
+ "muhq.space/muhqs-game/go/ui"
+)
+
+const (
+ START_BUTTON_WIDTH = 150
+ START_BUTTON_HEIGHT = 50
+)
+
+type StartMenu struct {
+ app *app
+ width, height int
+
+ playerName string
+ startDeck string
+ remote string
+ mapPath string
+ widgets []ui.Widget
+}
+
+func NewStartMenu(app *app) *StartMenu {
+ m := &StartMenu{app: app}
+
+ return m
+}
+
+func (m *StartMenu) build () {
+ m.widgets = append(m.widgets, ui.NewSimpleButton(
+ (m.width - START_BUTTON_WIDTH) / 2,
+ (m.height - START_BUTTON_HEIGHT) / 2,
+ 150,
+ 50,
+ "Start",
+ func(*ui.SimpleButton) {
+ m.app.active = m.next()
+ }))
+}
+
+func (m *StartMenu) Update() error {
+ if m.widgets == nil {
+ m.build()
+ }
+
+ x, y := ebiten.CursorPosition()
+ if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
+ for _, w := range m.widgets {
+ if !w.Contains(x, y) {
+ continue
+ }
+
+ if btn, ok := w.(ui.Button); ok {
+ btn.Click(x, y)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (m *StartMenu) Draw(screen *ebiten.Image) {
+ for _, w := range m.widgets {
+ w.Draw(screen)
+ }
+}
+
+func (m *StartMenu) Layout(width, height int) (int, int) {
+ return m.width, m.height
+}
+
+func (m *StartMenu) next() ebiten.Game {
+ if m.remote != "" {
+ log.Fatal("Remote games are currently not implemented")
+ }
+
+ g := newGame(m.app).loadMap(m.mapPath).addActivePlayer(m.playerName, m.startDeck)
+
+ if m.mapPath == "the-kraken" {
+ idx := len(g.gameState.Players)
+ color := playerColors[idx]
+ g.gameState.AddKraken()
+ g.gameState.Players[idx].Color = color
+ }
+
+ return g.Start()
+}
diff --git a/go/game/action.go b/go/game/action.go
index a2ca038e..0f7a1294 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -2,7 +2,6 @@ package game
import (
"fmt"
- "log"
)
type ActionPrototype interface {
@@ -217,7 +216,6 @@ type BuyAction struct {
}
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)
}
diff --git a/go/game/cardParsing.go b/go/game/cardParsing.go
index af8e1560..3a316c4f 100644
--- a/go/game/cardParsing.go
+++ b/go/game/cardParsing.go
@@ -2,6 +2,7 @@ package game
import (
"log"
+ "regexp"
"strconv"
"strings"
)
@@ -39,6 +40,11 @@ func newParsedCardImplementation(c *Card) *parsedCardImplementation {
for _, effect := range c.getEffects() {
impl.parseEffect(effect)
}
+
+ for _, fullAction := range c.getCanonicalValues("full_action") {
+ impl.parseFullAction(fullAction)
+ }
+
return impl
}
@@ -48,6 +54,38 @@ func (impl *parsedCardImplementation) parseEffect(effect string) {
}
}
+func (impl *parsedCardImplementation) parseFullAction(fullAction string) {
+ var resolveProtos []func(*State, *Unit) func(*State)
+ gainRessourceRegex := regexp.MustCompile(`gain [1-9][0-9]* resource`)
+
+ if find := gainRessourceRegex.Find([]byte(fullAction)); find != nil {
+ tokens := strings.SplitN(string(find), " ", 3)
+ gain, err := strconv.Atoi(tokens[1])
+ if err != nil {
+ log.Fatalf("Failed to parse resource gain full action: %s", err)
+ }
+
+ resolveProto := func(_ *State, u *Unit) func(*State) {
+ return func(*State) {
+ u.controller.gainResource(gain)
+ }
+ }
+ resolveProtos = append(resolveProtos, resolveProto)
+ }
+
+ if resolveProtos != nil {
+ impl.genFullActionPrototypes = func(u *Unit) []*FullActionPrototype {
+ fullActionPrototypes := make([]*FullActionPrototype, 0, len(resolveProtos))
+
+ for _, proto := range resolveProtos {
+ fullActionPrototypes = append(fullActionPrototypes, NewFullActionPrototype(u, proto))
+ }
+
+ return fullActionPrototypes
+ }
+ }
+}
+
func parseUnitConstrain(constrain string) func(ctx interface{}, u *Unit) bool {
enemy := strings.Contains(constrain, "enemy")
if enemy {
diff --git a/go/game/kraken.go b/go/game/kraken.go
index 177423ff..6ea22876 100644
--- a/go/game/kraken.go
+++ b/go/game/kraken.go
@@ -1,7 +1,6 @@
package game
import (
- "image/color"
"log"
"strings"
"sync"
@@ -27,10 +26,10 @@ const (
// 2 kraken/tides_change!
// 2 kraken/unholy_cannonball`
-var KrakenDecklist = `2 kraken/frost_pylon`
+var KrakenDecklist = `2 kraken/clownfish`
-func NewKraken(id int, s *State, c color.Color) *Player {
- kraken := NewPlayer(id, KRAKEN_NAME, NewDeck(), s, c)
+func NewKraken(id int, s *State) *Player {
+ kraken := NewPlayer(id, KRAKEN_NAME, NewDeck(), s)
d := NewDeckFromDeckList(KrakenDecklist)
d.Shuffle(s.Rand)
kraken.Deck = d
@@ -49,14 +48,20 @@ func NewKraken(id int, s *State, c color.Color) *Player {
return kraken
}
+func (s *State) AddKraken() {
+ kraken := NewKraken(len(s.Players)+1, s)
+ s.Players = append(s.Players, kraken)
+}
+
type KrakenControl struct {
kraken *Player
actions chan Action
syncGameState *sync.WaitGroup
+ turn int
}
func addKrakenControl(kraken *Player) {
- ctrl := &KrakenControl{kraken, make(chan Action), new(sync.WaitGroup)}
+ ctrl := &KrakenControl{kraken, make(chan Action), new(sync.WaitGroup), 0}
kraken.Ctrl = ctrl
ctrl.syncGameState.Add(1)
@@ -78,6 +83,10 @@ func (ctrl *KrakenControl) krakenTurn() {
kraken := ctrl.kraken
ctrl.awaitGameStateSync()
+ ctrl.turn++
+ if ctrl.turn != kraken.Turn {
+ log.Fatalf("Ai turn out of sync %d != %d", ctrl.turn, kraken.Turn)
+ }
for {
drawn := kraken.Deck.Draw(1)
diff --git a/go/game/player.go b/go/game/player.go
index b6521d50..0eb474e9 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -24,9 +24,7 @@ type Player struct {
Ctrl PlayerControl
}
-func NewPlayer(id int, name string, deck *Deck,
- gameState *State, color color.Color,
-) *Player {
+func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
store := NewStore()
deck.MoveInto(store)
@@ -41,7 +39,6 @@ func NewPlayer(id int, name string, deck *Deck,
Deck: NewDeckFromDeckList(gameState.Map.StartDeckList),
Store: store,
gameState: gameState,
- Color: color,
Ctrl: nil,
}
return &p
@@ -55,8 +52,12 @@ func (p *Player) resourceGain() int {
}
}
+func (p *Player) gainResource(gain int) {
+ p.Resource += gain
+}
+
func (p *Player) Upkeep() {
- p.Resource += p.resourceGain()
+ p.gainResource(p.resourceGain())
// TODO: handle upkeep triggers
diff --git a/go/game/state.go b/go/game/state.go
index d0a516c9..816fc0d0 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -2,7 +2,6 @@ package game
import (
"fmt"
- "image/color"
"log"
"math/rand"
"strconv"
@@ -37,8 +36,8 @@ func NewState() *State {
return s
}
-func (s *State) AddNewPlayer(name string, deck *Deck, color color.Color) {
- p := NewPlayer(len(s.Players)+1, name, deck, s, color)
+func (s *State) AddNewPlayer(name string, deck *Deck) {
+ p := NewPlayer(len(s.Players)+1, name, deck, s)
s.Players = append(s.Players, p)
}
@@ -409,7 +408,6 @@ func (s *State) WaterSpawnTiles(p *Player) []*Tile {
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) {