aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-12 20:58:14 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:44 +0100
commit9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74 (patch)
treeb0ed4b509da71d0d6320f979a1eebbd91fdf7321
parent41d16fb4910fc0402b6b081302c953f7dabc8cf9 (diff)
downloadmuhqs-game-9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74.tar.gz
muhqs-game-9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74.zip
implement unit movement
-rw-r--r--assets/symbols/archer.pngbin45278 -> 18667 bytes
-rw-r--r--go/assets/retrieve.go3
-rw-r--r--go/client/main.go126
-rw-r--r--go/game/card.go151
-rw-r--r--go/game/deck.go68
-rw-r--r--go/game/discardPiles.go7
-rw-r--r--go/game/hand.go29
-rw-r--r--go/game/map.go8
-rw-r--r--go/game/permanent.go10
-rw-r--r--go/game/player.go23
-rw-r--r--go/game/range.go39
-rw-r--r--go/game/range_test.go35
-rw-r--r--go/game/set.go63
-rw-r--r--go/game/state.go36
-rw-r--r--go/game/tile.go4
-rw-r--r--go/game/unit.go89
-rw-r--r--go/go.mod11
-rw-r--r--go/go.sum9
-rw-r--r--go/ui/map.go109
-rw-r--r--go/ui/mapView.go212
20 files changed, 869 insertions, 163 deletions
diff --git a/assets/symbols/archer.png b/assets/symbols/archer.png
index e83f2d17..f641f0a6 100644
--- a/assets/symbols/archer.png
+++ b/assets/symbols/archer.png
Binary files differ
diff --git a/go/assets/retrieve.go b/go/assets/retrieve.go
index fea3d141..492a983d 100644
--- a/go/assets/retrieve.go
+++ b/go/assets/retrieve.go
@@ -22,7 +22,8 @@ func retrieveAsset(path string) *ebiten.Image {
log.Printf("loading %s\n", path)
resp, err := http.Get(PROTOCOL + path)
if err != nil {
- log.Fatal(err)
+ log.Println(err)
+ return nil
}
defer resp.Body.Close()
diff --git a/go/client/main.go b/go/client/main.go
index b91b15f7..b0696fdf 100644
--- a/go/client/main.go
+++ b/go/client/main.go
@@ -6,37 +6,122 @@ import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
+ "github.com/hajimehoshi/ebiten/v2/inpututil"
"muhq.space/muhqs-game/go/assets"
"muhq.space/muhqs-game/go/game"
"muhq.space/muhqs-game/go/ui"
)
-type Game struct{}
-
const (
- screenWidth = 1920
- screenHeight = 1080
-)
-
-var (
- m *ui.MapLayer
- players []game.Player
+ screenWidth = 1280
+ screenHeight = 1024
)
-func (g *Game) Update() error {
- return nil
+type HandLayer struct {
}
-func (g *Game) Draw(screen *ebiten.Image) {
+func (l *HandLayer) Draw(screen *ebiten.Image) {
cardImg := assets.GetCard("base/archer", "en")
// _, cardImgHeight := cardImg.Size()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(0, float64(screenHeight))
op.GeoM.Scale(0.5, 0.5)
screen.DrawImage(cardImg, op)
- m.Draw(screen)
- ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()))
+}
+
+type Game struct {
+ gameState *game.State
+ handLayer *HandLayer
+ mapView *ui.MapView
+ activeObject interface{}
+}
+
+func NewGame() *Game {
+ g := &Game{}
+ g.gameState = game.NewState()
+ return g
+}
+
+func NewGameFromMap(mapName string) *Game {
+ g := NewGame()
+ g.loadMap(mapName)
+ return g
+}
+
+func (g *Game) loadMap(mapName string) {
+ m, err := game.GetMap(mapName)
+ if err != nil {
+ log.Fatal(err)
+ }
+ g.gameState.Map = m
+ g.mapView = ui.NewMapView(g.gameState)
+}
+
+func (g *Game) addPlayer(name string, deck *game.Deck) {
+ g.gameState.AddNewPlayer(name, deck)
+}
+
+func (g *Game) getPlayer(name string) *game.Player {
+ for _, p := range g.gameState.Players {
+ if p.Name == name {
+ return p
+ }
+ }
+ return nil
+}
+
+func (g *Game) moveUnit(unit *game.Unit, tile *game.Tile) {
+ g.gameState.MoveUnit(unit, tile)
+ g.mapView.ClearTileHighlights()
+ g.mapView.ForceRedraw()
+}
+
+func (g *Game) findObjectAt(x, y int) interface{} {
+ if obj := g.mapView.FindObjectAt(x, y); obj != nil {
+ return obj
+ }
+
+ return nil
+}
+
+func (g *Game) Update() error {
+ if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
+ x, y := ebiten.CursorPosition()
+ obj := g.findObjectAt(x, y)
+ if obj != nil {
+ switch obj.(type) {
+ case *game.Tile:
+ tile := obj.(*game.Tile)
+ if g.activeObject != nil {
+ switch g.activeObject.(type) {
+ case *game.Unit:
+ unit := g.activeObject.(*game.Unit)
+ g.moveUnit(unit, tile)
+ g.activeObject = nil
+ }
+ } else {
+ g.mapView.HighlightTile(tile)
+ }
+ case *game.Unit:
+ unit := obj.(*game.Unit)
+ g.activeObject = unit
+ g.mapView.HighlightTiles(unit.MoveRangeTiles(g.gameState.Map))
+ default:
+ log.Fatalf("Object of type %T not handled", obj)
+ }
+ } else {
+ fmt.Printf("No object found at cursor position (%d, %d)\n", x, y)
+ }
+ }
+ return nil
+}
+
+func (g *Game) Draw(screen *ebiten.Image) {
+ g.handLayer.Draw(screen)
+ g.mapView.Draw(screen)
+ ebitenutil.DebugPrint(screen,
+ fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()))
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
@@ -46,13 +131,14 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
func main() {
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Muhq's Game")
- _m, err := game.GetMap("2P-ring-street")
- if err != nil {
- log.Fatal(err)
- }
- m = ui.NewMapLayerFromMap(_m)
- if err := ebiten.RunGame(&Game{}); err != nil {
+ g := NewGameFromMap("2P-ring-street")
+
+ g.addPlayer("muhq", game.NewDeckFromDeckList(g.gameState.Map.StartDeckList))
+ muhq := g.getPlayer("muhq")
+ g.gameState.AddNewUnit("base/archer", game.Position{0, 0}, muhq)
+
+ if err := ebiten.RunGame(g); err != nil {
log.Fatal(err)
}
}
diff --git a/go/game/card.go b/go/game/card.go
index 6cd20257..f77a274a 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -1,18 +1,151 @@
package game
+import (
+ "io"
+ "log"
+ "net/http"
+ "path"
+
+ "gopkg.in/yaml.v3"
+)
+
type CardType int
const (
- Unit CardType = iota + 1
- Spell
- Artifact
- Equipment
- Potion
- Itention
+ UndefinedCardType CardType = iota
+ UnitType
+ SpellType
+ ArtifactType
+ EquipmentType
+ PotionType
+ ItentionType
+)
+
+func (ct CardType) String() string {
+ switch ct {
+ case UndefinedCardType:
+ return "undefined"
+ case UnitType:
+ return "unit"
+ case SpellType:
+ return "spell"
+ case ArtifactType:
+ return "artifact"
+ case EquipmentType:
+ return "equipment"
+ case PotionType:
+ return "potion"
+ case ItentionType:
+ return "itention"
+ }
+ log.Panicf("Switch not exhausting %d", ct)
+ return ""
+}
+
+var CardTypes = struct {
+ Unit CardType
+ Spell CardType
+ Artifact CardType
+ Equipment CardType
+ Potion CardType
+ Itention CardType
+}{
+ Unit: UnitType,
+ Spell: SpellType,
+ Artifact: ArtifactType,
+ Equipment: EquipmentType,
+ Potion: PotionType,
+ Itention: ItentionType,
+}
+
+var CardTypeNames = map[string]CardType{
+ "unit": UnitType,
+ "spell": SpellType,
+ "artifact": ArtifactType,
+ "equipment": EquipmentType,
+ "potion": PotionType,
+ "itention": ItentionType,
+}
+
+const (
+ CARD_DATA_URL string = "data/cards"
)
type Card struct {
- Name string
- Set SetIdentifier
- Type CardType
+ Name string
+ Set SetIdentifier
+ Type CardType
+ Values map[string]interface{}
+}
+
+var cards = map[string]map[string][]byte{}
+
+func retrieveCardDefinition(cardPath string) []byte {
+ url := PROTOCOL + path.Join(BASE_URL, CARD_DATA_URL, cardPath+".yml")
+ log.Printf("loading %s\n", url)
+ resp, err := http.Get(url)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer resp.Body.Close()
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return data
+}
+
+func (card *Card) parseDefinition(definition []byte) {
+ data := make(map[interface{}]interface{})
+ err := yaml.Unmarshal(definition, &data)
+ if err != nil {
+ log.Fatalf("parsing %s failed with: %v\n", definition, err)
+ }
+
+ name, ok := data["name"]
+ if !ok {
+ log.Fatalf("no name in card yaml %s\n", definition)
+ }
+ delete(data, "name")
+
+ switch name.(type) {
+ case string:
+ card.Name = name.(string)
+ case map[string]interface{}:
+ cardName := name.(map[string]interface{})["en"]
+ card.Name = cardName.(string)
+ default:
+ log.Fatalf("Unexpected type %T of yml card field 'Name'\n", name)
+ }
+
+ t, ok := data["type"]
+ if !ok {
+ log.Fatalf("no type in card yaml %s\n", definition)
+ }
+ delete(data, "type")
+
+ card.Type = CardTypeNames[t.(string)]
+
+ for k, v := range data {
+ card.Values[k.(string)] = v
+ }
+}
+
+func NewCard(cardPath string) *Card {
+ cardName := path.Base(cardPath)
+ set := path.Dir(cardPath)
+
+ cardDefinition, found := cards[set][cardName]
+ if !found {
+ cardDefinition = retrieveCardDefinition(cardPath)
+ if cards[set] == nil {
+ cards[set] = map[string][]byte{}
+ }
+ cards[set][cardName] = cardDefinition
+ }
+
+ c := Card{"", SetIdentifierNames[set], UndefinedCardType, map[string]interface{}{}}
+ c.parseDefinition(cardDefinition)
+ return &c
}
diff --git a/go/game/deck.go b/go/game/deck.go
index 33126ae8..f4290322 100644
--- a/go/game/deck.go
+++ b/go/game/deck.go
@@ -1,5 +1,71 @@
package game
+import (
+ "log"
+ "math/rand"
+ "strconv"
+ "strings"
+ "time"
+)
+
type Deck struct {
- Cards []Card
+ Cards []*Card
+}
+
+func NewDeck() *Deck {
+ d := Deck{[]*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]
+ })
+}
+
+func (d *Deck) Draw(amount int) []*Card {
+ drawn := d.Cards[-amount:]
+ d.Cards = d.Cards[:amount]
+ return drawn
+}
+
+func NewDeckFromCardPaths(cardPaths []string) *Deck {
+ d := NewDeck()
+ for _, cardPath := range cardPaths {
+ card := NewCard(cardPath)
+ d.AddCard(card)
+ }
+ return d
+}
+
+func NewDeckFromDeckList(deckList string) *Deck {
+ cardPaths := []string{}
+ lines := strings.Split(deckList, "\n")
+ for _, line := range lines {
+ tokens := strings.SplitN(line, " ", 2)
+ cardPath := tokens[1]
+ amount, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for i := 0; i < amount; i++ {
+ cardPaths = append(cardPaths, cardPath)
+ }
+ }
+
+ return NewDeckFromCardPaths(cardPaths)
}
diff --git a/go/game/discardPiles.go b/go/game/discardPiles.go
index 9c1c1e45..f7cf8994 100644
--- a/go/game/discardPiles.go
+++ b/go/game/discardPiles.go
@@ -1,5 +1,10 @@
package game
type DiscardPile struct {
- Cards []Card
+ Cards []*Card
+}
+
+func NewDiscardPile() *DiscardPile {
+ dp := DiscardPile{[]*Card{}}
+ return &dp
}
diff --git a/go/game/hand.go b/go/game/hand.go
index 42187f5a..fde4a686 100644
--- a/go/game/hand.go
+++ b/go/game/hand.go
@@ -1,5 +1,32 @@
package game
type Hand struct {
- Cards []Card
+ Cards []*Card
+}
+
+func NewHand() *Hand {
+ h := Hand{[]*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) {
+ for i, c := range h.Cards {
+ if c != card {
+ continue
+ }
+
+ h.Cards = append(h.Cards[:i], h.Cards[i+1:]...)
+ }
}
diff --git a/go/game/map.go b/go/game/map.go
index cecce9ca..ebf59d25 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -1,8 +1,8 @@
package game
import (
- "fmt"
"errors"
+ "fmt"
"io"
"log"
"math/rand"
@@ -20,7 +20,8 @@ const (
BASE_URL string = "localhost:8000/"
MAP_URL_PART string = "html/build/maps"
- DEFAULT_RESSOURCE_GAIN int = 5
+ DEFAULT_RESSOURCE_GAIN int = 5
+ DEFAULT_START_DECK string = "3 misc/farmer"
)
type MapYml struct {
@@ -32,6 +33,7 @@ type Map struct {
Tiles [][]Tile
symbols map[string]string
RessourceGain int
+ StartDeckList string
}
func readMap(r io.Reader) (*Map, error) {
@@ -52,7 +54,7 @@ func ParseMap(data []byte) (*Map, error) {
mapYml.Symbols[" "] = "neutral"
- m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN}
+ m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK}
rows := strings.Split(mapYml.Map, "\n")
for y := 0; y < len(rows); y++ {
diff --git a/go/game/permanent.go b/go/game/permanent.go
index a4418e3e..37111c4d 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -1,8 +1,10 @@
package game
type Permanent struct {
- Card *Card
- Tile *Tile
- Damage int
- Pile []*Permanent
+ Card *Card
+ Tile *Tile
+ Damage int
+ Pile []*Permanent
+ Controller *Player
+ Owner *Player
}
diff --git a/go/game/player.go b/go/game/player.go
index 6b9041ad..20ac13a5 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -1,5 +1,9 @@
package game
+const (
+ MAX_DRAW int = 3
+)
+
type Player struct {
Id int
Name string
@@ -7,4 +11,23 @@ type Player struct {
DiscardPile *DiscardPile
Deck *Deck
Ressource int
+ gameState *State
+}
+
+func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
+ p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState}
+ return &p
+}
+
+func (p *Player) Draw() {
+ toDraw := MAX_DRAW - p.Hand.Size()
+ if toDraw < 1 {
+ return
+ }
+
+ drawnCards := p.Deck.Draw(toDraw)
+ p.Hand.AddCards(drawnCards)
+}
+
+func (p *Player) PlayPermanent(card *Card) {
}
diff --git a/go/game/range.go b/go/game/range.go
new file mode 100644
index 00000000..86ec2f50
--- /dev/null
+++ b/go/game/range.go
@@ -0,0 +1,39 @@
+package game
+
+import (
+ "math"
+)
+
+func IsPositionInRange(origin Position, pos Position, r int) bool {
+ x, y := float64(pos.X-origin.X), float64(pos.Y-origin.Y)
+ ax, ay := math.Abs(x), math.Abs(y)
+ var abs float64
+ if pos.X > pos.Y {
+ abs = ax + math.Floor(ay/2)
+ } else {
+ abs = ay + math.Floor(ax/2)
+ }
+
+ return abs <= float64(r)
+}
+
+func PositionsInRange(origin Position, r int, includeOrigin bool) []Position {
+ positions := []Position{}
+ for x := -r; x <= r; x++ {
+ for y := -r; y <= r; y++ {
+ pos := Position{origin.X + x, origin.Y + y}
+ if pos.X < 0 || pos.Y < 0 {
+ continue
+ }
+
+ if !includeOrigin && pos == origin {
+ continue
+ }
+
+ if IsPositionInRange(origin, pos, r) {
+ positions = append(positions, pos)
+ }
+ }
+ }
+ return positions
+}
diff --git a/go/game/range_test.go b/go/game/range_test.go
new file mode 100644
index 00000000..afa164de
--- /dev/null
+++ b/go/game/range_test.go
@@ -0,0 +1,35 @@
+package game
+
+import (
+ "golang.org/x/exp/slices"
+ "testing"
+)
+
+func TestPositionsInRange(t *testing.T) {
+ origin := Position{0, 0}
+ positionsInRange1 := []Position{Position{0, 1}, Position{1, 0}, Position{1, 1}}
+ calculatedPositions := PositionsInRange(origin, 1, false)
+ if len(positionsInRange1) != len(calculatedPositions) {
+ t.Fatalf("Unexpected number of calculated positions")
+ }
+
+ for _, p := range positionsInRange1 {
+ if !slices.Contains(calculatedPositions, p) {
+ t.Fatalf("Position %v not in calculated positions", p)
+ }
+ }
+
+ origin = Position{2, 2}
+ positionsInRange2 := []Position{Position{0, 1}, Position{0, 2}, Position{0, 3},
+ Position{1, 0}, Position{1, 1}, Position{1, 2}, Position{1, 3}, Position{1, 4},
+ Position{2, 0}, Position{2, 1}, Position{2, 3}, Position{2, 4},
+ Position{3, 0}, Position{3, 1}, Position{3, 2}, Position{3, 3}, Position{3, 4},
+ Position{4, 1}, Position{4, 2}, Position{4, 3}}
+ calculatedPositions = PositionsInRange(origin, 2, false)
+
+ for _, p := range positionsInRange2 {
+ if !slices.Contains(calculatedPositions, p) {
+ t.Fatalf("Position %v not in calculated positions", p)
+ }
+ }
+}
diff --git a/go/game/set.go b/go/game/set.go
index 4136eac6..b0565eca 100644
--- a/go/game/set.go
+++ b/go/game/set.go
@@ -1,5 +1,9 @@
package game
+import (
+ "log"
+)
+
type SetIdentifier int
const (
@@ -13,3 +17,62 @@ const (
Tyrant
Exp1
)
+
+func (si SetIdentifier) String() string {
+ switch si {
+ case Base:
+ return "base"
+ case Magic:
+ return "magic"
+ case Equipments:
+ return "equipments"
+ case Misc:
+ return "misc"
+ case Nautics:
+ return "nautics"
+ case Potions:
+ return "potions"
+ case Kraken:
+ return "kraken"
+ case Tyrant:
+ return "tyrant"
+ case Exp1:
+ return "exp1"
+ }
+ log.Panicf("Switch not exhausting %d", si)
+ return ""
+}
+
+var SetIdentifiers = struct {
+ Base SetIdentifier
+ Magic SetIdentifier
+ Equipments SetIdentifier
+ Misc SetIdentifier
+ Nautics SetIdentifier
+ Potions SetIdentifier
+ Kraken SetIdentifier
+ Tyrant SetIdentifier
+ Exp1 SetIdentifier
+}{
+ Base: Base,
+ Magic: Magic,
+ Equipments: Equipments,
+ Misc: Misc,
+ Nautics: Nautics,
+ Potions: Potions,
+ Kraken: Kraken,
+ Tyrant: Tyrant,
+ Exp1: Exp1,
+}
+
+var SetIdentifierNames = map[string]SetIdentifier{
+ "base": Base,
+ "magic": Magic,
+ "equipments": Equipments,
+ "misc": Misc,
+ "nautics": Nautics,
+ "potions": Potions,
+ "kraken": Kraken,
+ "tyrant": Tyrant,
+ "exp1": Exp1,
+}
diff --git a/go/game/state.go b/go/game/state.go
index 9dcf73c4..caae8106 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -1,12 +1,32 @@
package game
type State struct {
- Decks []Deck
- Stores []Store
- Map Map
- Stack Stack
- Hands []Hand
- DiscardPiles []DiscardPile
- Players []Player
- Permanents []Permanent
+ Decks []*Deck
+ Stores []*Store
+ Map *Map
+ Stack *Stack
+ Hands []*Hand
+ DiscardPiles []*DiscardPile
+ Players []*Player
+ Permanents []*Permanent
+ Units []*Unit
+}
+
+func NewState() *State {
+ return &State{}
+}
+
+func (s *State) AddNewPlayer(name string, deck *Deck) {
+ p := NewPlayer(len(s.Players), name, deck, s)
+ 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.Permanent)
+ s.Units = append(s.Units, unit)
+}
+
+func (s *State) MoveUnit(u *Unit, t *Tile) {
+ u.move(s, t)
}
diff --git a/go/game/tile.go b/go/game/tile.go
index fb62420b..50688d83 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -85,3 +85,7 @@ func NewTileFromString(tile string, pos Position) (Tile, error) {
}
return t, nil
}
+
+func (t *Tile) IsFree() bool {
+ return t.Permanent == nil
+}
diff --git a/go/game/unit.go b/go/game/unit.go
new file mode 100644
index 00000000..4c07d625
--- /dev/null
+++ b/go/game/unit.go
@@ -0,0 +1,89 @@
+package game
+
+import (
+ "golang.org/x/exp/slices"
+ "log"
+ "strconv"
+ "strings"
+)
+
+type UnitStates int
+
+const (
+ Paralysis UnitStates = iota + 1
+ Poison
+ Panic
+ Rage
+ Ward
+)
+
+type Attack struct {
+ Damage int
+ Range int
+}
+
+func parseAttack(attack string) Attack {
+ tokens := strings.Split(attack, " ")
+ a, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ log.Fatalf("Attack %s has no valid Damage value\n", attack)
+ }
+
+ 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)
+ }
+ }
+ return Attack{a, r}
+}
+
+type Unit struct {
+ Permanent
+ Health int
+ Movement int
+ Attack Attack
+ Upkeep int
+ // FullActions []FullActions
+ // FreeActions []FreeActions
+ States []UnitStates
+ // Effects []Effects
+}
+
+func NewUnit(cardPath string, tile *Tile, owner *Player) *Unit {
+ card := NewCard(cardPath)
+
+ return &Unit{
+ Permanent{card, tile, 0, []*Permanent{}, owner, owner},
+ card.Values["health"].(int),
+ card.Values["movement"].(int),
+ parseAttack(card.Values["attack"].(string)),
+ card.Values["upkeep"].(int),
+ []UnitStates{},
+ }
+}
+
+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)
+ }
+
+ u.Tile.Permanent = nil
+ u.Tile = tile
+}
+
+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) {
+ tiles = append(tiles, tile)
+ }
+ }
+ return tiles
+}
+
+func (u *Unit) IsAvailableTile(tile *Tile) bool {
+ return tile.IsFree()
+}
diff --git a/go/go.mod b/go/go.mod
index 963c3bd7..23ca4ee1 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -2,16 +2,19 @@ module muhq.space/muhqs-game/go
go 1.19
-require github.com/hajimehoshi/ebiten/v2 v2.4.13
+require (
+ github.com/hajimehoshi/ebiten/v2 v2.4.13
+ golang.org/x/exp v0.0.0-20221230185412-738e83a70c30
+ gopkg.in/yaml.v3 v3.0.1
+)
require (
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad // indirect
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 // indirect
github.com/jezek/xgb v1.0.1 // indirect
- golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
+ golang.org/x/exp/shiny v0.0.0-20230111222715-75897c7a292a // indirect
golang.org/x/image v0.1.0 // indirect
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect
- golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
+ golang.org/x/sys v0.1.0 // indirect
)
diff --git a/go/go.sum b/go/go.sum
index 99412296..9f167880 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -24,8 +24,11 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws=
+golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp/shiny v0.0.0-20230111222715-75897c7a292a h1:tGTJlXP7PfpVCoqOY74MqILeMGqJwhOy42ToU8r+c20=
+golang.org/x/exp/shiny v0.0.0-20230111222715-75897c7a292a/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
@@ -58,8 +61,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -75,6 +79,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go/ui/map.go b/go/ui/map.go
deleted file mode 100644
index 2e83ad2d..00000000
--- a/go/ui/map.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package ui
-
-import (
- "log"
- "math"
-
- "github.com/hajimehoshi/ebiten/v2"
- "muhq.space/muhqs-game/go/assets"
- "muhq.space/muhqs-game/go/game"
-)
-
-const (
- TILE_WIDTH int = 50
- TILE_HEIGHT int = 50
-)
-
-type MapLayer struct {
- m *game.Map
- img *ebiten.Image
-}
-
-func NewMapLayerFromMap(m *game.Map) *MapLayer {
- mv := MapLayer{m, nil}
- return &mv
-}
-
-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,
- // the origin point is the upper-left corner.
- op.GeoM.Translate(-float64(TILE_WIDTH)/2, -float64(TILE_HEIGHT)/2)
-
- // Rotate the image. As a result, the anchor point of this rotate is
- // the center of the image.
- op.GeoM.Rotate(radian)
-
- // Reset translation
- op.GeoM.Translate(float64(TILE_WIDTH)/2, float64(TILE_HEIGHT)/2)
-}
-
-func (m *MapLayer) rotateStreet(x, y int, op *ebiten.DrawImageOptions) {
- connections, left, right, above, below := m.m.FindStreetConnections(x, y)
-
- if connections == 0 {
- // This street is not connected to another street ->
- // check any other non neutral tiles
- connections, left, right, above, below = m.m.FindAnyConnections(x, y)
- }
-
- // This street is not connected to anything. Seams odd!
- if connections == 0 {
- log.Printf("Street at (%d, %d) is not connected", x, y)
- } else if (connections == 1 ||
- (connections == 2 && ((left && right) || (above && below)))) &&
- (above || below) {
- rotateStreetImg(x, y, math.Pi/2, op)
- } else if connections == 2 {
- // normal orientation above and right
- if right && below {
- rotateStreetImg(x, y, 3*math.Pi/2, op)
- } else if left && below {
- rotateStreetImg(x, y, math.Pi, op)
- } else if above && left {
- rotateStreetImg(x, y, math.Pi/2, op)
- }
- } else if connections == 3 {
- // normal orientation left above right
- if above && right && below {
- rotateStreetImg(x, y, 3*math.Pi/2, op)
- } else if left && below && right {
- rotateStreetImg(x, y, math.Pi, op)
- } else if below && left && above {
- rotateStreetImg(x, y, math.Pi/2, op)
- }
- }
-}
-
-func (m *MapLayer) Draw(screen *ebiten.Image) {
- if m.img == nil {
- // TODO: support non symetric maps
- maxWidth := len(m.m.Tiles[0]) * TILE_WIDTH
- maxHeight := len(m.m.Tiles) * TILE_HEIGHT
- m.img = ebiten.NewImage(maxWidth, maxHeight)
-
- x_px, y_px := 0.0, 0.0
- for y := 0; y < len(m.m.Tiles); y++ {
- for x := 0; x < len(m.m.Tiles[y]); x++ {
- tile := &m.m.Tiles[y][x]
- tileImg := assets.GetTile(tile.Raw)
-
- op := &ebiten.DrawImageOptions{}
- // Rotate Street
- if tile.Type == game.TileTypes.Street {
- m.rotateStreet(x, y, op)
- }
-
- op.GeoM.Translate(x_px, y_px)
-
- m.img.DrawImage(tileImg, op)
-
- x_px += float64(TILE_WIDTH)
- }
- x_px = 0
- y_px += float64(TILE_HEIGHT)
- }
- }
-
- screen.DrawImage(m.img, &ebiten.DrawImageOptions{})
-}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
new file mode 100644
index 00000000..10929f68
--- /dev/null
+++ b/go/ui/mapView.go
@@ -0,0 +1,212 @@
+package ui
+
+import (
+ "log"
+ "math"
+ "strings"
+
+ "golang.org/x/exp/slices"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "muhq.space/muhqs-game/go/assets"
+ "muhq.space/muhqs-game/go/game"
+)
+
+const (
+ PERMANENT_WIDTH int = 40
+ PERMANENT_HEIGHT int = 40
+
+ TILE_WIDTH int = 50
+ TILE_HEIGHT int = 50
+)
+
+type MapView struct {
+ gameState *game.State
+ mapLayer *ebiten.Image
+ permanentsLayer *ebiten.Image
+ tileHighlights []game.Position
+ permanentsHighlights []*game.Permanent
+}
+
+func NewMapView(g *game.State) *MapView {
+ vw := MapView{g, nil, nil, []game.Position{}, []*game.Permanent{}}
+ return &vw
+}
+
+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,
+ // the origin point is the upper-left corner.
+ op.GeoM.Translate(-float64(TILE_WIDTH)/2, -float64(TILE_HEIGHT)/2)
+
+ // Rotate the image. As a result, the anchor point of this rotate is
+ // the center of the image.
+ op.GeoM.Rotate(radian)
+
+ // Reset translation
+ op.GeoM.Translate(float64(TILE_WIDTH)/2, float64(TILE_HEIGHT)/2)
+}
+
+func (vw *MapView) rotateStreet(x, y int, op *ebiten.DrawImageOptions) {
+ connections, left, right, above, below := vw.gameState.Map.FindStreetConnections(x, y)
+
+ if connections == 0 {
+ // This street is not connected to another street ->
+ // check any other non neutral tiles
+ connections, left, right, above, below = vw.gameState.Map.FindAnyConnections(x, y)
+ }
+
+ // This street is not connected to anything. Seams odd!
+ if connections == 0 {
+ log.Printf("Street at (%d, %d) is not connected", x, y)
+ } else if (connections == 1 ||
+ (connections == 2 && ((left && right) || (above && below)))) &&
+ (above || below) {
+ rotateStreetImg(x, y, math.Pi/2, op)
+ } else if connections == 2 {
+ // normal orientation above and right
+ if right && below {
+ rotateStreetImg(x, y, 3*math.Pi/2, op)
+ } else if left && below {
+ rotateStreetImg(x, y, math.Pi, op)
+ } else if above && left {
+ rotateStreetImg(x, y, math.Pi/2, op)
+ }
+ } else if connections == 3 {
+ // normal orientation left above right
+ if above && right && below {
+ rotateStreetImg(x, y, 3*math.Pi/2, op)
+ } else if left && below && right {
+ rotateStreetImg(x, y, math.Pi, op)
+ } else if below && left && above {
+ rotateStreetImg(x, y, math.Pi/2, op)
+ }
+ }
+}
+
+func (vw *MapView) newLayerImage() *ebiten.Image {
+ // TODO: support non symetric maps
+ maxWidth := len(vw.gameState.Map.Tiles[0]) * TILE_WIDTH
+ maxHeight := len(vw.gameState.Map.Tiles) * TILE_HEIGHT
+ return ebiten.NewImage(maxWidth, maxHeight)
+}
+
+func (vw *MapView) drawMapLayer(screen *ebiten.Image) {
+ if vw.mapLayer == nil {
+ vw.mapLayer = vw.newLayerImage()
+
+ x_px, y_px := 0.0, 0.0
+ for y := 0; y < len(vw.gameState.Map.Tiles); y++ {
+ for x := 0; x < len(vw.gameState.Map.Tiles[y]); x++ {
+ pos := game.Position{x, y}
+ tile := vw.gameState.Map.TileAt(pos)
+ tileImg := assets.GetTile(tile.Raw)
+
+ op := &ebiten.DrawImageOptions{}
+ // Rotate Street
+ if tile.Type == game.TileTypes.Street {
+ vw.rotateStreet(x, y, op)
+ }
+
+ op.GeoM.Translate(x_px, y_px)
+
+ if slices.Contains(vw.tileHighlights, pos) {
+ op.ColorM.Scale(1, 0, 0, 0.5)
+ }
+
+ vw.mapLayer.DrawImage(tileImg, op)
+
+ x_px += float64(TILE_WIDTH)
+ }
+ x_px = 0
+ y_px += float64(TILE_HEIGHT)
+ }
+ }
+
+ screen.DrawImage(vw.mapLayer, &ebiten.DrawImageOptions{})
+}
+
+func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) {
+ if vw.permanentsLayer == nil {
+ vw.permanentsLayer = vw.newLayerImage()
+
+ for _, p := range vw.gameState.Permanents {
+ permanentSymbol := assets.GetSymbol(strings.ToLower(p.Card.Name))
+ // TODO: Implement generic symbols
+ if permanentSymbol == nil {
+ continue
+ }
+
+ op := &ebiten.DrawImageOptions{}
+ x_px := p.Tile.Position.X * TILE_WIDTH + (TILE_WIDTH - PERMANENT_WIDTH) / 2
+ y_px := p.Tile.Position.Y * TILE_HEIGHT + (TILE_HEIGHT - PERMANENT_HEIGHT) / 2
+ op.GeoM.Translate(float64(x_px), float64(y_px))
+ vw.permanentsLayer.DrawImage(permanentSymbol, op)
+ }
+ }
+
+ screen.DrawImage(vw.permanentsLayer, &ebiten.DrawImageOptions{})
+}
+
+func (vw *MapView) Draw(screen *ebiten.Image) {
+ vw.drawMapLayer(screen)
+ vw.drawPermanentsLayer(screen)
+}
+
+func (vw *MapView) ForceRedraw() {
+ vw.mapLayer = nil
+ vw.permanentsLayer = nil
+}
+
+func (vw *MapView) FindObjectAt(x_px, y_px int) interface{} {
+ x := x_px / TILE_WIDTH
+ y := y_px / TILE_HEIGHT
+
+ // TODO: detect if a permanent or the containing tile was selected
+ for _, u := range vw.gameState.Units {
+ pos := u.Tile.Position
+ if pos.X == x && pos.Y == y {
+ return u
+ }
+ }
+
+ if y < len(vw.gameState.Map.Tiles) && x < len(vw.gameState.Map.Tiles[y]) {
+ return &vw.gameState.Map.Tiles[y][x]
+ }
+
+ return nil
+}
+
+func (vw *MapView) HighlightPositions(pos []game.Position) {
+ vw.ForceRedraw()
+ vw.tileHighlights = pos
+}
+
+func (vw *MapView) HighlightTiles(tiles []*game.Tile) {
+ pos := make([]game.Position, 0, len(tiles))
+ for _, t := range tiles {
+ pos = append(pos, t.Position)
+ }
+ vw.HighlightPositions(pos)
+}
+
+func (vw *MapView) HighlightTile(t *game.Tile) {
+ vw.HighlightTiles([]*game.Tile{t})
+}
+
+func (vw *MapView) ClearTileHighlights() {
+ vw.HighlightTiles([]*game.Tile{})
+}
+
+func (vw *MapView) HighlightPermanents(permanents []*game.Permanent) {
+ vw.ForceRedraw()
+ vw.permanentsHighlights = permanents
+}
+
+func (vw *MapView) HighlightPermanent(p *game.Permanent) {
+ vw.HighlightPermanents([]*game.Permanent{p})
+}
+
+func (vw *MapView) ClearPermanentsHighlights() {
+ vw.HighlightPermanents([]*game.Permanent{})
+}