diff options
| -rw-r--r-- | go/Makefile | 4 | ||||
| -rw-r--r-- | go/ai-companion/main.go | 176 | ||||
| -rw-r--r-- | go/ui/freeMapControl.go | 204 |
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() +} |
