diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-24 00:27:50 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:44 +0100 |
| commit | 73d40903aecad3efcf7c6055c133f0db86558203 (patch) | |
| tree | e9c8f00fb186e270da92f273f570579f2d0b0d95 /go | |
| parent | fbba48698d8f8b83dfd7d914b0e37338e0d4e2ef (diff) | |
| download | muhqs-game-73d40903aecad3efcf7c6055c133f0db86558203.tar.gz muhqs-game-73d40903aecad3efcf7c6055c133f0db86558203.zip | |
intermediate commit
Diffstat (limited to 'go')
| -rw-r--r-- | go/Makefile | 11 | ||||
| -rw-r--r-- | go/assets/retrieve.go | 21 | ||||
| -rw-r--r-- | go/client/main.go | 185 | ||||
| -rw-r--r-- | go/font/font.go (renamed from go/ui/font.go) | 2 | ||||
| -rw-r--r-- | go/game/action.go | 111 | ||||
| -rw-r--r-- | go/game/artifact.go | 5 | ||||
| -rw-r--r-- | go/game/card.go | 47 | ||||
| -rw-r--r-- | go/game/deck.go | 22 | ||||
| -rw-r--r-- | go/game/discardPiles.go | 4 | ||||
| -rw-r--r-- | go/game/hand.go | 29 | ||||
| -rw-r--r-- | go/game/kraken.go | 57 | ||||
| -rw-r--r-- | go/game/map.go | 23 | ||||
| -rw-r--r-- | go/game/pileOfCards.go | 59 | ||||
| -rw-r--r-- | go/game/player.go | 51 | ||||
| -rw-r--r-- | go/game/playerControl.go | 66 | ||||
| -rw-r--r-- | go/game/range.go | 5 | ||||
| -rw-r--r-- | go/game/stack.go | 10 | ||||
| -rw-r--r-- | go/game/state.go | 187 | ||||
| -rw-r--r-- | go/game/store.go | 7 | ||||
| -rw-r--r-- | go/game/tile.go | 17 | ||||
| -rw-r--r-- | go/game/unit.go | 25 | ||||
| -rw-r--r-- | go/game/winCondition.go | 36 | ||||
| -rw-r--r-- | go/ui/buffer.go | 39 | ||||
| -rw-r--r-- | go/ui/button.go | 45 | ||||
| -rw-r--r-- | go/ui/choice.go | 8 | ||||
| -rw-r--r-- | go/ui/hand.go | 68 | ||||
| -rw-r--r-- | go/ui/mapView.go | 19 | ||||
| -rw-r--r-- | go/ui/stateBar.go | 42 | ||||
| -rw-r--r-- | go/ui/textBox.go | 42 | ||||
| -rw-r--r-- | go/ui/widget.go | 40 |
30 files changed, 1009 insertions, 274 deletions
diff --git a/go/Makefile b/go/Makefile index c38e3143..88c32ebe 100644 --- a/go/Makefile +++ b/go/Makefile @@ -4,15 +4,16 @@ APPS := server client OBJS := $(foreach app,$(APPS),$(app)/$(app)) WASM := client/client.wasm webtools/webtools.wasm -GOFMT := go fmt - -.PHONY: all fmt test check $(OBJS) $(WASM) -all: $(OBJS) $(WASM) +GOFMT := gofumpt -l -w . .PHONY: client server client: client/client server: server/server +.PHONY: all fmt test check $(OBJS) $(WASM) +all: $(OBJS) $(WASM) + + $(OBJS): @echo Building $(@F) @pushd $(@D) >/dev/null && go build && popd >/dev/null @@ -23,7 +24,7 @@ $(OBJS): fmt: @set -e; for package in $(PACKAGES) webtools; do \ - pushd $$package >/dev/null && $(GOFMT) && popd >/dev/null; \ + pushd $$package >/dev/null && $(GOFMT); popd >/dev/null; \ done test: diff --git a/go/assets/retrieve.go b/go/assets/retrieve.go index 492a983d..cb062196 100644 --- a/go/assets/retrieve.go +++ b/go/assets/retrieve.go @@ -1,6 +1,7 @@ package assets import ( + "image/color" "image/png" "log" "net/http" @@ -8,6 +9,9 @@ import ( "strings" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + + "muhq.space/muhqs-game/go/font" ) const ( @@ -15,7 +19,7 @@ const ( BASE_URL string = "localhost:8000/" IMAGE_URL_PART string = "html/build/latex-build" TILE_URL_PART string = "assets/tiles" - SYMBOL_URL_PART string = "assets/symbols" + SYMBOL_URL_PART string = "assets/symbols/40x40" ) func retrieveAsset(path string) *ebiten.Image { @@ -25,9 +29,12 @@ func retrieveAsset(path string) *ebiten.Image { log.Println(err) return nil } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil + } + img, err := png.Decode(resp.Body) if err != nil { log.Fatal("decoding ", err) @@ -81,3 +88,13 @@ func GetSymbol(symbol string) *ebiten.Image { } return img } + +func GetGenericSymbol(symbol string) *ebiten.Image { + img, found := symbols[symbol] + if !found { + genericBase := GetSymbol("generic_base") + img = ebiten.NewImageFromImage(genericBase) + text.Draw(img, symbol, font.Font, 5, 27, color.Black) + } + return img +} diff --git a/go/client/main.go b/go/client/main.go index c8f2e46d..f2b7e36f 100644 --- a/go/client/main.go +++ b/go/client/main.go @@ -9,7 +9,7 @@ import ( "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" - "muhq.space/muhqs-game/go/assets" + "muhq.space/muhqs-game/go/font" "muhq.space/muhqs-game/go/game" "muhq.space/muhqs-game/go/ui" ) @@ -24,23 +24,18 @@ const ( STACK_BUFFER_WIDTH = 300 HOVER_THRESHOLD = 60 -) - -type HandLayer struct{} -func (l *HandLayer) Draw(screen *ebiten.Image) { - cardImg := assets.GetCard("base/archer", "en") - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(0, float64(screenHeight)) - op.GeoM.Scale(0.5, 0.5) - screen.DrawImage(cardImg, op) -} + HAND_VIEW_HEIGHT = 500 + HAND_VIEW_WIDTH = 700 -var ( - playerGreen = color.RGBA{1, 0xff, 1, 0xff} - playerBlue = color.RGBA{1, 1, 0xff, 0xff} + STATE_BAR_WIDTH = 50 ) +var playerColors = []color.RGBA{ + {1, 0xff, 1, 0xff}, + {1, 1, 0xff, 0xff}, +} + type hoverDetector struct { last_x, last_y int ticks int @@ -70,9 +65,14 @@ func (h *hoverDetector) startedHovering() bool { type Game struct { gameState *game.State - handLayer *HandLayer + activePlayer *game.Player + playerCtrl *game.ChanPlayerControl + hasPriority bool + handLayer *ui.HandView + stateBar *ui.StateBar mapView *ui.MapView - resolveButton *ui.SimpleButton + actionChoice *ui.Choice + passButton *ui.SimpleButton stackBuffer *ui.Buffer widgets []ui.Widget selectedObject interface{} @@ -84,13 +84,14 @@ func NewGame() *Game { g.widgets = []ui.Widget{} g.gameState = game.NewState() - g.resolveButton = ui.NewSimpleButton(screenWidth-125, screenHeight-DEFAULT_BUTTON_HEIGHT, - 125, DEFAULT_BUTTON_HEIGHT, func(*ui.SimpleButton) { - g.resolveAction() - }, "Resolve") - g.addWidget(g.resolveButton) + g.passButton = ui.NewSimpleButton(screenWidth-100, screenHeight-DEFAULT_BUTTON_HEIGHT, + 100, DEFAULT_BUTTON_HEIGHT, "Pass", func(*ui.SimpleButton) { + if g.hasPriority { + g.passPriority() + } + }) - g.stackBuffer = ui.NewBuffer(screenWidth-STACK_BUFFER_WIDTH, g.resolveButton.Y-400, + g.stackBuffer = ui.NewBuffer(screenWidth-STACK_BUFFER_WIDTH, g.passButton.Y-400, STACK_BUFFER_WIDTH, 400) return g } @@ -110,8 +111,21 @@ func (g *Game) loadMap(mapName string) { g.mapView = ui.NewMapView(g.gameState) } -func (g *Game) addPlayer(name string, deck *game.Deck, color color.Color) { - g.gameState.AddNewPlayer(name, deck, color) +// 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, store *game.Store) *game.Player { + color := playerColors[len(g.gameState.Players)] + g.gameState.AddNewPlayer(name, deck, game.NewStore(), color) + return g.getPlayer(name) } func (g *Game) getPlayer(name string) *game.Player { @@ -123,6 +137,20 @@ func (g *Game) getPlayer(name string) *game.Player { return nil } +func (g *Game) initPlayerUi(player *game.Player) { + g.activePlayer = player + g.playerCtrl = game.NewChanPlayerControl() + player.Ctrl = g.playerCtrl + + g.handLayer = ui.NewHandView(STATE_BAR_WIDTH, g.mapView.GetHeight(), + HAND_VIEW_WIDTH, screenHeight-g.mapView.GetHeight(), player.Hand) + g.widgets = append(g.widgets, g.handLayer) + + g.stateBar = ui.NewStateBar(0, screenHeight-HAND_VIEW_HEIGHT, + STATE_BAR_WIDTH, HAND_VIEW_HEIGHT, g.gameState, player) + g.widgets = append(g.widgets, g.stateBar) +} + func (g *Game) addWidget(w ui.Widget) { g.widgets = append(g.widgets, w) } @@ -140,6 +168,24 @@ func (g *Game) removeWidget(widget ui.Widget) { } } +func (g *Game) addActionChoice(perm game.Permanent) { + actions := perm.GetAvailActionPrototypes() + 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(500, 200, actions, onClick) + g.addWidget(g.actionChoice) + } +} + +func (g *Game) removeActionChoice() { + g.removeWidget(g.actionChoice) + g.actionChoice = nil +} + func (g *Game) clearMapHighlights() { g.mapView.ClearPermanentsHighlights() g.mapView.ClearTileHighlights() @@ -147,18 +193,34 @@ func (g *Game) clearMapHighlights() { } func (g *Game) declareAction(a game.Action) { - a.Declare(g.gameState) + g.playerCtrl.SendAction(a) g.clearMapHighlights() - g.stackBuffer.AddLine(a.String()) + g.hasPriority = false + g.removeWidget(g.passButton) + g.stateBar.ForceRedraw() } -func (g *Game) resolveAction() { - if g.gameState.Stack.IsEmpty() { - return +func (g *Game) passPriority() { + g.declareAction(game.NewPassPriorityAction(g.activePlayer)) +} + +func (g *Game) handlePlayerNotifications() { + n := g.playerCtrl.RecvNotification() + for n != nil { + log.Printf("Recv player notification %v\n", n) + switch n.Notification { + case game.PriorityNotification: + g.addWidget(g.passButton) + g.hasPriority = true + case game.DeclaredActionNotification: + g.stackBuffer.AddLine(n.Action.String()) + case game.ResolvedActionNotification: + g.clearMapHighlights() + g.stackBuffer.RemoveLast() + g.mapView.ForceRedraw() + } + n = g.playerCtrl.RecvNotification() } - g.gameState.Stack.Pop() - g.clearMapHighlights() - g.stackBuffer.RemoveLast() } func (g *Game) findObjectAt(x, y int) interface{} { @@ -175,6 +237,8 @@ func (g *Game) findObjectAt(x, y int) interface{} { } func (g *Game) Update() error { + g.handlePlayerNotifications() + x, y := ebiten.CursorPosition() g.hoverDetector.update(x, y) @@ -195,11 +259,19 @@ func (g *Game) Update() error { 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) - g.selectedObject = nil + 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) } } else { @@ -209,15 +281,7 @@ func (g *Game) Update() error { case game.Permanent: perm := obj if g.selectedObject == nil { - actions := perm.GetAvailActionPrototypes() - if len(actions) > 0 { - g.selectedObject = perm - onClick := func(c *ui.Choice, x, y int) { - g.removeWidget(c) - g.selectedObject = actions[c.GetChoosen(x, y)] - } - g.addWidget(ui.NewActionChoice(500, 200, actions, onClick)) - } + g.addActionChoice(perm) } else { switch obj := g.selectedObject.(type) { case *game.AttackActionPrototype: @@ -228,6 +292,9 @@ func (g *Game) Update() error { } } + case ui.HandCard: + g.selectedObject = obj.C + default: log.Fatalf("Object of type %T not handled", obj) } @@ -238,6 +305,11 @@ func (g *Game) Update() error { case *game.AttackActionPrototype: perms := obj.Unit.AttackablePermanents(g.gameState.Map) g.mapView.HighlightPermanents(perms) + case *game.Card: + if obj.IsPermanent() { + spawnTiles := g.gameState.SpawnTiles(g.activePlayer) + g.mapView.HighlightTiles(spawnTiles) + } } } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil { var hoverHint *ui.TextBox @@ -255,8 +327,9 @@ func (g *Game) Update() error { } } } else if inpututil.IsKeyJustPressed(ebiten.KeySpace) { - g.resolveAction() + g.passButton.Click(0, 0) } else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + g.removeActionChoice() g.selectedObject = nil g.hoverDetector.reset() g.clearMapHighlights() @@ -265,7 +338,6 @@ func (g *Game) Update() error { } func (g *Game) Draw(screen *ebiten.Image) { - g.handLayer.Draw(screen) g.mapView.Draw(screen) for _, b := range g.widgets { b.Draw(screen) @@ -282,20 +354,27 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { } func main() { - ui.InitFont() + font.InitFont() ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Muhq's Game") - g := NewGameFromMap("2P-ring-street") + g := NewGameFromMap("the-kraken") + + muhq := g.addPlayer("muhq", + game.NewDeckFromDeckList(g.gameState.Map.StartDeckList), + game.NewStore()) + g.initPlayerUi(muhq) - g.addPlayer("muhq", game.NewDeckFromDeckList(g.gameState.Map.StartDeckList), playerBlue) - muhq := g.getPlayer("muhq") - g.gameState.AddNewUnit("base/archer", game.Position{X: 0, Y: 0}, muhq) + enemy := g.addPlayer("enemy", + game.NewDeckFromDeckList(g.gameState.Map.StartDeckList), + game.NewStore()) + enemy.Ctrl = &game.DummyPlayerControl{enemy} - g.addPlayer("enemy", game.NewDeckFromDeckList(g.gameState.Map.StartDeckList), playerGreen) - enemy := g.getPlayer("enemy") - g.gameState.AddNewArtifact("base/palisade", game.Position{X: 1, Y: 1}, enemy) + go func() { + w := g.gameState.Loop() + log.Println("Winners are", w) + }() if err := ebiten.RunGame(g); err != nil { log.Fatal(err) diff --git a/go/ui/font.go b/go/font/font.go index 065ac0d8..1ff9b64d 100644 --- a/go/ui/font.go +++ b/go/font/font.go @@ -1,4 +1,4 @@ -package ui +package font import ( "log" diff --git a/go/game/action.go b/go/game/action.go index 58eea8da..a705a0fe 100644 --- a/go/game/action.go +++ b/go/game/action.go @@ -11,39 +11,81 @@ type ActionPrototype interface { } type Action interface { - Declare(s *State) Resolve(s *State) + PayCosts(s *State) bool String() string } +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 - FastDeclare(s *State) } type ActionBase struct { Source interface{} Card *Card resolveFunc func(s *State) + costFunc func(s *State) bool desc string } -func (a *ActionBase) Declare(s *State) { - if !s.Stack.IsEmpty() { - log.Fatalf("Slow action can not be declared while other actions are on the stack") - } - s.Stack.Push(a) -} - func (a *ActionBase) Resolve(s *State) { a.resolveFunc(s) } +func (a *ActionBase) PayCosts(s *State) bool { + return a.costFunc(s) +} + func (a *ActionBase) String() string { - if p, ok := a.Source.(Permanent); ok { + switch source := a.Source.(type) { + case Permanent: + p := source return fmt.Sprintf("%s: %s %s", p.GetController().Name, p.GetTile().Position.String(), a.desc) + case *Player: + return fmt.Sprintf("%s %s", source.Name, a.desc) + default: + return fmt.Sprintf("%v %s", source, a.desc) + } +} + +type PlayAction struct { + ActionBase + targets interface{} +} + +func NewPlayAction(p *Player, c *Card, targets interface{}) Action { + p.Hand.RemoveCard(c) + + resolveFunc := func(s *State) { + s.ResolvePlay(p, c, targets) + } + + costFunc := func(*State) bool { + var cost int + switch c.Type { + case UnitType: + cost = c.Values["upkeep"].(int) + default: + cost = c.Values["play"].(int) + } + if p.Ressource < cost { + log.Println("Cant pay", cost) + return false + } + p.Ressource -= cost + return true } - return fmt.Sprintf("%s %s", a.Source, a.desc) + + desc := fmt.Sprintf("p %s@%v", c.Name, targets) + return &ActionBase{p, c, resolveFunc, costFunc, desc} } type MoveActionPrototype struct { @@ -65,8 +107,17 @@ func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action { resolveFunc := func(s *State) { u.move(s, target) } + + costFunc := func(*State) bool { + if u.AvailMoveActions < 1 { + return false + } + u.AvailMoveActions-- + return true + } + desc := fmt.Sprintf("-> %s", target.Position.String()) - return &ActionBase{u, u.GetCard(), resolveFunc, desc} + return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc} } type AttackActionPrototype struct { @@ -84,15 +135,25 @@ func (proto *AttackActionPrototype) String() string { func (proto *AttackActionPrototype) Specialize(args ...interface{}) Action { target := args[0].(Permanent) u := proto.Unit + resolveFunc := func(s *State) { s.Fight(u, target) } + + costFunc := func(*State) bool { + if u.AvailAttackActions < 1 { + return false + } + u.AvailAttackActions-- + return true + } + desc := fmt.Sprintf("x %s", target.GetTile().Position.String()) - return &ActionBase{u, u.GetCard(), resolveFunc, desc} + return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc} } type FullActionPrototype struct { - Permanent Permanent + Unit *Unit } func (proto *FullActionPrototype) String() string { @@ -100,10 +161,18 @@ func (proto *FullActionPrototype) String() string { } func (proto *FullActionPrototype) Specialize(args ...interface{}) Action { - p := proto.Permanent + u := proto.Unit resolveFunc := args[0].(func(s *State)) + costFunc := func(*State) bool { + if u.AvailAttackActions < 1 && u.AvailMoveActions < 1{ + return false + } + u.AvailAttackActions-- + u.AvailMoveActions-- + return true + } desc := "full_action" - return &ActionBase{p, p.GetCard(), resolveFunc, desc} + return &ActionBase{u, u.GetCard(), resolveFunc, costFunc, desc} } type FreeActionPrototype struct { @@ -117,17 +186,11 @@ func (proto *FreeActionPrototype) String() string { func (proto *FreeActionPrototype) Specialize(args ...interface{}) Action { p := proto.Permanent resolveFunc := args[0].(func(s *State)) + costFunc := args[1].(func(s *State) bool) desc := "free_action" - return &FreeAction{ActionBase{p, p.GetCard(), resolveFunc, desc}} + return &FreeAction{ActionBase{p, p.GetCard(), resolveFunc, costFunc, desc}} } type FreeAction struct { ActionBase } - -func (a *FreeAction) FastDeclare(s *State) { - s.Stack.Push(a) -} - -func (a *FreeAction) Resolve(s *State) { -} diff --git a/go/game/artifact.go b/go/game/artifact.go index 452d5a6a..92a0361a 100644 --- a/go/game/artifact.go +++ b/go/game/artifact.go @@ -11,9 +11,12 @@ type Artifact struct { Solid int } -func NewArtifact(cardPath string, tile *Tile, owner *Player) *Artifact { +func NewArtifactFromPath(cardPath string, tile *Tile, owner *Player) *Artifact { card := NewCard(cardPath) + return NewArtifact(card, tile, owner) +} +func NewArtifact(card *Card, tile *Tile, owner *Player) *Artifact { a := &Artifact{NewPermanentBase(card, tile, owner), 0} tile.Permanent = a diff --git a/go/game/card.go b/go/game/card.go index 8576a29e..87bbe723 100644 --- a/go/game/card.go +++ b/go/game/card.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "path" + "strings" "gopkg.in/yaml.v3" ) @@ -18,7 +19,7 @@ const ( ArtifactType EquipmentType PotionType - ItentionType + IntentionType ) func (ct CardType) String() string { @@ -35,8 +36,8 @@ func (ct CardType) String() string { return "equipment" case PotionType: return "potion" - case ItentionType: - return "itention" + case IntentionType: + return "intention" } log.Panicf("Switch not exhausting %d", ct) return "" @@ -48,14 +49,14 @@ var CardTypes = struct { Artifact CardType Equipment CardType Potion CardType - Itention CardType + Intention CardType }{ Unit: UnitType, Spell: SpellType, Artifact: ArtifactType, Equipment: EquipmentType, Potion: PotionType, - Itention: ItentionType, + Intention: IntentionType, } var CardTypeNames = map[string]CardType{ @@ -64,18 +65,20 @@ var CardTypeNames = map[string]CardType{ "artifact": ArtifactType, "equipment": EquipmentType, "potion": PotionType, - "itention": ItentionType, + "intention": IntentionType, } const ( CARD_DATA_URL string = "data/cards" + NOT_BUYABLE int = -1 ) type Card struct { - Name string - Set SetIdentifier - Type CardType - Values map[string]interface{} + Name string + Set SetIdentifier + Type CardType + BuyCost int + Values map[string]interface{} } var cards = map[string]map[string][]byte{} @@ -103,8 +106,8 @@ func (card *Card) parseDefinition(definition []byte) { log.Fatalf("parsing %s failed with: %v\n", definition, err) } - name, ok := data["name"] - if !ok { + name, found := data["name"] + if !found { log.Fatalf("no name in card yaml %s\n", definition) } delete(data, "name") @@ -119,14 +122,20 @@ func (card *Card) parseDefinition(definition []byte) { log.Fatalf("Unexpected type %T of yml card field 'Name'\n", name) } - t, ok := data["type"] - if !ok { + t, found := data["type"] + if !found { log.Fatalf("no type in card yaml %s\n", definition) } delete(data, "type") card.Type = CardTypeNames[t.(string)] + bc, found := data["buy"] + if found { + card.BuyCost = bc.(int) + } + delete(data, "buy") + for k, v := range data { card.Values[k.(string)] = v } @@ -165,7 +174,15 @@ func NewCard(cardPath string) *Card { cards[set][cardName] = cardDefinition } - c := Card{"", SetIdentifierNames[set], UndefinedCardType, map[string]interface{}{}} + c := Card{"", SetIdentifierNames[set], UndefinedCardType, -1, map[string]interface{}{}} c.parseDefinition(cardDefinition) return &c } + +func (c *Card) Path() string { + return path.Join(c.Set.String(), strings.ToLower(c.Name)) +} + +func (c *Card) IsPermanent() bool { + return c.Type != UndefinedCardType && c.Type != SpellType && c.Type != IntentionType +} diff --git a/go/game/deck.go b/go/game/deck.go index f4290322..58a08985 100644 --- a/go/game/deck.go +++ b/go/game/deck.go @@ -9,36 +9,24 @@ import ( ) type Deck struct { - Cards []*Card + PileOfCardsBase } func NewDeck() *Deck { - d := Deck{[]*Card{}} + d := Deck{PileOfCardsBase{[]*Card{}}} return &d } -func (d *Deck) AddCards(cards []*Card) { - d.Cards = append(d.Cards, cards...) -} - -func (d *Deck) AddCard(card *Card) { - d.Cards = append(d.Cards, card) -} - -func (d *Deck) Size() int { - return len(d.Cards) -} - func (d *Deck) Shuffle() { rand.Seed(time.Now().UnixNano()) rand.Shuffle(d.Size(), func(i, j int) { - d.Cards[i], d.Cards[j] = d.Cards[j], d.Cards[i] + d.cards[i], d.cards[j] = d.cards[j], d.cards[i] }) } func (d *Deck) Draw(amount int) []*Card { - drawn := d.Cards[-amount:] - d.Cards = d.Cards[:amount] + drawn := d.cards[0:amount] + d.cards = d.cards[:amount] return drawn } diff --git a/go/game/discardPiles.go b/go/game/discardPiles.go index f7cf8994..c39a9646 100644 --- a/go/game/discardPiles.go +++ b/go/game/discardPiles.go @@ -1,10 +1,10 @@ package game type DiscardPile struct { - Cards []*Card + PileOfCardsBase } func NewDiscardPile() *DiscardPile { - dp := DiscardPile{[]*Card{}} + dp := DiscardPile{PileOfCardsBase{[]*Card{}}} return &dp } diff --git a/go/game/hand.go b/go/game/hand.go index 169c8cb3..2cbbe5b6 100644 --- a/go/game/hand.go +++ b/go/game/hand.go @@ -1,35 +1,10 @@ package game type Hand struct { - Cards []*Card + PileOfCardsBase } func NewHand() *Hand { - h := Hand{[]*Card{}} + h := Hand{PileOfCardsBase{[]*Card{}}} return &h } - -func (h *Hand) Size() int { - return len(h.Cards) -} - -func (h *Hand) AddCards(cards []*Card) { - h.Cards = append(h.Cards, cards...) -} - -func (h *Hand) AddCard(card *Card) { - h.Cards = append(h.Cards, card) -} - -func (h *Hand) RemoveCard(card *Card) { - ncards := len(h.Cards) - for i, c := range h.Cards { - if c != card { - continue - } - - h.Cards[i] = h.Cards[ncards-1] - h.Cards = h.Cards[:ncards-1] - return - } -} diff --git a/go/game/kraken.go b/go/game/kraken.go new file mode 100644 index 00000000..d88d87a3 --- /dev/null +++ b/go/game/kraken.go @@ -0,0 +1,57 @@ +package game + +import ( + "image/color" +) + +var KrakenDecklist = `angry_squid +chachalot +clownfish +deja_vu! +devour_the_poor! +dolphin +drowned_sailor +flying_dutchmen +frost_pylon +gigantic_hail +ice_berg +sailfish +sold_out! +suprise! +tentacle_slap +the_kraken +tides_change! +unholy_cannonball` + +func NewKraken(id int, s *State, c color.Color) *Player { + d := NewDeckFromDeckList(KrakenDecklist) + kraken := NewPlayer(id, "The Kraken", d, NewStore(), s, c) + kraken.DrawPerTurn = 0 + kraken.Ctrl = &KrakenControl{kraken} + return kraken +} + +type KrakenControl struct { + kraken *Player +} + +func (ctrl *KrakenControl) RecvAction() Action { + if ctrl.kraken.gameState.ActivePlayerId == ctrl.kraken.Id { + return NewPassPriorityAction(ctrl.kraken) + } + + // TODO: implement AI + return NewPassPriorityAction(ctrl.kraken) +} + +func (*KrakenControl) SendNotification(PlayerNotification) { +} + +func (s *State) FindKraken() *Player { + for _, p := range s.Players { + if p.Name == "The Kraken" { + return p + } + } + return nil +} diff --git a/go/game/map.go b/go/game/map.go index c84dc519..5783c9dd 100644 --- a/go/game/map.go +++ b/go/game/map.go @@ -29,11 +29,16 @@ type MapYml struct { Symbols map[string]string `yaml:"symbols"` } +func DummyWinCondition(*State) []*Player { + return []*Player{} +} + type Map struct { Tiles [][]Tile symbols map[string]string RessourceGain int StartDeckList string + WinCondition func(*State) []*Player } func readMap(r io.Reader) (*Map, error) { @@ -54,7 +59,7 @@ func ParseMap(data []byte) (*Map, error) { mapYml.Symbols[" "] = "neutral" - m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK} + m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK, DummyWinCondition} rows := strings.Split(mapYml.Map, "\n") for y := 0; y < len(rows); y++ { @@ -187,6 +192,9 @@ func LoadMap(file string) (*Map, error) { } func (m *Map) TileAt(p Position) *Tile { + if p.Y >= len(m.Tiles) || p.X >= len(m.Tiles[p.Y]) { + return nil + } return &m.Tiles[p.Y][p.X] } @@ -226,3 +234,16 @@ func (m *Map) RandomTileFromSymbols(symbols []string) (Position, error) { } return m.RandomTile(types), nil } + +func (m *Map) FilterTiles(filter func(t *Tile) bool) []*Tile { + tiles := []*Tile{} + for y := 0; y < len(m.Tiles); y++ { + for x := 0; x < len(m.Tiles[y]); x++ { + tile := &m.Tiles[y][x] + if filter(tile) { + tiles = append(tiles, tile) + } + } + } + return tiles +} diff --git a/go/game/pileOfCards.go b/go/game/pileOfCards.go new file mode 100644 index 00000000..807235a5 --- /dev/null +++ b/go/game/pileOfCards.go @@ -0,0 +1,59 @@ +package game + +type PileOfCards interface { + Size() int + IsEmpty() bool + GetCards() []*Card + AddCards(cards []*Card) + AddCard(card *Card) + RemoveCard(card *Card) + MoveCard(card *Card, into PileOfCards) + MoveInto(into PileOfCards) +} + +type PileOfCardsBase struct { + cards []*Card +} + +func (poc *PileOfCardsBase) Size() int { + return len(poc.cards) +} + +func (poc *PileOfCardsBase) IsEmpty() bool { + return poc.Size() == 0 +} + +func (poc *PileOfCardsBase) GetCards() []*Card { + return poc.cards +} + +func (poc *PileOfCardsBase) AddCards(cards []*Card) { + poc.cards = append(poc.cards, cards...) +} + +func (poc *PileOfCardsBase) AddCard(card *Card) { + poc.cards = append(poc.cards, card) +} + +func (poc *PileOfCardsBase) RemoveCard(card *Card) { + ncards := poc.Size() + for i, c := range poc.cards { + if c != card { + continue + } + + poc.cards[i] = poc.cards[ncards-1] + poc.cards = poc.cards[:ncards-1] + return + } +} + +func (poc *PileOfCardsBase) MoveCard(c *Card, into PileOfCards) { + poc.RemoveCard(c) + into.AddCard(c) +} + +func (poc *PileOfCardsBase) MoveInto(into PileOfCards) { + into.AddCards(poc.cards) + poc.cards = []*Card{} +} diff --git a/go/game/player.go b/go/game/player.go index 78ba69a7..1e8c32f9 100644 --- a/go/game/player.go +++ b/go/game/player.go @@ -11,21 +11,66 @@ const ( type Player struct { Id int Name string + Turn int + DrawPerTurn int Hand *Hand DiscardPile *DiscardPile Deck *Deck + Store *Store Ressource int gameState *State Color color.Color + Ctrl PlayerControl } -func NewPlayer(id int, name string, deck *Deck, gameState *State, color color.Color) *Player { - p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState, color} +func NewPlayer(id int, name string, deck *Deck, store *Store, gameState *State, color color.Color) *Player { + p := Player{id, name, 0, MAX_DRAW, NewHand(), NewDiscardPile(), deck, store, 0, gameState, color, nil} return &p } +func (p *Player) Upkeep() { + p.Ressource += p.gameState.Map.RessourceGain + + // TODO: handle upkeep triggers + + for _, unit := range p.gameState.Units { + if unit.Controller != p { + continue + } + + // TODO: let players decide if they want to pay the upkeep + p.Ressource -= unit.Upkeep + } +} + +func (p *Player) ActionPhase() { + a := PromptAction(p.Ctrl) + for _, ok := a.(*PassPriorityAction); !ok; { + p.gameState.DeclareAction(a) + p.gameState.Stack.Resolve() + a = PromptAction(p.Ctrl) + } +} + +func (p *Player) buy(c *Card) { + p.Ressource -= c.BuyCost + p.DiscardPile.AddCard(c) +} + +func (p *Player) BuyStep() { + // TODO: select card to buy +} + +func (p *Player) DiscardCard(c *Card) { + p.Hand.MoveCard(c, p.DiscardPile) +} + +func (p *Player) DiscardStep() { + // TODO: select cards to discard +} + func (p *Player) Draw() { - toDraw := MAX_DRAW - p.Hand.Size() + toDraw := p.DrawPerTurn - p.Hand.Size() if toDraw < 1 { return } diff --git a/go/game/playerControl.go b/go/game/playerControl.go new file mode 100644 index 00000000..2491dab7 --- /dev/null +++ b/go/game/playerControl.go @@ -0,0 +1,66 @@ +package game +import "log" + +type PlayerNotificationType int + +const ( + DeclaredActionNotification = iota + ResolvedActionNotification + PriorityNotification +) + +type PlayerNotification struct { + Notification PlayerNotificationType + Action Action +} + +func NewPriorityNotification() PlayerNotification { + return PlayerNotification{PriorityNotification, nil} +} + +func NewDeclaredActionNotification(a Action) PlayerNotification { + return PlayerNotification{DeclaredActionNotification, a} +} + +func NewResolvedActionNotification(a Action) PlayerNotification { + return PlayerNotification{ResolvedActionNotification, a} +} + +type PlayerControl interface { + RecvAction() Action + SendNotification(PlayerNotification) +} + +type DummyPlayerControl struct{ P *Player } + +func (c *DummyPlayerControl) RecvAction() Action { return NewPassPriorityAction(c.P) } +func (*DummyPlayerControl) SendNotification(PlayerNotification) { return } + +type ChanPlayerControl struct { + Actions chan Action + Notifications chan PlayerNotification +} + +func (c *ChanPlayerControl) SendAction(a Action) { c.Actions <- a } +func (c *ChanPlayerControl) RecvAction() Action { return <-c.Actions } +func (c *ChanPlayerControl) SendNotification(n PlayerNotification) { c.Notifications <- n } +func (c *ChanPlayerControl) RecvNotification() *PlayerNotification { + select { + case n := <-c.Notifications: + return &n + default: + return nil + } +} + +func NewChanPlayerControl() *ChanPlayerControl { + a := make(chan Action) + n := make(chan PlayerNotification) + return &ChanPlayerControl{a, n} +} + +func PromptAction(ctrl PlayerControl) Action { + log.Println("PromptAction") + ctrl.SendNotification(NewPriorityNotification()) + return ctrl.RecvAction() +} diff --git a/go/game/range.go b/go/game/range.go index e12e1e67..1aa50f8a 100644 --- a/go/game/range.go +++ b/go/game/range.go @@ -41,7 +41,10 @@ func PositionsInRange(origin Position, r int, includeOrigin bool) []Position { func TilesInRange(m *Map, p Permanent, r int) []*Tile { tiles := []*Tile{} for _, pos := range PositionsInRange(p.GetTile().Position, r, false) { - tiles = append(tiles, m.TileAt(pos)) + tile := m.TileAt(pos) + if tile != nil { + tiles = append(tiles, tile) + } } return tiles } diff --git a/go/game/stack.go b/go/game/stack.go index 22c20fa3..45ba4072 100644 --- a/go/game/stack.go +++ b/go/game/stack.go @@ -18,6 +18,7 @@ func (s *Stack) IsEmpty() bool { } func (s *Stack) Push(a Action) { + s.gameState.broadcastNotification(NewDeclaredActionNotification(a)) s.Actions = append(s.Actions, a) } @@ -29,4 +30,13 @@ func (s *Stack) Pop() { a := s.Actions[l-1] s.Actions = s.Actions[:l-1] a.Resolve(s.gameState) + s.gameState.broadcastNotification(NewResolvedActionNotification(a)) +} + +func (s *Stack) Resolve() { + for !s.IsEmpty() { + log.Println("Resolve Stack") + s.gameState.AllPassing() + s.Pop() + } } diff --git a/go/game/state.go b/go/game/state.go index 5cab9458..1a6ae093 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -2,21 +2,25 @@ package game import ( "image/color" + "log" "math/rand" + "strconv" + "strings" ) type State struct { - Decks []*Deck - Stores []*Store - Map *Map - Stack *Stack - Hands []*Hand - DiscardPiles []*DiscardPile - Players []*Player - Permanents []Permanent - Units []*Unit - Artifacts []*Artifact - Rand rand.Rand + Decks []*Deck + Stores []*Store + Map *Map + Stack *Stack + Hands []*Hand + DiscardPiles []*DiscardPile + Players []*Player + ActivePlayerId int + Permanents []Permanent + Units []*Unit + Artifacts []*Artifact + Rand rand.Rand } func NewState() *State { @@ -25,15 +29,138 @@ func NewState() *State { return s } -func (s *State) AddNewPlayer(name string, deck *Deck, color color.Color) { - p := NewPlayer(len(s.Players), name, deck, s, color) +func (s *State) AddNewPlayer(name string, deck *Deck, store *Store, color color.Color) { + p := NewPlayer(len(s.Players)+1, name, deck, store, s, color) s.Players = append(s.Players, p) } -func (s *State) AddNewUnit(card string, pos Position, owner *Player) { - unit := NewUnit(card, s.Map.TileAt(pos), owner) - s.Permanents = append(s.Permanents, unit) - s.Units = append(s.Units, unit) +func (s *State) Loop() []*Player { + winners := []*Player{} + for len(winners) == 0 { + for i, p := range s.Players { + log.Printf("Starting %s turn %d\n", p.Name, p.Turn) + s.ActivePlayerId = i + p.Turn++ + + if p.Deck.IsEmpty() { + log.Printf("Shuffle %s pile into the deck\n", p.Name) + p.DiscardPile.MoveInto(p.Deck) + p.Deck.Shuffle() + } + + log.Printf("%s draw\n", p.Name) + p.Draw() + + log.Printf("%s Upkeep\n", p.Name) + p.Upkeep() + s.PhaseChange(i) + + log.Printf("%s Actions\n", p.Name) + p.ActionPhase() + s.PhaseChange(i) + + log.Printf("%s Buystep\n", p.Name) + p.BuyStep() + s.PhaseChange(i) + + log.Printf("%s Discard\n", p.Name) + p.DiscardStep() + + winners = s.Map.WinCondition(s) + } + } + + return winners +} + +func (s *State) PushAction(a Action) { + if !a.PayCosts(s) { + log.Fatalf("Cost for a could not be paid") + } + 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") + } + s.PushAction(a) +} + +func (s *State) DeclareFastAction(a Action) { + s.PushAction(a) +} + +func (s *State) AllPassing() bool { + nPlayers := len(s.Players) + for i := 0; i < nPlayers; i++ { + p := s.Players[(i+s.ActivePlayerId)%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) + default: + s.Stack.Push(a) + return false + } + } + return true +} + +func (s *State) PhaseChange(activePlaerId int) { + log.Println("Phase change") + for !s.AllPassing() { + s.Stack.Resolve() + } +} + +func (s *State) broadcastNotification(n PlayerNotification) { + log.Printf("broadcasting %v\n", n) + for _, p := range s.Players { + p.Ctrl.SendNotification(n) + } +} + +func (s *State) Upkeep(p *Player) { + p.Ressource += s.Map.RessourceGain + // TODO: handle upkeep trigger + for _, unit := range s.Units { + if unit.Controller != p { + continue + } + + // TODO: let players decide if they want to pay the upkeep + p.Ressource -= unit.Upkeep + } +} + +func (s *State) AddPermanent(perm Permanent) { + s.Permanents = append(s.Permanents, perm) + switch p := perm.(type) { + case *Unit: + s.Units = append(s.Units, p) + case *Artifact: + s.Artifacts = append(s.Artifacts, p) + } +} + +func (s *State) AddNewUnit(card *Card, pos Position, owner *Player) { + s.AddPermanent(NewUnit(card, s.Map.TileAt(pos), owner)) +} + +func (s *State) AddNewArtifact(card *Card, pos Position, owner *Player) { + s.AddPermanent(NewArtifact(card, s.Map.TileAt(pos), owner)) +} + +func (s *State) AddNewPermFromPath(cardPath string, pos Position, owner *Player) { + card := NewCard(cardPath) + switch card.Type { + case UnitType: + s.AddNewUnit(card, pos, owner) + case ArtifactType: + s.AddNewArtifact(card, pos, owner) + } } func (s *State) removePermanent(p Permanent) { @@ -66,12 +193,6 @@ func (s *State) DestroyPermanent(p Permanent) { } } -func (s *State) AddNewArtifact(card string, pos Position, owner *Player) { - artifact := NewArtifact(card, s.Map.TileAt(pos), owner) - s.Permanents = append(s.Permanents, artifact) - s.Artifacts = append(s.Artifacts, artifact) -} - func (s *State) Fight(p1, p2 Permanent) { p1.Fight(p2) p2.Fight(p1) @@ -97,3 +218,23 @@ func (s *State) EnemyUnits(player *Player) []*Unit { } return enemyUnits } + +func (s *State) ResolvePlay(p *Player, c *Card, targets interface{}) { + switch c.Type { + case UnitType: + s.AddNewUnit(c, targets.(Position), p) + } + p.DiscardPile.AddCard(c) +} + +func (s *State) SpawnTiles(p *Player) []*Tile { + spawns := s.Map.FilterTiles(func(t *Tile) bool { + if t.Type != TileTypes.Spawn { + return false + } + + return strings.HasSuffix(t.Raw, strconv.Itoa(p.Id)) || t.Raw == "player spawn" + }) + + return spawns +} diff --git a/go/game/store.go b/go/game/store.go index 3f5793fb..46c136e0 100644 --- a/go/game/store.go +++ b/go/game/store.go @@ -1,5 +1,10 @@ package game type Store struct { - Cards []Card + PileOfCardsBase +} + +func NewStore() *Store { + s := &Store{PileOfCardsBase{[]*Card{}}} + return s } diff --git a/go/game/tile.go b/go/game/tile.go index f66b4fb0..cbceb6f7 100644 --- a/go/game/tile.go +++ b/go/game/tile.go @@ -64,18 +64,25 @@ type Tile struct { Position Position Permanent Permanent Type TileType + Water bool Raw string } func INVALID_TILE() Tile { - return Tile{INVALID_POSITION(), nil, 0, ""} + return Tile{INVALID_POSITION(), nil, 0, false, ""} } -func NewTileFromString(tile string, pos Position) (Tile, error) { +func NewTileFromString(t string, pos Position) (Tile, error) { + tile := strings.ToLower(t) tokens := strings.Split(tile, " ") - tileType := TileNames[strings.ToLower(tokens[0])] - t := Tile{pos, nil, tileType, tile} - return t, nil + tileType, found:= TileNames[tokens[0]] + if !found { + if strings.Contains(tile, "spawn") { + tileType = spawn + } + } + water := strings.Contains(tile, "water") + return Tile{pos, nil, tileType, water, t}, nil } func (t *Tile) IsFree() bool { diff --git a/go/game/unit.go b/go/game/unit.go index 028ff274..02ba9830 100644 --- a/go/game/unit.go +++ b/go/game/unit.go @@ -31,18 +31,23 @@ func (a *Attack) String() string { return fmt.Sprintf("%d range %d", a.Damage, a.Range) } -func parseAttack(attack string) Attack { - tokens := strings.Split(attack, " ") +func parseAttack(attack interface{}) Attack { + if damage, ok := attack.(int); ok { + return Attack{damage, 1} + } + + attackStr := attack.(string) + tokens := strings.Split(attackStr, " ") a, err := strconv.Atoi(tokens[0]) if err != nil { - log.Fatalf("Attack %s has no valid Damage value\n", attack) + log.Fatalf("Attack %s has no valid Damage value\n", attackStr) } r := 1 if len(tokens) == 3 { r, err = strconv.Atoi(tokens[2]) if err != nil { - log.Fatalf("Attack %s has no valid Range value\n", attack) + log.Fatalf("Attack %s has no valid Range value\n", attackStr) } } return Attack{a, r} @@ -62,14 +67,12 @@ type Unit struct { AvailAttackActions int } -func NewUnit(cardPath string, tile *Tile, owner *Player) *Unit { - card := NewCard(cardPath) - +func NewUnit(card *Card, tile *Tile, owner *Player) *Unit { u := &Unit{ NewPermanentBase(card, tile, owner), card.Values["health"].(int), card.Values["movement"].(int), - parseAttack(card.Values["attack"].(string)), + parseAttack(card.Values["attack"]), card.Values["upkeep"].(int), []*FullActionPrototype{}, []*FreeActionPrototype{}, @@ -82,6 +85,10 @@ func NewUnit(cardPath string, tile *Tile, owner *Player) *Unit { return u } +func NewUnitFromPath(cardPath string, tile *Tile, owner *Player) *Unit { + return NewUnit(NewCard(cardPath), tile, owner) +} + 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) @@ -98,7 +105,7 @@ func (u *Unit) MoveRangeTiles(m *Map) []*Tile { tiles := []*Tile{} for _, pos := range PositionsInRange(u.Tile.Position, u.Movement, false) { tile := m.TileAt(pos) - if u.IsAvailableTile(tile) { + if tile != nil && u.IsAvailableTile(tile) { tiles = append(tiles, tile) } } diff --git a/go/game/winCondition.go b/go/game/winCondition.go new file mode 100644 index 00000000..fa07257f --- /dev/null +++ b/go/game/winCondition.go @@ -0,0 +1,36 @@ +package game + +import ( + "golang.org/x/exp/slices" +) + +func KingGame(s *State) []*Player { + foundKings := map[*Player]struct{}{} + for _, u := range s.Units { + if u.Card.Name != "King" { + continue + } + + foundKings[u.Owner] = struct{}{} + } + + if len(foundKings) == len(s.Players) { + return []*Player{} + } + + loosers := make([]*Player, 0, len(s.Players)) + copy(loosers, s.Players) + for p := range foundKings { + i := slices.Index(loosers, p) + loosers[i] = loosers[len(loosers)-1] + loosers = loosers[:len(loosers)-1] + } + + winners := []*Player{} + for _, p := range s.Players { + if !slices.Contains(loosers, p) { + winners = append(winners, p) + } + } + return winners +} diff --git a/go/ui/buffer.go b/go/ui/buffer.go index cc9f2377..e0e6b001 100644 --- a/go/ui/buffer.go +++ b/go/ui/buffer.go @@ -5,6 +5,8 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text" + + "muhq.space/muhqs-game/go/font" ) var ( @@ -13,47 +15,30 @@ var ( ) type Buffer struct { - X, Y int - Width, Height int - img *ebiten.Image - lines []string - pos int + WidgetBase + lines []string + pos int } func NewBuffer(x, y int, width, height int) *Buffer { - return &Buffer{x, y, width, height, nil, []string{}, 0} + b := &Buffer{NewWidgetBase(x, y, width, height), []string{}, 0} + b.renderImpl = func() *ebiten.Image { return b.render() } + return b } -func (b *Buffer) render() { +func (b *Buffer) render() *ebiten.Image { img := ebiten.NewImage(b.Width, b.Height) img.Fill(BUFFER_BACKGROUND) y := 0 for _, line := range b.lines[b.pos:] { - bounds := text.BoundString(Font, line) + bounds := text.BoundString(font.Font, line) y += bounds.Dy() if y > b.Height { break } - text.Draw(img, line, Font, 0, y, BUFFER_FOREGROUND) - } - b.img = img -} - -func (b *Buffer) Draw(screen *ebiten.Image) { - if b.img == nil { - b.render() + text.Draw(img, line, font.Font, 0, y, BUFFER_FOREGROUND) } - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(b.X), float64(b.Y)) - screen.DrawImage(b.img, op) -} - -func (b *Buffer) FindObjectAt(x, y int) interface{} { - return nil -} - -func (b *Buffer) ForceRedraw() { - b.img = nil + return img } func (b *Buffer) AddLines(lines []string) { diff --git a/go/ui/button.go b/go/ui/button.go index 93180204..97dbad22 100644 --- a/go/ui/button.go +++ b/go/ui/button.go @@ -6,43 +6,36 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/text" + + "muhq.space/muhqs-game/go/font" ) type Button interface { Widget - Contains(x, y int) bool Click(x, y int) } type SimpleButton struct { - X, Y int - Width, Height int - img *ebiten.Image - OnClick func(b *SimpleButton) + WidgetBase + label string + OnClick func(b *SimpleButton) + BG color.Color + FG color.Color } -func NewSimpleButton(x, y, w, h int, onClick func(b *SimpleButton), label string) *SimpleButton { - img := ebiten.NewImage(w, h) - - // b := text.BoundString(f, label) - ebitenutil.DrawRect(img, float64(0), float64(0), float64(w), float64(h), gray) - text.Draw(img, label, Font, 20, 25, color.White) - - return &SimpleButton{x, y, w, h, img, onClick} +func NewSimpleButton(x, y, w, h int, label string, onClick func(b *SimpleButton)) *SimpleButton { + sb := &SimpleButton{NewWidgetBase(x, y, w, h), label, onClick, gray, color.White} + sb.renderImpl = func() *ebiten.Image { return sb.render() } + return sb } -func (b *SimpleButton) Draw(screen *ebiten.Image) { - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(b.X), float64(b.Y)) - screen.DrawImage(b.img, op) -} - -func (b *SimpleButton) Click(x, y int) { - b.OnClick(b) -} +func (b *SimpleButton) render() *ebiten.Image { + img := ebiten.NewImage(b.Width, b.Height) -func (b *SimpleButton) Contains(x, y int) bool { - return x >= b.X && x <= b.X+b.Width && y >= b.Y && y <= b.Y+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) + return img } func (b *SimpleButton) FindObjectAt(x, y int) interface{} { @@ -51,3 +44,7 @@ func (b *SimpleButton) FindObjectAt(x, y int) interface{} { } return nil } + +func (b *SimpleButton) Click(x, y int) { + b.OnClick(b) +} diff --git a/go/ui/choice.go b/go/ui/choice.go index 2020ed0a..71308d1b 100644 --- a/go/ui/choice.go +++ b/go/ui/choice.go @@ -21,8 +21,8 @@ func NewChoice(x, y int, width, choiceHeight int, choices []string, c := &Choice{x, y, width, choiceHeight, choices, onClick, []Button{}} for i, choice := range c.choices { - b := NewSimpleButton(c.X, c.Y+i*c.choiceHeight, c.Width, c.choiceHeight, - choiceButtonOnClickDummy, choice) + b := NewSimpleButton(c.X, c.Y+i*c.choiceHeight, c.Width, c.choiceHeight, choice, + choiceButtonOnClickDummy) c.buttons = append(c.buttons, b) } @@ -55,6 +55,10 @@ func (c *Choice) FindObjectAt(x, y int) interface{} { return nil } +func (c *Choice) Render() *ebiten.Image { + return nil +} + func NewActionChoice(x, y int, actions []game.ActionPrototype, onClick func(*Choice, int, int)) *Choice { // TODO: use dynamic with width and height width := 200 diff --git a/go/ui/hand.go b/go/ui/hand.go new file mode 100644 index 00000000..0fdde253 --- /dev/null +++ b/go/ui/hand.go @@ -0,0 +1,68 @@ +package ui + +import ( + "log" + + "github.com/hajimehoshi/ebiten/v2" + "muhq.space/muhqs-game/go/assets" + "muhq.space/muhqs-game/go/game" +) + +const ( + HAND_CARD_WIDTH = 200 +) + +type HandCard struct { + C *game.Card +} + +type HandView struct { + WidgetBase + hand *game.Hand + scale float64 +} + +func NewHandView(x, y, width, height int, h *game.Hand) *HandView { + hv := &HandView{NewWidgetBase(x, y, width, height), h, -1} + hv.renderImpl = func() *ebiten.Image { return hv.render() } + return hv +} + +func (hv *HandView) render() *ebiten.Image { + img := ebiten.NewImage(hv.Width, hv.Height) + for i, card := range hv.hand.GetCards() { + cardImg := assets.GetCard(card.Path(), "en") + if cardImg == nil { + log.Fatalf("No image found for %s", card.Path()) + } + + if hv.scale == -1 { + b := cardImg.Bounds() + x_scale := float64(HAND_CARD_WIDTH) / float64(b.Dx()) + y_scale := float64(hv.Height) / float64(b.Dy()) + hv.scale = x_scale + if y_scale < x_scale { + hv.scale = y_scale + } + } + + op := &ebiten.DrawImageOptions{} + op.GeoM.Scale(hv.scale, hv.scale) + op.GeoM.Translate(float64(i*HAND_CARD_WIDTH), 0) + img.DrawImage(cardImg, op) + } + return img +} + +func (hv *HandView) FindObjectAt(x, y int) interface{} { + if !hv.Contains(x, y) { + return nil + } + + i := x / HAND_CARD_WIDTH + if i >= hv.hand.Size() { + return nil + } + + return HandCard{hv.hand.GetCards()[i]} +} diff --git a/go/ui/mapView.go b/go/ui/mapView.go index 39c92ae4..6c887920 100644 --- a/go/ui/mapView.go +++ b/go/ui/mapView.go @@ -1,9 +1,11 @@ package ui import ( + "fmt" "log" "math" "strings" + "unicode" "golang.org/x/exp/slices" @@ -33,6 +35,10 @@ func NewMapView(g *game.State) *MapView { return &vw } +func (mv *MapView) GetHeight() int { + return len(mv.gameState.Map.Tiles) * TILE_HEIGHT +} + func rotateStreetImg(x, y int, radian float64, op *ebiten.DrawImageOptions) { // Move the image's center to the screen's upper-left corner. // This is a preparation for rotating. When geometry matrices are applied, @@ -130,11 +136,14 @@ func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) { if vw.permanentsLayer == nil { vw.permanentsLayer = vw.newLayerImage() - for _, p := range vw.gameState.Permanents { + for i, p := range vw.gameState.Permanents { permanentSymbol := assets.GetSymbol(strings.ToLower(p.GetCard().Name)) - // TODO: Implement generic symbols if permanentSymbol == nil { - continue + 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) + } } t := p.GetTile() @@ -225,3 +234,7 @@ func (vw *MapView) HighlightPermanent(p game.Permanent) { func (vw *MapView) ClearPermanentsHighlights() { vw.HighlightPermanents([]game.Permanent{}) } + +func (*MapView) Render(Widget) *ebiten.Image { + return nil +} diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go new file mode 100644 index 00000000..e175c0c5 --- /dev/null +++ b/go/ui/stateBar.go @@ -0,0 +1,42 @@ +package ui + +import ( + "fmt" + + "github.com/hajimehoshi/ebiten/v2" + "muhq.space/muhqs-game/go/game" +) + +type StateBar struct { + X, Y int + Width, Height int + gameState *game.State + activePlayer *game.Player + ressourceView *TextBox +} + +func NewStateBar(x, y int, width, height int, s *game.State, p *game.Player) *StateBar { + sb := &StateBar{x, y, width, height, s, p, nil} + return sb +} + +func (sb *StateBar) ressourceLabel() string { + return fmt.Sprintf("%d", sb.activePlayer.Ressource) +} + +func (sb *StateBar) Draw(screen *ebiten.Image) { + if sb.ressourceView == nil { + sb.ressourceView = NewFixedTextBox(sb.X, sb.Y+sb.Height-sb.Width, sb.Width, sb.Width, + sb.ressourceLabel()) + } + sb.ressourceView.Draw(screen) +} + +func (sb *StateBar) FindObjectAt(x, y int) interface{} { + return nil +} + +func (sb *StateBar) ForceRedraw() { + sb.ressourceView.Text = sb.ressourceLabel() + sb.ressourceView = nil +} diff --git a/go/ui/textBox.go b/go/ui/textBox.go index e07440b5..5e2d140b 100644 --- a/go/ui/textBox.go +++ b/go/ui/textBox.go @@ -6,31 +6,30 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text" + "muhq.space/muhqs-game/go/font" "muhq.space/muhqs-game/go/game" ) var ( TEXTBOX_BACKGROUND = gray TEXTBOX_FOREGROUND = color.White + + TEXTBOX_AUTO_BORDER = 3 ) type TextBox struct { - X, Y int - Width, Height int - img *ebiten.Image - text string + WidgetBase + Text string } func NewFixedTextBox(x, y int, width, height int, t string) *TextBox { - tb := &TextBox{x, y, width, height, nil, t} - tb.render() + tb := &TextBox{NewWidgetBase(x, y, width, height), t} + tb.renderImpl = func() *ebiten.Image { return tb.render() } return tb } func NewAutoTextBox(x, y int, t string) *TextBox { - tb := &TextBox{x, y, -1, -1, nil, t} - tb.render() - return tb + return NewFixedTextBox(x, y, -1, -1, t) } func NewUnitInfo(x, y int, u *game.Unit) *TextBox { @@ -45,23 +44,14 @@ func NewPermInfo(x, y int, p game.Permanent) *TextBox { return NewAutoTextBox(x, y, info) } -func (tb *TextBox) render() { - b := text.BoundString(Font, tb.text) +func (tb *TextBox) render() *ebiten.Image { + b := text.BoundString(font.Font, tb.Text) if tb.Width == -1 || tb.Height == -1 { - tb.Width = b.Dx() - tb.Height = b.Dy() + tb.Width = b.Dx() + 2*TEXTBOX_AUTO_BORDER + tb.Height = b.Dy() + 2*TEXTBOX_AUTO_BORDER } - tb.img = ebiten.NewImage(tb.Width, tb.Height) - tb.img.Fill(TEXTBOX_BACKGROUND) - text.Draw(tb.img, tb.text, Font, b.Min.X, -b.Min.Y, TEXTBOX_FOREGROUND) -} - -func (tb *TextBox) Draw(screen *ebiten.Image) { - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(tb.X), float64(tb.Y)) - screen.DrawImage(tb.img, op) -} - -func (tb TextBox) FindObjectAt(x, y int) interface{} { - return nil + 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) + return img } diff --git a/go/ui/widget.go b/go/ui/widget.go index c8fb0816..d783eb36 100644 --- a/go/ui/widget.go +++ b/go/ui/widget.go @@ -5,6 +5,42 @@ import ( ) type Widget interface { - Draw(screen *ebiten.Image) - FindObjectAt(x, y int) interface{} + Draw(*ebiten.Image) + FindObjectAt(int, int) interface{} +} + +type WidgetBase struct { + X, Y int + Width, Height int + img *ebiten.Image + renderImpl func() *ebiten.Image +} + +func renderDummy() *ebiten.Image { + return nil +} + +func NewWidgetBase(x, y, width, height int) WidgetBase { + return WidgetBase{x, y, width, height, nil, renderDummy} +} + +func (w *WidgetBase) Draw(screen *ebiten.Image) { + if w.img == nil { + w.img = w.renderImpl() + } + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(w.X), float64(w.Y)) + screen.DrawImage(w.img, op) +} + +func (w *WidgetBase) ForceRedraw() { + w.img = nil +} + +func (w *WidgetBase) Contains(x, y int) bool { + return x >= 0 && y >= 0 && x >= w.X && x <= w.X+w.Width && y >= w.Y && y <= w.Y+w.Height +} + +func (w *WidgetBase) FindObjectAt(int, int) interface{} { + return nil } |
