diff options
| author | Ewy <ewy@ewy.one> | 2026-09-21 21:40:05 +0200 |
|---|---|---|
| committer | Ewy <ewy@ewy.one> | 2026-09-21 21:40:05 +0200 |
| commit | 83b2f2e9922f46fe46fd5330e6b38c66d1483ade (patch) | |
| tree | 21a6a4402fe99e2afabb67f2cc0d41d0c0ea85be | |
| parent | 1f8364b145dbd0ff9ff7583f50aeb7498967b0f9 (diff) | |
(wip)
| -rw-r--r-- | engine/action.go | 83 | ||||
| -rw-r--r-- | engine/card.go | 5 | ||||
| -rw-r--r-- | engine/card/attack.go | 4 | ||||
| -rw-r--r-- | engine/card/range.go | 11 | ||||
| -rw-r--r-- | engine/game.go | 34 | ||||
| -rw-r--r-- | engine/play/play.go | 1 | ||||
| -rw-r--r-- | engine/player.go | 1 | ||||
| -rw-r--r-- | engine/stack.go | 10 | ||||
| -rw-r--r-- | engine/vigour.go | 43 | ||||
| -rw-r--r-- | server/api/game.go | 10 | ||||
| -rw-r--r-- | server/web/game/game.js | 62 | ||||
| -rw-r--r-- | server/web/game/index.html | 7 |
12 files changed, 196 insertions, 75 deletions
diff --git a/engine/action.go b/engine/action.go index 38749c7..178c4a0 100644 --- a/engine/action.go +++ b/engine/action.go @@ -1,12 +1,17 @@ package engine type BaseAction struct { - User *Player - BaseSource IsCard + User *Player + BaseSource IsCard + Description string } -func (b *BaseAction) Actor() *Player { - return b.User +func (b *BaseAction) Actor(g *Game) *Player { + u := b.User + if u == nil { + u = g.CurrentPlayer + } + return u } func (b *BaseAction) Source() IsCard { @@ -17,17 +22,13 @@ func (b *BaseAction) Then(g *Game) { } type Action interface { - Actor() *Player + Actor(g *Game) *Player Source() IsCard Then(g *Game) } -func Interactions(a ...Action) map[InteractionID]Action { - m := make(map[InteractionID]Action, len(a)) - for i, ac := range a { - m[InteractionID(i)] = ac - } - return m +func Interactions(a ...Action) []Action { + return a } type DamageDecision bool @@ -46,28 +47,24 @@ type DesignateDamage struct { func (d *DesignateDamage) Then(g *Game) { } -type PlayCard struct { - *BaseAction -} - -func (u *PlayCard) Then(g *Game) { - g.Actions = Interactions(&PlayCard{}, &UseVigor{}) -} - -type PlayAllOutCard struct { - *BaseAction -} - -func (p *PlayAllOutCard) Then(g *Game) { - g.Actions = Interactions(&EndTurn{}) -} - type ShuffleDeck struct { *BaseAction } func (s *ShuffleDeck) Then(g *Game) { - g.Actions = Interactions(&DrawCard{}) + g.Actions = Interactions(&DrawCard{ + BaseAction: &BaseAction{ + Description: "Draw a card (or lose 1 life)", + }, + }) +} + +func ShuffleDeckAction(g *Game) Action { + return &ShuffleDeck{ + BaseAction: &BaseAction{ + Description: "Pay 1 life to shuffle your deck", + }, + } } type DrawCard struct { @@ -75,21 +72,37 @@ type DrawCard struct { } func (d *DrawCard) Then(g *Game) { - g.Actions = Interactions(&PlayCard{}, &PlayAllOutCard{}, &UseVigor{}) -} - -type UseVigor struct { - *BaseAction + if len(g.CurrentPlayer.DrawPile) < 1 { + g.CurrentPlayer.Life.Subtract(1) + } else { + c := g.Yin.DrawPile.Pop() + g.CurrentPlayer.Hand = append(g.CurrentPlayer.Hand, c.Base()) + } + g.Actions = append(VigorActions(g, g.CurrentPlayer), EndTurnAction(g)) } -func (u *UseVigor) Then(g *Game) { - g.Actions = Interactions(&PlayCard{}, &UseVigor{}) +func DrawCardAction(g *Game) Action { + return &DrawCard{ + BaseAction: &BaseAction{ + Description: "Draw a card (or lose 1 life)", + }, + } } type EndTurn struct { *BaseAction } +func EndTurnAction(g *Game) Action { + return &EndTurn{ + BaseAction: &BaseAction{ + User: g.CurrentPlayer, + BaseSource: nil, + Description: "End your turn", + }, + } +} + func (e *EndTurn) Then(g *Game) { g.NextTurn() } diff --git a/engine/card.go b/engine/card.go index 52d5a6e..3eb6a8f 100644 --- a/engine/card.go +++ b/engine/card.go @@ -11,6 +11,11 @@ func (c *Card) Base() *Card { return c } +func (c *Card) CanPlay(g *Game) bool { + panic("unimplemented") +} + type IsCard interface { Base() *Card + CanPlay(g *Game) bool } diff --git a/engine/card/attack.go b/engine/card/attack.go index d05d44a..a07cf8a 100644 --- a/engine/card/attack.go +++ b/engine/card/attack.go @@ -7,9 +7,7 @@ import ( type Attack struct { *engine.Card - - MinDistance RangeValue - MaxDistance RangeValue + *Range AuraDamage DamageValue LifeDamage DamageValue diff --git a/engine/card/range.go b/engine/card/range.go index 3c6a5c9..96c1a3d 100644 --- a/engine/card/range.go +++ b/engine/card/range.go @@ -6,6 +6,13 @@ const NoMinimumRange = RangeValue(0) const NoMaximumRange = RangeValue(10) type Range struct { - Min RangeValue - Max RangeValue + MinimumRange, MaximumRange RangeValue +} + +func (r *Range) MinRange() RangeValue {return r.MinimumRange} +func (r *Range) MaxRange() RangeValue {return r.MaximumRange} + +type HasRange interface { + MinRange() RangeValue + MaxRange() RangeValue } diff --git a/engine/game.go b/engine/game.go index 5130864..8365869 100644 --- a/engine/game.go +++ b/engine/game.go @@ -4,6 +4,12 @@ import ( "sakura/engine/pool" ) +type Turn struct { + Player *Player + ActionTaken bool + Actions []Action +} + type Game struct { Shadow *pool.Pool Distance *pool.Pool @@ -12,9 +18,11 @@ type Game struct { Stack []IsCard - Actions map[InteractionID]Action + Actions []Action CurrentPlayer *Player + + History []Turn } func (g *Game) propagate() { @@ -25,7 +33,6 @@ func NewGame() *Game { g := &Game{ Shadow: pool.New(0, pool.Infinite), Distance: pool.New(10, 10), - Actions: nil, } g.Yin = NewPlayer(g) g.Yang = NewPlayer(g) @@ -52,15 +59,18 @@ func (g *Game) Winner() *Player { func (g *Game) NextTurn() { current := g.CurrentPlayer.Opponent(g) g.CurrentPlayer = current - g.Actions = Interactions(&ShuffleDeck{ - &BaseAction{ - User: current, - BaseSource: nil, - }, - }, &DrawCard{ - &BaseAction{ - User: current, - BaseSource: nil, - }, + g.History = append(g.History, Turn{ + Player: current, + ActionTaken: false, }) + g.Actions = Interactions(ShuffleDeckAction(g),DrawCardAction(g)), +} + +func (g *Game) CurrentTurn() Turn { + return g.History[len(g.History) - 1] +} + +func (g *Game) ActionToHistory(a Action) { + thisTurn := g.History[len(g.History)-1] + thisTurn.Actions = append(thisTurn.Actions, a) } diff --git a/engine/play/play.go b/engine/play/play.go index 42d9fa3..78b2c68 100644 --- a/engine/play/play.go +++ b/engine/play/play.go @@ -25,6 +25,7 @@ func Attack(g *engine.Game, p *engine.Player, a card.IsAttack) { BaseAction: &engine.BaseAction{ User: p, BaseSource: r, + Description: "Execute this unknown attack", }, } } diff --git a/engine/player.go b/engine/player.go index 5f1d7ca..eb7ac49 100644 --- a/engine/player.go +++ b/engine/player.go @@ -12,6 +12,7 @@ type Player struct { Flare *pool.Pool Hand Stack MasterDeck Stack + DrawPile Stack Ultimates []*Card } diff --git a/engine/stack.go b/engine/stack.go index d7fc7be..8bcff6f 100644 --- a/engine/stack.go +++ b/engine/stack.go @@ -20,3 +20,13 @@ func (s Stack) Filter(predicate func(c IsCard) bool) Stack { } return res } + +func (s Stack) Pop() IsCard { + if len(s) < 1 { + return nil + } else { + c := s[0] + s = s[1:] + return c + } +} diff --git a/engine/vigour.go b/engine/vigour.go index 62ececd..24732d0 100644 --- a/engine/vigour.go +++ b/engine/vigour.go @@ -7,14 +7,40 @@ import ( type Vigour int type VigorAction struct { + *BaseAction Name string From func(p *Player) *pool.Pool To func(p *Player) *pool.Pool } +func (v *VigorAction) Then(g *Game) { + v.From(v.Actor(g)).Add(-1) + v.To(v.Actor(g)).Add(1) + v.Actor(g).Vigour -= 1 +} + +func (v *VigorAction) Can(p *Player) bool { + return p.Vigour > 0 && v.From(p).Has(1) && v.To(p).Fits(1) +} + +func VigorActions(g *Game, p *Player) []Action { + var acs []Action + for _, a := range Actions { + if a.Can(p) { + acs = append(acs, a) + } + } + return acs +} + var Actions = []*VigorAction{ { Name: "Approach", + BaseAction: &BaseAction{ + User: nil, + BaseSource: nil, + Description: "Move a sakura from Distance to Aura", + }, From: func(p *Player) *pool.Pool { return p.Game.Distance }, @@ -24,6 +50,11 @@ var Actions = []*VigorAction{ }, { Name: "Retreat", + BaseAction: &BaseAction{ + User: nil, + BaseSource: nil, + Description: "Move a sakura from Aura to Distance", + }, From: func(p *Player) *pool.Pool { return p.Aura }, @@ -33,6 +64,11 @@ var Actions = []*VigorAction{ }, { Name: "Charge", + BaseAction: &BaseAction{ + User: nil, + BaseSource: nil, + Description: "Move a sakura from your Aura to Flare", + }, From: func(p *Player) *pool.Pool { return p.Aura }, @@ -41,7 +77,12 @@ var Actions = []*VigorAction{ }, }, { - Name: "Recover", + Name: "Charge", + BaseAction: &BaseAction{ + User: nil, + BaseSource: nil, + Description: "Move a sakura from Aura to Flare", + }, From: func(p *Player) *pool.Pool { return p.Game.Shadow }, diff --git a/server/api/game.go b/server/api/game.go index bccf12a..636ff66 100644 --- a/server/api/game.go +++ b/server/api/game.go @@ -1,11 +1,12 @@ package api import ( - "github.com/gorilla/websocket" "net/http" "sakura/engine" "slices" "uuid" + + "github.com/gorilla/websocket" ) type Conn struct { @@ -73,7 +74,12 @@ func PumpRead(id uuid.UUID, g *engine.Game, conn *websocket.Conn) { if err != nil { break } - g.Actions[interaction].Then(g) + a := g.Actions[interaction] + if a == nil { + continue + } + a.Then(g) + g.ActionToHistory(a) Update(id, g) } } diff --git a/server/web/game/game.js b/server/web/game/game.js index 5e71786..4b0024c 100644 --- a/server/web/game/game.js +++ b/server/web/game/game.js @@ -1,39 +1,63 @@ -"use strict" +"use strict"; const showGame = (g) => { - const tmpl = document.querySelector("template#tmpl") - const disp = document.querySelector("#game") - const clone = document.importNode(tmpl.content, true) - doDistance(clone, g.Distance.Value) - disp.replaceChildren(clone) + const tmpl = document.querySelector("template#tmpl"); + const disp = document.querySelector("#game"); + const clone = document.importNode(tmpl.content, true); + doDistance(clone, g.Distance.Value); + doActions(clone, g.Actions); + disp.replaceChildren(clone); }; const doDistance = (clone, value) => { clone.querySelector(".distance").setAttribute("data-value", value); [...clone.querySelectorAll(".distance .sakura")].map((n, i) => { if (i < value) { - n.checked = true + n.checked = true; } else { - n.checked = false + n.checked = false; } - }) + }); +}; + +const doActions = (g, actions) => { + const actionList = g.querySelector(".actions"); + const actionTemplate = actionList.querySelector(".action").cloneNode(true); + if (actions) { + actionList.replaceChildren( + ...actions.map((a, i) => { + const newAction = actionTemplate.cloneNode(true); + newAction.setAttribute("data-index", i); + newAction.querySelector(".description").innerText = a.Description; + return newAction; + }), + ); + } else { + actionList.replaceChildren() + } +}; +const doAction = (idx) => { + window.socket.send(String(idx)) } const connect = (gameId) => { const connection = new WebSocket(`/api/game/${gameId}/connect`); window.socket = connection; connection.addEventListener("message", (ev) => { - const game = JSON.parse(ev.data); - console.debug(game); - showGame(game); - document.querySelector("#game-json").innerText = - JSON.stringify(ev.data, null, 2); + const game = JSON.parse(ev.data); + console.debug(game); + showGame(game); + document.querySelector("#game-json").innerText = JSON.stringify( + ev.data, + null, + 2, + ); }); -} +}; const init = () => { - const id = window.location.hash.slice(1) - connect(id) -} + const id = window.location.hash.slice(1); + connect(id); +}; -init() +init(); diff --git a/server/web/game/index.html b/server/web/game/index.html index 414ff5c..f4f6593 100644 --- a/server/web/game/index.html +++ b/server/web/game/index.html @@ -15,7 +15,7 @@ <header> <details> <summary>Game state</summary> - <pre id="game-json"></pre> + <code id="game-json"></code> </details> </header> <main id="game"></main> @@ -33,6 +33,11 @@ <input type="checkbox" class="sakura" /> <input type="checkbox" class="sakura" /> </div> + <ul class="actions"> + <li class="action" data-index=""> + <button type="button" class="description" onclick="window.socket.send(closest('.action').getAttribute('data-index'))">Do</button> + </li> + </s> </template> <script src="/game/game.js"></script> |
