diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-08-21 23:37:40 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-21 23:39:34 +0200 |
| commit | 6ea5f8fb816e876a6b434d90565660e42f27a7f3 (patch) | |
| tree | b18b314142024405085a2d2ed3e1e17723197401 /go/game | |
| parent | af168066174ee2e7172bd3acb16e316265c1189b (diff) | |
| download | muhqs-game-6ea5f8fb816e876a6b434d90565660e42f27a7f3.tar.gz muhqs-game-6ea5f8fb816e876a6b434d90565660e42f27a7f3.zip | |
support concessions
Diffstat (limited to 'go/game')
| -rw-r--r-- | go/game/action.go | 27 | ||||
| -rw-r--r-- | go/game/player.go | 38 | ||||
| -rw-r--r-- | go/game/state.go | 27 | ||||
| -rw-r--r-- | go/game/winCondition.go | 30 | ||||
| -rw-r--r-- | go/game/winCondition_test.go | 28 |
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) + } +} |
