diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-31 22:19:28 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:45 +0100 |
| commit | 3fd15c9b83fff074b588ff9688f9e49aab38cd32 (patch) | |
| tree | faf0710b755118dcd5ce7ae99fade3300e369292 /go | |
| parent | 01b8c63effcdf862f6de2ec86ab47b5adf2a49bf (diff) | |
| download | muhqs-game-3fd15c9b83fff074b588ff9688f9e49aab38cd32.tar.gz muhqs-game-3fd15c9b83fff074b588ff9688f9e49aab38cd32.zip | |
intermediate commit
Diffstat (limited to 'go')
| -rw-r--r-- | go/client/game.go | 537 | ||||
| -rw-r--r-- | go/client/main.go | 578 | ||||
| -rw-r--r-- | go/client/startMenu.go | 92 | ||||
| -rw-r--r-- | go/game/action.go | 2 | ||||
| -rw-r--r-- | go/game/cardParsing.go | 38 | ||||
| -rw-r--r-- | go/game/kraken.go | 19 | ||||
| -rw-r--r-- | go/game/player.go | 11 | ||||
| -rw-r--r-- | go/game/state.go | 6 |
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) { |
