aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2022-12-27 14:29:39 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:43 +0100
commit7f7389422834f628c9e45278e90fab6911173b25 (patch)
tree0bd8c0f4daaed7619d467cf82e7f7add09ef1f32
parent3c2ee4ee48480a28dc606878503f314ec8862d45 (diff)
downloadmuhqs-game-7f7389422834f628c9e45278e90fab6911173b25.tar.gz
muhqs-game-7f7389422834f628c9e45278e90fab6911173b25.zip
intermediate commit
-rw-r--r--go/.gitignore3
-rw-r--r--go/Makefile25
-rw-r--r--go/assets/retrieve.go82
-rw-r--r--go/client/main.go58
-rw-r--r--go/game/action.go6
-rw-r--r--go/game/card.go18
-rw-r--r--go/game/deck.go5
-rw-r--r--go/game/discardPiles.go5
-rw-r--r--go/game/hand.go5
-rw-r--r--go/game/map.go181
-rw-r--r--go/game/map_test.go53
-rw-r--r--go/game/permanent.go8
-rw-r--r--go/game/player.go10
-rw-r--r--go/game/pos.go10
-rw-r--r--go/game/set.go15
-rw-r--r--go/game/stack.go5
-rw-r--r--go/game/state.go12
-rw-r--r--go/game/store.go5
-rw-r--r--go/game/tile.go87
-rw-r--r--go/go.mod17
-rw-r--r--go/go.sum80
-rw-r--r--go/server/main.go13
-rw-r--r--go/ui/map.go109
23 files changed, 812 insertions, 0 deletions
diff --git a/go/.gitignore b/go/.gitignore
new file mode 100644
index 00000000..eff64396
--- /dev/null
+++ b/go/.gitignore
@@ -0,0 +1,3 @@
+client/client
+client/client.wasm
+server/server
diff --git a/go/Makefile b/go/Makefile
new file mode 100644
index 00000000..220a1022
--- /dev/null
+++ b/go/Makefile
@@ -0,0 +1,25 @@
+PACKAGES := assets game server client
+
+APPS := server client
+OBJS := $(foreach app,$(APPS),$(app)/$(app))
+
+.PHONY: all fmt test server/server client/client client/client.wasm
+all: $(OBJS)
+
+server/server:
+ @echo Building Server ...
+ pushd $(@D) >/dev/null; go build; popd >/dev/null
+
+client/client:
+ @echo Building Client ...
+ pushd $(@D) >/dev/null; go build; popd >/dev/null
+
+client/client.wasm:
+ @echo Building Client.wasm ...
+ pushd $(@D) >/dev/null; env GOOS=js GOARCH=wasm go build -o $(@F); popd >/dev/null
+
+fmt:
+ for package in $(PACKAGES); do pushd $$package >/dev/null; go fmt; popd >/dev/null; done
+
+test:
+ for package in $(PACKAGES); do pushd $$package >/dev/null; go test; popd >/dev/null; done
diff --git a/go/assets/retrieve.go b/go/assets/retrieve.go
new file mode 100644
index 00000000..fea3d141
--- /dev/null
+++ b/go/assets/retrieve.go
@@ -0,0 +1,82 @@
+package assets
+
+import (
+ "image/png"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+const (
+ PROTOCOL string = "http://"
+ 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"
+)
+
+func retrieveAsset(path string) *ebiten.Image {
+ log.Printf("loading %s\n", path)
+ resp, err := http.Get(PROTOCOL + path)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer resp.Body.Close()
+
+ img, err := png.Decode(resp.Body)
+ if err != nil {
+ log.Fatal("decoding ", err)
+ }
+ return ebiten.NewImageFromImage(img)
+}
+
+var cards = map[string]map[string]map[string]*ebiten.Image{}
+
+func GetCard(cardPath string, language string) *ebiten.Image {
+ card := path.Base(cardPath)
+ set := path.Dir(cardPath)
+ img, found := cards[language][set][card]
+ if !found {
+ url := path.Join(BASE_URL, IMAGE_URL_PART, language, cardPath+".png")
+ img = retrieveAsset(url)
+
+ if cards[language] == nil {
+ cards[language] = map[string]map[string]*ebiten.Image{}
+ }
+
+ if cards[language][set] == nil {
+ cards[language][set] = map[string]*ebiten.Image{}
+ }
+
+ cards[language][set][card] = img
+ }
+ return img
+}
+
+var tiles = map[string]*ebiten.Image{}
+
+func GetTile(tile string) *ebiten.Image {
+ img, found := tiles[tile]
+ if !found {
+ url := path.Join(BASE_URL, TILE_URL_PART, strings.Replace(tile, " ", "_", -1)+".png")
+ img = retrieveAsset(url)
+ tiles[tile] = img
+ }
+ return img
+}
+
+var symbols = map[string]*ebiten.Image{}
+
+func GetSymbol(symbol string) *ebiten.Image {
+ img, found := symbols[symbol]
+ if !found {
+ url := path.Join(BASE_URL, SYMBOL_URL_PART, symbol+".png")
+ img = retrieveAsset(url)
+ symbols[symbol] = img
+ }
+ return img
+}
diff --git a/go/client/main.go b/go/client/main.go
new file mode 100644
index 00000000..b91b15f7
--- /dev/null
+++ b/go/client/main.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/ebitenutil"
+
+ "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
+)
+
+func (g *Game) Update() error {
+ return nil
+}
+
+func (g *Game) 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()))
+}
+
+func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
+ return screenWidth, screenHeight
+}
+
+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 {
+ log.Fatal(err)
+ }
+}
diff --git a/go/game/action.go b/go/game/action.go
new file mode 100644
index 00000000..723d4866
--- /dev/null
+++ b/go/game/action.go
@@ -0,0 +1,6 @@
+package game
+
+type Action struct {
+ Owner *Player
+ Card *Card
+}
diff --git a/go/game/card.go b/go/game/card.go
new file mode 100644
index 00000000..6cd20257
--- /dev/null
+++ b/go/game/card.go
@@ -0,0 +1,18 @@
+package game
+
+type CardType int
+
+const (
+ Unit CardType = iota + 1
+ Spell
+ Artifact
+ Equipment
+ Potion
+ Itention
+)
+
+type Card struct {
+ Name string
+ Set SetIdentifier
+ Type CardType
+}
diff --git a/go/game/deck.go b/go/game/deck.go
new file mode 100644
index 00000000..33126ae8
--- /dev/null
+++ b/go/game/deck.go
@@ -0,0 +1,5 @@
+package game
+
+type Deck struct {
+ Cards []Card
+}
diff --git a/go/game/discardPiles.go b/go/game/discardPiles.go
new file mode 100644
index 00000000..9c1c1e45
--- /dev/null
+++ b/go/game/discardPiles.go
@@ -0,0 +1,5 @@
+package game
+
+type DiscardPile struct {
+ Cards []Card
+}
diff --git a/go/game/hand.go b/go/game/hand.go
new file mode 100644
index 00000000..42187f5a
--- /dev/null
+++ b/go/game/hand.go
@@ -0,0 +1,5 @@
+package game
+
+type Hand struct {
+ Cards []Card
+}
diff --git a/go/game/map.go b/go/game/map.go
new file mode 100644
index 00000000..0b6fe87c
--- /dev/null
+++ b/go/game/map.go
@@ -0,0 +1,181 @@
+package game
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+)
+
+const (
+ PROTOCOL string = "http://"
+ BASE_URL string = "localhost:8000/"
+ MAP_URL_PART string = "html/build/maps"
+
+ DEFAULT_RESSOURCE_GAIN int = 5
+)
+
+type MapYml struct {
+ Map string `yaml:"map"`
+ Symbols map[string]string `yaml:"symbols"`
+}
+
+type Map struct {
+ Tiles [][]Tile
+ RessourceGain int
+}
+
+func parseMap(r io.Reader) (*Map, error) {
+ data, err := io.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+
+ mapYml := MapYml{}
+ err = yaml.Unmarshal(data, &mapYml)
+ if err != nil {
+ return nil, err
+ }
+
+ m := &Map{[][]Tile{}, DEFAULT_RESSOURCE_GAIN}
+
+ mapYml.Symbols[" "] = "neutral"
+
+ rows := strings.Split(mapYml.Map, "\n")
+ for y := 0; y < len(rows); y++ {
+ m.Tiles = append(m.Tiles, []Tile{})
+ row := rows[y]
+ for x := 0; x < len(rows[y]); x++ {
+ s := string(row[x])
+ tile, err := NewTileFromString(mapYml.Symbols[s], Position{x, y})
+ if err != nil {
+ return nil, err
+ }
+ m.Tiles[y] = append(m.Tiles[y], tile)
+ }
+ }
+
+ m.selectStreets()
+
+ return m, nil
+}
+
+func (m *Map) selectStreets() {
+ for y := 0; y < len(m.Tiles); y++ {
+ for x := 0; x < len(m.Tiles[y]); x++ {
+ tile := &m.Tiles[y][x]
+ if tile.Type != TileTypes.Street {
+ continue
+ }
+ connections, left, right, above, below := m.FindStreetConnections(x, y)
+
+ // Decide if curve or straight
+ if connections == 2 && (left || right) && (above || below) {
+ tile.Raw = "street 2"
+ }
+
+ if connections > 2 {
+ tile.Raw = fmt.Sprintf("street %d", connections)
+ }
+ }
+ }
+}
+
+func (m *Map) FindNeighbours(x, y int) (left, right, above, below *Tile) {
+ left, right, above, below = nil, nil, nil, nil
+ if y > 0 {
+ above = &m.Tiles[y-1][x]
+ }
+
+ if y < len(m.Tiles) {
+ below = &m.Tiles[y+1][x]
+ }
+
+ if x > 0 {
+ left = &m.Tiles[y][x-1]
+ }
+
+ if x < len(m.Tiles[y]) {
+ right = &m.Tiles[y][x+1]
+ }
+
+ return left, right, above, below
+}
+
+func (m *Map) FindAnyConnections(x, y int) (connections int, left, right, above, below bool) {
+ leftT, rightT, aboveT, belowT := m.FindNeighbours(x, y)
+ connections = 0
+
+ if leftT.Type != TileTypes.Neutral {
+ left = true
+ connections += 1
+ }
+ if rightT.Type != TileTypes.Neutral {
+ right = true
+ connections += 1
+ }
+ if aboveT.Type != TileTypes.Neutral {
+ above = true
+ connections += 1
+ }
+ if belowT.Type != TileTypes.Neutral {
+ below = true
+ connections += 1
+ }
+
+ return
+}
+
+func (m *Map) FindStreetConnections(x int, y int) (connections int, left, right, above, below bool) {
+ leftT, rightT, aboveT, belowT := m.FindNeighbours(x, y)
+ connections = 0
+
+ if leftT.Type == TileTypes.Street {
+ left = true
+ connections += 1
+ }
+ if rightT.Type == TileTypes.Street {
+ right = true
+ connections += 1
+ }
+ if aboveT.Type == TileTypes.Street {
+ above = true
+ connections += 1
+ }
+ if belowT.Type == TileTypes.Street {
+ below = true
+ connections += 1
+ }
+
+ return
+}
+
+func GetMap(name string) (*Map, error) {
+ url := path.Join(BASE_URL, MAP_URL_PART, name+".yml")
+ log.Printf("loading map from %s\n", url)
+ resp, err := http.Get(PROTOCOL + url)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer resp.Body.Close()
+ return parseMap(resp.Body)
+}
+
+func LoadMap(file string) (*Map, error) {
+ f, err := os.Open("/tmp/123")
+ if err != nil {
+ log.Fatal("Opening map ", file, " failed: ", err)
+ }
+ defer f.Close()
+
+ return parseMap(f)
+}
+
+func (m *Map) TileAt(p Position) *Tile {
+ return &m.Tiles[p.Y][p.X]
+}
diff --git a/go/game/map_test.go b/go/game/map_test.go
new file mode 100644
index 00000000..732c8b86
--- /dev/null
+++ b/go/game/map_test.go
@@ -0,0 +1,53 @@
+package game
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestParseMap(t *testing.T) {
+ s := `map: |1+
+ T H HT
+ FSSSSS2H
+ SHSH HS
+ S SST S
+ H HFH H
+ S TSS S
+ SH HSHS
+ H1SSSSSF
+ TH H T
+
+symbols:
+ T: tower
+ H: house
+ F: farm
+ S: street
+ 1: spawn player 1
+ 2: spawn player 2
+
+kings:
+ - (1,9)
+ - (9,1)`
+
+ r := strings.NewReader(s)
+ m, err := parseMap(r)
+ if err != nil {
+ t.Fatalf("Parsing failed with: %v", err)
+ }
+
+ if m.Tiles[0][0].Type != TileTypes.Tower {
+ t.Fatalf("Unexpected tile at 0,0: %v (expected %v)", m.Tiles[0][0], TileTypes.Tower)
+ }
+
+ for y := 0; y < len(m.Tiles); y++ {
+ for x := 0; y < len(m.Tiles[y]); y++ {
+ if m.TileAt(Position{x, y}) != &m.Tiles[y][x] {
+ t.Fatalf("TileAt and Tiles differ")
+ }
+ }
+ }
+
+ if m.RessourceGain != DEFAULT_RESSOURCE_GAIN {
+ t.Fatalf("ressource gain is not the default one")
+ }
+}
diff --git a/go/game/permanent.go b/go/game/permanent.go
new file mode 100644
index 00000000..a4418e3e
--- /dev/null
+++ b/go/game/permanent.go
@@ -0,0 +1,8 @@
+package game
+
+type Permanent struct {
+ Card *Card
+ Tile *Tile
+ Damage int
+ Pile []*Permanent
+}
diff --git a/go/game/player.go b/go/game/player.go
new file mode 100644
index 00000000..6b9041ad
--- /dev/null
+++ b/go/game/player.go
@@ -0,0 +1,10 @@
+package game
+
+type Player struct {
+ Id int
+ Name string
+ Hand *Hand
+ DiscardPile *DiscardPile
+ Deck *Deck
+ Ressource int
+}
diff --git a/go/game/pos.go b/go/game/pos.go
new file mode 100644
index 00000000..2b3459cd
--- /dev/null
+++ b/go/game/pos.go
@@ -0,0 +1,10 @@
+package game
+
+type Position struct {
+ X int
+ Y int
+}
+
+func INVALID_POSITION() Position {
+ return Position{-1, -1}
+}
diff --git a/go/game/set.go b/go/game/set.go
new file mode 100644
index 00000000..4136eac6
--- /dev/null
+++ b/go/game/set.go
@@ -0,0 +1,15 @@
+package game
+
+type SetIdentifier int
+
+const (
+ Base SetIdentifier = iota + 1
+ Magic
+ Equipments
+ Misc
+ Nautics
+ Potions
+ Kraken
+ Tyrant
+ Exp1
+)
diff --git a/go/game/stack.go b/go/game/stack.go
new file mode 100644
index 00000000..5d62e601
--- /dev/null
+++ b/go/game/stack.go
@@ -0,0 +1,5 @@
+package game
+
+type Stack struct {
+ Actions []Action
+}
diff --git a/go/game/state.go b/go/game/state.go
new file mode 100644
index 00000000..9dcf73c4
--- /dev/null
+++ b/go/game/state.go
@@ -0,0 +1,12 @@
+package game
+
+type State struct {
+ Decks []Deck
+ Stores []Store
+ Map Map
+ Stack Stack
+ Hands []Hand
+ DiscardPiles []DiscardPile
+ Players []Player
+ Permanents []Permanent
+}
diff --git a/go/game/store.go b/go/game/store.go
new file mode 100644
index 00000000..3f5793fb
--- /dev/null
+++ b/go/game/store.go
@@ -0,0 +1,5 @@
+package game
+
+type Store struct {
+ Cards []Card
+}
diff --git a/go/game/tile.go b/go/game/tile.go
new file mode 100644
index 00000000..fb62420b
--- /dev/null
+++ b/go/game/tile.go
@@ -0,0 +1,87 @@
+package game
+
+import (
+ "log"
+ "strconv"
+ "strings"
+)
+
+type TileType int
+
+const (
+ neutral TileType = iota + 1
+ tower
+ house
+ farm
+ street
+ spawn
+)
+
+func (t TileType) String() string {
+ switch t {
+ case neutral:
+ return "neutral"
+ case tower:
+ return "tower"
+ case house:
+ return "house"
+ case farm:
+ return "farm"
+ case street:
+ return "street"
+ case spawn:
+ return "spawn"
+ }
+ log.Panicf("Switch not exhausting %d", t)
+ return ""
+}
+
+var TileTypes = struct {
+ Neutral TileType
+ Tower TileType
+ House TileType
+ Farm TileType
+ Street TileType
+ Spawn TileType
+}{
+ Neutral: neutral,
+ Tower: tower,
+ House: house,
+ Farm: farm,
+ Street: street,
+ Spawn: spawn}
+
+var TileNames = map[string]TileType{
+ "neutral": neutral,
+ "tower": tower,
+ "house": house,
+ "farm": farm,
+ "street": street,
+ "spawn": spawn,
+}
+
+type Tile struct {
+ Position Position
+ Permanent *Permanent
+ Type TileType
+ Raw string
+ PlayerId int
+}
+
+func INVALID_TILE() Tile {
+ return Tile{INVALID_POSITION(), nil, 0, "", -1}
+}
+
+func NewTileFromString(tile string, pos Position) (Tile, error) {
+ tokens := strings.Split(tile, " ")
+ tileType := TileNames[strings.ToLower(tokens[0])]
+ t := Tile{pos, nil, tileType, tile, -1}
+ if tokens[0] == "spawn" {
+ playerId, err := strconv.Atoi(tokens[2])
+ if err != nil {
+ return INVALID_TILE(), err
+ }
+ t.PlayerId = playerId
+ }
+ return t, nil
+}
diff --git a/go/go.mod b/go/go.mod
new file mode 100644
index 00000000..963c3bd7
--- /dev/null
+++ b/go/go.mod
@@ -0,0 +1,17 @@
+module muhq.space/muhqs-game/go
+
+go 1.19
+
+require github.com/hajimehoshi/ebiten/v2 v2.4.13
+
+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/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
+)
diff --git a/go/go.sum b/go/go.sum
new file mode 100644
index 00000000..99412296
--- /dev/null
+++ b/go/go.sum
@@ -0,0 +1,80 @@
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 h1:A8UnJ/5OKzki4HBDwoRQz7I6sxKsokpMXcGh+fUxpfc=
+github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad h1:kX51IjbsJPCvzV9jUoVQG9GEUqIq5hjfYzXTqQ52Rh8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/hajimehoshi/bitmapfont/v2 v2.2.2/go.mod h1:Ua/x9Dkz7M9CU4zr1VHWOqGwjKdXbOTRsH7lWfb1Co0=
+github.com/hajimehoshi/ebiten/v2 v2.4.13 h1:ZZ5y+bFkAbUeD2WGquHF+xSbg83SIbcsxCwEVeZgHWM=
+github.com/hajimehoshi/ebiten/v2 v2.4.13/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4=
+github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 h1:s01qIIRG7vN/5ndLwkDktjx44ulFk6apvAjVBYR50Yo=
+github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
+github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
+github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
+github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
+github.com/jakecoffman/cp v1.2.1/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
+github.com/jezek/xgb v1.0.1 h1:YUGhxps0aR7J2Xplbs23OHnV1mWaxFVcOl9b+1RQkt8=
+github.com/jezek/xgb v1.0.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
+github.com/jfreymuth/oggvorbis v1.0.4/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
+github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+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/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=
+golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 h1:3vUV5x5+3LfQbgk7paCM6INOaJG9xXQbn79xoNkwfIk=
+golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+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/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=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+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/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/server/main.go b/go/server/main.go
new file mode 100644
index 00000000..ddc689ef
--- /dev/null
+++ b/go/server/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "fmt"
+
+ "muhq.space/muhqs-game/go/game"
+)
+
+var games = []game.State{}
+
+func main() {
+ fmt.Println("Started muhq's game server")
+}
diff --git a/go/ui/map.go b/go/ui/map.go
new file mode 100644
index 00000000..2e83ad2d
--- /dev/null
+++ b/go/ui/map.go
@@ -0,0 +1,109 @@
+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{})
+}