aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go/Makefile4
-rw-r--r--go/ai-companion/main.go176
-rw-r--r--go/ui/freeMapControl.go204
3 files changed, 382 insertions, 2 deletions
diff --git a/go/Makefile b/go/Makefile
index 20ef58e9..09b0435e 100644
--- a/go/Makefile
+++ b/go/Makefile
@@ -1,10 +1,10 @@
-PACKAGES := activities assets game server client dummy-ui
+PACKAGES := activities ai-companion assets game server client dummy-ui
SHELL := bash
APPS := server client dummy-ui
OBJS := $(foreach app,$(APPS),$(app)/$(app))
-WASM := client/client.wasm webtools/webtools.wasm dummy-ui/dummy-ui.wasm
+WASM := client/client.wasm webtools/webtools.wasm dummy-ui/dummy-ui.wasm ai-companion/ai-companion.wasm
GOFMT ?= gofumpt -l -w .
diff --git a/go/ai-companion/main.go b/go/ai-companion/main.go
new file mode 100644
index 00000000..769b3e7d
--- /dev/null
+++ b/go/ai-companion/main.go
@@ -0,0 +1,176 @@
+// This is a companion tool supporting a cooperative game.
+// It will execute the AI opponent and allows
+// the players to easily control the board state.
+// Before executing the AI the number of players, their store and the map must be specified.
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "strings"
+ "syscall/js"
+
+ "github.com/hajimehoshi/ebiten/v2"
+
+ "muhq.space/muhqs-game/go/font"
+ "muhq.space/muhqs-game/go/game"
+ "muhq.space/muhqs-game/go/ui"
+)
+
+type app struct {
+ ui.Collection
+ windowWidth int
+ windowHeight int
+
+ state *game.LocalState
+ ai *game.Player
+ krakenAction game.Action
+
+ mapControl *ui.FreeMapControl
+ cardView *ui.ToggleCardView
+ resolveBtn ui.Button
+ cancelBtn ui.Button
+}
+
+func (a *app) handleKrakenAction() {
+ log.Println("receive kraken action")
+ log.Println(a.state.ActivePhase(), a.state.ActivePlayer().Name)
+ a.krakenAction, _ = a.ai.Ctrl.RecvAction()
+ switch act := a.krakenAction.(type) {
+ case *game.PlayAction:
+ log.Println("kraken play action")
+ card := act.Card
+ a.cardView = ui.NewToggleCardView(10, 0, a.windowWidth - 10, a.windowHeight, card.Path())
+ a.AddWidget(a.cardView)
+ case *game.PassPriority:
+ log.Println("kraken passes priority")
+ default:
+ log.Println("kraken permanent")
+ pAct := act.(game.Action)
+ p, ok := pAct.Source().(game.Permanent)
+ if !ok {
+ log.Fatal("Unexpected Action", act)
+ }
+ a.mapControl.Mv.AddHighlightPermanent(p, ui.HighlightSelectionColor)
+ // TODO: prompt
+ }
+}
+
+func (a *app) Update() error {
+ if err := ui.TouchManager.Update(); err != nil {
+ return err
+ }
+
+ if a.krakenAction == nil {
+ a.handleKrakenAction()
+ }
+
+ return a.Collection.Update()
+}
+
+func (a *app) Layout(width, height int) (int, int) {
+ return a.windowWidth, a.windowHeight
+}
+
+type player struct {
+ Name string `json:"name"`
+ Cards []string `json:"cards"`
+}
+
+type opts struct {
+ MapName string `json:"mapName"`
+ Players []player `json:"players"`
+}
+
+func (a *app) startAiTurn() {
+ a.state.SetPhase(game.Phases.ActionPhase)
+ a.state.SetActivePlayer(g.ai)
+ g.ai.Turn++
+ a.ai.Resource = a.ai.ResourceGain()
+}
+
+func (a *app) init(mapName string, players []player) {
+ a.state = game.NewLocalState()
+
+ m, err := game.GetMap(mapName)
+ if err != nil {
+ log.Fatal(err)
+ }
+ a.state.SetMap(m)
+ a.mapControl = ui.NewFreeMapControl(a.state)
+ // a.mapControl.Scale(1.2)
+ a.AddWidget(a.mapControl)
+
+ switch mapName {
+ case "the-kraken":
+ g.ai = g.state.AddNewAiPlayer("kraken")
+ case "the-tyrant":
+ // TODO: init the tyrant AI
+ default:
+ log.Fatal("Invalid map: %s", mapName)
+ }
+
+ // Add players after the AI so it will alwa^s be the kraken's turn
+ for _, player := range players {
+ deck := game.NewDeckFromCardPaths(player.Cards)
+ a.state.AddNewPlayer(player.Name, deck)
+ }
+
+ a.startAiTurn()
+}
+
+func (g *app) cancelOrHide() {
+ g.mapControl.CancelControl()
+ g.RemoveWidget(g.cardView)
+ g.krakenAction = nil
+}
+
+var g *app
+
+func gameInit(opts opts) {
+ g = &app{windowWidth: 800, windowHeight: 1000}
+ g.init(opts.MapName, opts.Players)
+
+ g.resolveBtn = ui.NewSimpleButton(
+ 201, 700, 150, 99, "resolve",
+ func(b *ui.SimpleButton) {
+ log.Println("ResolveBtn Pressed")
+ switch act := g.krakenAction.(type) {
+ case *game.PlayAction:
+ g.state.ResolveAction(act)
+ g.RemoveWidget(g.cardView)
+ case *game.PassPriority:
+ g.krakenAction = nil
+ g.startAiTurn()
+ }
+ g.krakenAction = nil
+ })
+ g.AddWidget(g.resolveBtn)
+
+ g.cancelBtn = ui.NewSimpleButton(
+ 0, 700, 150, 99, "cancel",
+ func(b *ui.SimpleButton) {
+ log.Println("CancelBtn Pressed")
+ g.cancelOrHide()
+ })
+ g.AddWidget(g.cancelBtn)
+ ebiten.SetWindowSize(g.windowWidth, g.windowHeight)
+}
+
+func main() {
+ font.InitFont()
+ ebiten.SetWindowTitle("Muhq's Game")
+ args := js.Global().Get("ebitenArgs")
+ log.Println(args.String())
+ if args.String() == "" {
+ gameInit(opts{MapName: "the-kraken", Players: []player{player{Name: "muhq", Cards: []string{}}}})
+ } else {
+ dec := json.NewDecoder(strings.NewReader(args.String()))
+ var opts opts
+ if err := dec.Decode(&opts); err != nil {
+ log.Fatal(err)
+ }
+ gameInit(opts)
+ }
+ _ = ebiten.RunGame(g)
+}
diff --git a/go/ui/freeMapControl.go b/go/ui/freeMapControl.go
new file mode 100644
index 00000000..574208d2
--- /dev/null
+++ b/go/ui/freeMapControl.go
@@ -0,0 +1,204 @@
+package ui
+
+import (
+ "log"
+
+ "github.com/hajimehoshi/ebiten/v2"
+
+ "muhq.space/muhqs-game/go/game"
+)
+
+type pannedPermanent struct {
+ WidgetBase
+ p game.Permanent
+}
+
+func NewPannedPermanent(x, y int, p game.Permanent) *pannedPermanent {
+ pp := &pannedPermanent{NewWidgetBase(x, y, PERMANENT_WIDTH, PERMANENT_HEIGHT), p}
+ pp.renderImpl = func() *ebiten.Image { return pp.render() }
+ log.Printf("create new panned permanent")
+ return pp
+}
+
+func (pp *pannedPermanent) move(pan *pan) {
+ pp.X, pp.Y = ebiten.TouchPosition(pan.id)
+}
+
+func (pp *pannedPermanent) render() *ebiten.Image {
+ if pp.img == nil {
+ pp.img = getPermanentSymbol(pp.p, 0)
+ }
+ return pp.img
+}
+
+type FreeMapControl struct {
+ Collection
+ Mv *MapView
+ gameState *game.LocalState
+ storeView *PocList
+ storesOnMap bool
+ choice Widget
+ pannedPermanent *pannedPermanent
+}
+
+func NewFreeMapControl(g *game.LocalState) *FreeMapControl {
+ mc := &FreeMapControl{gameState: g}
+ mc.Mv = NewMapView(g)
+ mc.Mv.RegisterHandler("click", func(x, y int) {
+ if obj := mc.Mv.FindObjectAt(x, y); obj != nil {
+ mc.handleSelection(obj, x, y)
+ }
+ })
+ mc.AddWidget(mc.Mv)
+
+ storeTiles := g.Map().FilterTiles(func(t *game.Tile) bool { return t.Type == game.TileTypes.Store })
+ mc.storesOnMap = len(storeTiles) > 0
+
+ return mc
+}
+
+func (mc *FreeMapControl) showStore(x, y int, tile *game.Tile) {
+ mc.storeView = NewPocList(x, y, mc.gameState.Map().StoreOn(tile.Position))
+ mc.AddWidget(mc.storeView)
+}
+
+func (mc *FreeMapControl) hideStore() {
+ mc.RemoveWidget(mc.storeView)
+ mc.storeView = nil
+}
+
+func (mc *FreeMapControl) addChoice(choice Widget) {
+ if mc.choice != nil {
+ mc.removeChoice()
+ }
+ mc.choice = choice
+ mc.AddWidget(choice)
+}
+
+func (mc *FreeMapControl) removeChoice() {
+ mc.RemoveWidget(mc.choice)
+ mc.choice = nil
+}
+
+func (mc *FreeMapControl) addCreatePermanentChoice(x, y int, kind game.CardType, tile *game.Tile) {
+ choices := []interface{}{"undefined"}
+ // TODO: add all fitting choices from the player decks
+ onclick := func(c *Choice, x, y int) {
+ mc.Mv.ClearPermanentsHighlights()
+ mc.removeChoice()
+ // choice := choices[c.GetChoosen(x, y)]
+ // mc.gameState.NewPerm(choice, tile)
+ }
+ mc.addChoice(NewChoice(x, y, choices, onclick))
+}
+
+func (mc *FreeMapControl) addPermanentChoice(x, y int, p game.Permanent) {
+ choices := []interface{}{"move", "destroy"}
+ onclick := func(c *Choice, x, y int) {
+ mc.Mv.ClearPermanentsHighlights()
+ mc.removeChoice()
+ switch choices[c.GetChoosen(x, y)] {
+ case "move":
+ case "destroy":
+ mc.gameState.RemovePermanent(p)
+ }
+ }
+ mc.addChoice(NewChoice(x, y, choices, onclick))
+}
+
+func (mc *FreeMapControl) handleSelection(obj interface{}, x, y int) {
+ // Hide the store if the user selected something else
+ if mc.storeView != nil {
+ mc.hideStore()
+ }
+
+ switch obj := obj.(type) {
+ case *game.Tile:
+ // TODO: incoorperate the store view intobthe tile choice
+ if mc.storesOnMap && obj.Type == game.TileTypes.Store {
+
+ mc.showStore(x, y, obj)
+ } else if obj.Permanent != nil {
+ // Permanent action
+ mc.Mv.AddHighlightPermanent(obj.Permanent, HighlightSelectionColor)
+ mc.addPermanentChoice(x, y, obj.Permanent)
+ } else {
+ mc.Mv.ClearTileHighlights()
+ mc.Mv.AddHighlightTile(obj, HighlightSelectionColor)
+ // Create permanent
+ choices := []interface{}{"unit", "artifact", "neutralize"}
+ onclick := func(c *Choice, x, y int) {
+ mc.Mv.ClearTileHighlights()
+ mc.removeChoice()
+ switch choices[c.GetChoosen(x, y)] {
+ // TODO: create permanents
+ case "unit":
+ mc.addCreatePermanentChoice(x, y, game.CardTypes.Unit, obj)
+ case "artifact":
+ mc.addCreatePermanentChoice(x, y, game.CardTypes.Artifact, obj)
+ case "neutralize":
+ obj.Neutralize()
+ mc.Mv.ForceRedraw()
+ mc.removeChoice()
+ log.Printf("Neutralize %p at\n", obj, obj.Position)
+ }
+ }
+ mc.addChoice(NewChoice(x, y, choices, onclick))
+
+ }
+ log.Printf("%s selected at %d %d\n", obj, x, y)
+ case game.Permanent:
+ mc.addPermanentChoice(x, y, obj)
+ }
+}
+
+func (mc *FreeMapControl) handlePanning() {
+ pan := TouchManager.pan
+ if pan != nil {
+ if mc.pannedPermanent != nil {
+ mc.pannedPermanent.move(pan)
+ } else if perm, ok := mc.Mv.FindObjectAt(pan.originX, pan.originY).(game.Permanent); ok {
+ x, y := ebiten.TouchPosition(pan.id)
+ mc.pannedPermanent = NewPannedPermanent(x, y, perm)
+ mc.AddWidget(mc.pannedPermanent)
+ }
+ } else if mc.pannedPermanent != nil {
+ pp := mc.pannedPermanent
+ o := mc.Mv.FindObjectAt(pp.X, pp.Y)
+ if tile, ok := o.(*game.Tile); ok {
+ log.Printf("drop panned permanet on %s\n", tile)
+ mc.gameState.MovePermanent(pp.p, tile)
+ mc.Mv.ForceRedraw()
+ }
+ mc.RemoveWidget(mc.pannedPermanent)
+ mc.pannedPermanent = nil
+ }
+}
+
+func (mc *FreeMapControl) Update() error {
+ c := mc.choice
+ if err := mc.Collection.Update(); err != nil {
+ return err
+ }
+
+ // The Choice has already handled the Update
+ if c != mc.choice {
+ return nil
+ }
+
+ mc.handlePanning()
+
+ return nil
+}
+
+func (mc *FreeMapControl) Scale(s float64) {
+ mc.Mv.Scale(s)
+}
+
+// Hide and cancel all control elements
+func (mc *FreeMapControl) CancelControl() {
+ mc.Mv.ClearPermanentsHighlights()
+ mc.Mv.ClearTileHighlights()
+ mc.hideStore()
+ mc.removeChoice()
+}