aboutsummaryrefslogtreecommitdiff
path: root/go/game
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-08-21 23:37:40 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-21 23:39:34 +0200
commit6ea5f8fb816e876a6b434d90565660e42f27a7f3 (patch)
treeb18b314142024405085a2d2ed3e1e17723197401 /go/game
parentaf168066174ee2e7172bd3acb16e316265c1189b (diff)
downloadmuhqs-game-6ea5f8fb816e876a6b434d90565660e42f27a7f3.tar.gz
muhqs-game-6ea5f8fb816e876a6b434d90565660e42f27a7f3.zip
support concessions
Diffstat (limited to 'go/game')
-rw-r--r--go/game/action.go27
-rw-r--r--go/game/player.go38
-rw-r--r--go/game/state.go27
-rw-r--r--go/game/winCondition.go30
-rw-r--r--go/game/winCondition_test.go28
5 files changed, 135 insertions, 15 deletions
diff --git a/go/game/action.go b/go/game/action.go
index dbbc8700..5a564b07 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -803,15 +803,34 @@ func UseAttackAction(a Action) {
}
}
+type ConcedeAction struct {
+ PassPriority
+}
+
+func NewConcedeAction(p *Player) *ConcedeAction {
+ return &ConcedeAction{PassPriority{p}}
+}
+
+func (a *ConcedeAction) resolve(*LocalState) {
+ a.player.concede()
+}
+
+func (a *ConcedeAction) String() string {
+ return fmt.Sprintf("%s concedes", a.player.Name)
+}
+
// Marshal an action to plain text.
// The format of the action is `SOURCE ACTION CONTEXT`.
// PassPriority: PLAYER pass
+// ConcedeAction: PLAYER concede
// DraftPick: PLAYER pick CARD:PACK
func MarshalAction(_a Action) []byte {
out := []byte{}
switch a := _a.(type) {
case *PassPriority:
out = fmt.Appendf(out, "%s\u00A0pass", a.player.Name)
+ case *ConcedeAction:
+ out = fmt.Appendf(out, "%s\u00A0concede", a.player.Name)
case *DraftPick:
out = fmt.Appendf(out, "%s\u00A0pick\u00A0%s:%s", a.player.Name, a.pick.Path(), a.pack)
default:
@@ -851,13 +870,17 @@ func UnmarshalAction(s State, in []byte) (a Action, err error) {
return nil, err
}
a = NewDraftPick(p, pack, pick)
- case "pass":
+ case "pass", "concede":
p := s.PlayerByName(string(m[1]))
if p == nil {
err = ErrUnknownPlayer
return
}
- a = NewPassPriority(p)
+ if _action == "pass" {
+ a = NewPassPriority(p)
+ } else {
+ a = NewConcedeAction(p)
+ }
}
return
}
diff --git a/go/game/player.go b/go/game/player.go
index 2edbe923..4eb8df2a 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -1,8 +1,8 @@
package game
import (
- "log"
-
+ "fmt"
+ "muhq.space/muhqs-game/go/log"
"muhq.space/muhqs-game/go/utils"
)
@@ -16,6 +16,7 @@ type Player struct {
Turn int
Resource int
DrawPerTurn int
+ Conceded bool
Hand *Hand
DiscardPile *DiscardPile
Deck *Deck
@@ -113,12 +114,13 @@ func (p *Player) reduceResource(amount int) {
func (p *Player) upkeep() []*Player {
p.gainResource(p.ResourceGain())
+ s := p.gameState
// TODO: handle upkeep triggers
// Skip upkeep prompt if player does not controll any units
controllsUnits := false
- for _, u := range p.gameState.units {
+ for _, u := range s.units {
if u.Controller() == p {
controllsUnits = true
break
@@ -137,12 +139,18 @@ func (p *Player) upkeep() []*Player {
a = newUpkeepAction(p)
}
+ if _, ok := a.(*ConcedeAction); ok {
+ p.concede()
+ return s._map.WinCondition.check(s)
+ }
+
// FIXME: upkeep action is special paying is part of its effect
- p.gameState.declareAction(a)
- return p.gameState.stack.resolve()
+ s.declareAction(a)
+ return s.stack.resolve()
}
func (p *Player) actionPhase() []*Player {
+ s := p.gameState
for {
a, err := promptAction(p.Ctrl)
if err != nil {
@@ -151,11 +159,15 @@ func (p *Player) actionPhase() []*Player {
if _, ok := a.(*PassPriority); ok {
return nil
}
+ if _, ok := a.(*ConcedeAction); ok {
+ p.concede()
+ return s._map.WinCondition.check(s)
+ }
if a == nil {
log.Fatal("Received nil action from ", p.Name)
}
- p.gameState.declareAction(a)
- w := p.gameState.stack.resolve()
+ s.declareAction(a)
+ w := s.stack.resolve()
if len(w) > 0 {
return w
}
@@ -175,6 +187,10 @@ func (p *Player) buyPhase() []*Player {
if _, ok := a.(*PassPriority); ok {
return nil
}
+ if _, ok := a.(*ConcedeAction); ok {
+ p.concede()
+ return s._map.WinCondition.check(s)
+ }
if a.Targets().HasSelections() {
s.declareAction(a)
@@ -222,6 +238,9 @@ func (p *Player) PromptHandCardSelection(min, max int) []*Card {
return utils.InterfaceSliceToTypedSlice[*Card](a.Target().sel)
case *PassPriority:
return nil
+ case *ConcedeAction:
+ p.concede()
+ return nil
default:
log.Panicf("Unexpected response type %T for hand card selection", a)
}
@@ -269,3 +288,8 @@ func (p *Player) addKnownStore(pos Position) {
func (p *Player) clearKnownStore() {
p.knownStores = make(map[Position]bool)
}
+
+func (p *Player) concede() {
+ p.Conceded = true
+ log.Info(fmt.Sprintf("%s concedes", p.Name))
+}
diff --git a/go/game/state.go b/go/game/state.go
index 37f31422..912d88f3 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -164,6 +164,7 @@ func (s *LocalState) Loop() []*Player {
for {
for _, p := range s.players {
+ // TODO: remove conceded players from turn rotation
s.activePlayerId = p.Id
p.Turn++
@@ -426,6 +427,9 @@ func (s *LocalState) orderTriggeredActions(triggeredActions []*TriggeredAction)
}
switch a := _a.(type) {
+ case *ConcedeAction:
+ a.player.concede()
+ return nil
case *DeclareTriggeredActionsAction:
for _, a := range a.actions { // Use the order passed back by the user
pActions = append(pActions, a)
@@ -451,11 +455,15 @@ func (s *LocalState) allPassing(skipFirst bool) (bool, []*Player) {
i++
}
for ; i < nPlayers; i++ {
- p := s.players[(i+s.activePlayerId-1)%nPlayers]
w := s.stateBasedActions()
if len(w) > 0 {
return false, w
}
+
+ p := s.players[(i+s.activePlayerId-1)%nPlayers]
+ if p.Conceded {
+ continue
+ }
p.Ctrl.SendNotification(newPriorityNotification())
_a, err := p.Ctrl.RecvAction()
if err != nil {
@@ -463,6 +471,8 @@ func (s *LocalState) allPassing(skipFirst bool) (bool, []*Player) {
}
switch a := _a.(type) {
+ case *ConcedeAction:
+ a.player.concede()
case *PassPriority:
log.Info("priority passed", "player", p.Name, "passed", fmt.Sprintf("%d/%d", i+1, nPlayers))
case nil:
@@ -588,9 +598,20 @@ func (s *LocalState) destroyPermanent(p Permanent) {
if pile := p.Pile(); len(pile) > 0 {
dropAction := newPileDropAction(p, t, pile)
- prompt(p.Controller().Ctrl,
+ _a, err := prompt(p.Controller().Ctrl,
newTargetSelectionPrompt(dropAction, fmt.Sprintf("Drop %s's pile", p)))
- s.declareAction(dropAction)
+ if err != nil {
+ // FIXME
+ }
+
+ switch a := _a.(type) {
+ case *ConcedeAction:
+ a.player.concede()
+ case *PileDropAction:
+ s.declareAction(a)
+ default:
+ log.Error("received other action on pile drop prompt")
+ }
}
p.Owner().DiscardPile.AddCard(p.Card())
diff --git a/go/game/winCondition.go b/go/game/winCondition.go
index a0728e60..6ca211b8 100644
--- a/go/game/winCondition.go
+++ b/go/game/winCondition.go
@@ -44,12 +44,17 @@ var KingGame = &winCondition{
}
if len(foundKings) == len(s.Players()) {
- return []*Player{}
+ return singleConcessionWinner(s)
}
- loosers := make([]*Player, 2, len(s.Players()))
+ nPlayers := len(s.Players())
+ loosers := make([]*Player, nPlayers, nPlayers)
copy(loosers, s.Players())
for p := range foundKings {
+ // Considere players who have conceded as loosers
+ if p.Conceded {
+ continue
+ }
i := slices.Index(loosers, p)
loosers[i] = loosers[len(loosers)-1]
loosers = loosers[:len(loosers)-1]
@@ -79,7 +84,26 @@ var DeathMatch = &winCondition{
winners = append(winners, p)
}
}
- return winners
+ if len(winners) > 0 {
+ return winners
+ }
+ return singleConcessionWinner(s)
},
desc: "Destroy all enemy units",
}
+
+func singleConcessionWinner(s *LocalState) []*Player {
+ var winner *Player
+ for _, p := range s.players {
+ if p.Conceded {
+ continue
+ }
+ if winner == nil {
+ winner = p
+ // Two players have not conceded yet
+ } else {
+ return nil
+ }
+ }
+ return []*Player{winner}
+}
diff --git a/go/game/winCondition_test.go b/go/game/winCondition_test.go
index c50e9692..3c1551a6 100644
--- a/go/game/winCondition_test.go
+++ b/go/game/winCondition_test.go
@@ -38,3 +38,31 @@ func TestKingGame(t *testing.T) {
t.Fatal("p or o is not a winner", w)
}
}
+
+func TestConcession(t *testing.T) {
+ s := NewLocalState()
+ s.SetMap(newEmpty2x2Map())
+ p := s.AddNewPlayer("p", NewDeck())
+ s.addNewUnit(NewCard("misc/king"), Position{0, 0}, p)
+ o := s.AddNewPlayer("o", NewDeck())
+ s.addNewUnit(NewCard("misc/king"), Position{1, 1}, o)
+
+ o.concede()
+ s._map.WinCondition = KingGame
+ w := s._map.WinCondition.check(s)
+ if len(w) != 1 {
+ t.Fatal("winner not declared", w)
+ }
+ if w[0] != p {
+ t.Fatal("p is not the winner", w)
+ }
+
+ s._map.WinCondition = DeathMatch
+ w = s._map.WinCondition.check(s)
+ if len(w) != 1 {
+ t.Fatal("winner not declared", w)
+ }
+ if w[0] != p {
+ t.Fatal("p is not the winner", w)
+ }
+}