aboutsummaryrefslogtreecommitdiff
path: root/go
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-24 00:27:50 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:44 +0100
commit73d40903aecad3efcf7c6055c133f0db86558203 (patch)
treee9c8f00fb186e270da92f273f570579f2d0b0d95 /go
parentfbba48698d8f8b83dfd7d914b0e37338e0d4e2ef (diff)
downloadmuhqs-game-73d40903aecad3efcf7c6055c133f0db86558203.tar.gz
muhqs-game-73d40903aecad3efcf7c6055c133f0db86558203.zip
intermediate commit
Diffstat (limited to 'go')
-rw-r--r--go/Makefile11
-rw-r--r--go/assets/retrieve.go21
-rw-r--r--go/client/main.go185
-rw-r--r--go/font/font.go (renamed from go/ui/font.go)2
-rw-r--r--go/game/action.go111
-rw-r--r--go/game/artifact.go5
-rw-r--r--go/game/card.go47
-rw-r--r--go/game/deck.go22
-rw-r--r--go/game/discardPiles.go4
-rw-r--r--go/game/hand.go29
-rw-r--r--go/game/kraken.go57
-rw-r--r--go/game/map.go23
-rw-r--r--go/game/pileOfCards.go59
-rw-r--r--go/game/player.go51
-rw-r--r--go/game/playerControl.go66
-rw-r--r--go/game/range.go5
-rw-r--r--go/game/stack.go10
-rw-r--r--go/game/state.go187
-rw-r--r--go/game/store.go7
-rw-r--r--go/game/tile.go17
-rw-r--r--go/game/unit.go25
-rw-r--r--go/game/winCondition.go36
-rw-r--r--go/ui/buffer.go39
-rw-r--r--go/ui/button.go45
-rw-r--r--go/ui/choice.go8
-rw-r--r--go/ui/hand.go68
-rw-r--r--go/ui/mapView.go19
-rw-r--r--go/ui/stateBar.go42
-rw-r--r--go/ui/textBox.go42
-rw-r--r--go/ui/widget.go40
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
}