mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Add UNO sudden death and multiplayer bot turn announcements
2-player UNO (especially No Mercy) regularly stalls past 100 turns. Sudden death announces at turn 80 with a 20-turn countdown, then scores hands by card point values — lowest total wins. Applies to solo (vs bot) and multiplayer when 2 active players remain. Also adds "It's X's turn" labels to bot plays in multiplayer, matching the format human plays already had, and adds sudden death checks to all turn advancement paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -225,6 +225,10 @@ type unoGame struct {
|
|||||||
stackTotal int // cumulative draw penalty during stacking
|
stackTotal int // cumulative draw penalty during stacking
|
||||||
stackMinValue int // minimum draw value to stack (0 = not stacking)
|
stackMinValue int // minimum draw value to stack (0 = not stacking)
|
||||||
|
|
||||||
|
// Sudden death (long-game point scoring)
|
||||||
|
suddenDeath bool
|
||||||
|
suddenDeathTurn int // turn at which game ends
|
||||||
|
|
||||||
idleTimer *time.Timer
|
idleTimer *time.Timer
|
||||||
warningTimer *time.Timer
|
warningTimer *time.Timer
|
||||||
}
|
}
|
||||||
@@ -346,6 +350,16 @@ var unoCommentary = map[string][]string{
|
|||||||
"long_game": {
|
"long_game": {
|
||||||
"\"Still going, are we? 💛\" *glances at bookmark*",
|
"\"Still going, are we? 💛\" *glances at bookmark*",
|
||||||
},
|
},
|
||||||
|
"sudden_death": {
|
||||||
|
"*GogoBee puts the book down.* \"This has gone on long enough. 💛\"",
|
||||||
|
"*GogoBee glances at the clock.* \"Time to settle this. 💛\"",
|
||||||
|
},
|
||||||
|
"sudden_death_win": {
|
||||||
|
"\"Well played. The numbers don't lie. 💛\" *resumes reading*",
|
||||||
|
},
|
||||||
|
"sudden_death_lose": {
|
||||||
|
"\"The math was on my side. 💛\" *turns a page*",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func unoBotName() string {
|
func unoBotName() string {
|
||||||
@@ -1154,6 +1168,10 @@ func (p *UnoPlugin) soloBotHandleStack(game *unoGame) error {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func (p *UnoPlugin) botTurn(game *unoGame) error {
|
func (p *UnoPlugin) botTurn(game *unoGame) error {
|
||||||
|
if p.checkSuddenDeath(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// No Mercy stacking: if a stack is pending, bot must handle it
|
// No Mercy stacking: if a stack is pending, bot must handle it
|
||||||
if game.noMercy && game.stackMinValue > 0 {
|
if game.noMercy && game.stackMinValue > 0 {
|
||||||
return p.soloBotHandleStack(game)
|
return p.soloBotHandleStack(game)
|
||||||
@@ -1572,9 +1590,132 @@ func (p *UnoPlugin) botChooseColor(game *unoGame) unoColor {
|
|||||||
return botPickColor(game.botHand)
|
return botPickColor(game.botHand)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sudden Death (long-game point scoring for 2-player matches)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const (
|
||||||
|
suddenDeathAnnounce = 80 // turn to announce sudden death
|
||||||
|
suddenDeathCountdown = 20 // turns after announcement
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkSuddenDeath checks whether sudden death should be announced or resolved.
|
||||||
|
// Returns true if the game ended.
|
||||||
|
func (p *UnoPlugin) checkSuddenDeath(game *unoGame) bool {
|
||||||
|
if game.done {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve: game ends at the deadline
|
||||||
|
if game.suddenDeath && game.turns >= game.suddenDeathTurn {
|
||||||
|
p.suddenDeathWinner(game)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce
|
||||||
|
if !game.suddenDeath && game.turns >= suddenDeathAnnounce {
|
||||||
|
game.suddenDeath = true
|
||||||
|
game.suddenDeathTurn = game.turns + suddenDeathCountdown
|
||||||
|
remaining := game.suddenDeathTurn - game.turns
|
||||||
|
|
||||||
|
ann := fmt.Sprintf("⏰ **SUDDEN DEATH!** This match ends in %d turns — lowest hand value wins!\n\n%s",
|
||||||
|
remaining, pickCommentary("sudden_death"))
|
||||||
|
p.SendMessage(game.dmRoomID, ann)
|
||||||
|
p.SendMessage(game.roomID, fmt.Sprintf("⏰ **Sudden Death activated!** %s's match ends in %d turns.",
|
||||||
|
p.DisplayName(game.playerID), remaining))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown reminders (DM only)
|
||||||
|
if game.suddenDeath {
|
||||||
|
remaining := game.suddenDeathTurn - game.turns
|
||||||
|
switch remaining {
|
||||||
|
case 10:
|
||||||
|
p.SendMessage(game.dmRoomID, "⏰ **10 turns remaining!**")
|
||||||
|
case 5:
|
||||||
|
p.SendMessage(game.dmRoomID, "⏰ **5 turns remaining!**")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnoPlugin) suddenDeathWinner(game *unoGame) {
|
||||||
|
p.mu.Lock()
|
||||||
|
if game.done {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
game.done = true
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
playerScore := scoreHand(game.playerHand)
|
||||||
|
botScore := scoreHand(game.botHand)
|
||||||
|
bn := unoBotName()
|
||||||
|
playerName := p.DisplayName(game.playerID)
|
||||||
|
|
||||||
|
// Build score breakdown
|
||||||
|
breakdown := fmt.Sprintf("**Final Scores:**\n%s: %s\n%s: %s",
|
||||||
|
playerName, formatHandScore(game.playerHand),
|
||||||
|
bn, formatHandScore(game.botHand))
|
||||||
|
|
||||||
|
var playerWins bool
|
||||||
|
switch {
|
||||||
|
case playerScore < botScore:
|
||||||
|
playerWins = true
|
||||||
|
case botScore < playerScore:
|
||||||
|
playerWins = false
|
||||||
|
case len(game.playerHand) < len(game.botHand):
|
||||||
|
// Tiebreaker: fewer cards
|
||||||
|
playerWins = true
|
||||||
|
breakdown += "\n*Tiebreaker: fewer cards!*"
|
||||||
|
case len(game.botHand) < len(game.playerHand):
|
||||||
|
playerWins = false
|
||||||
|
breakdown += "\n*Tiebreaker: fewer cards!*"
|
||||||
|
default:
|
||||||
|
// Final tiebreaker: player wins (bot had the advantage of going second in the countdown)
|
||||||
|
playerWins = true
|
||||||
|
breakdown += "\n*Tiebreaker: dead even — player takes it!*"
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("⏰ **TIME'S UP — SUDDEN DEATH!**\n\n%s", breakdown))
|
||||||
|
|
||||||
|
potBefore := p.getPot()
|
||||||
|
|
||||||
|
if playerWins {
|
||||||
|
payout := p.claimFromPot(game.wager)
|
||||||
|
totalPayout := game.wager + payout
|
||||||
|
p.euro.Credit(game.playerID, totalPayout, "uno_win")
|
||||||
|
|
||||||
|
newPot := p.getPot()
|
||||||
|
p.SendMessage(game.dmRoomID, "🎉 **You win on points!**")
|
||||||
|
p.SendMessage(game.roomID, fmt.Sprintf(
|
||||||
|
"⏰ **Sudden Death!** %s wins on points!\n%s\n€%d claimed from the community pot. (Pot: €%d)\n\n%s",
|
||||||
|
playerName, breakdown, int(payout), int(newPot), pickCommentary("sudden_death_win")))
|
||||||
|
|
||||||
|
p.recordGame(game, "sudden_death_player", potBefore)
|
||||||
|
} else {
|
||||||
|
p.addToPot(game.wager)
|
||||||
|
newPot := p.getPot()
|
||||||
|
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("💀 **%s wins on points.** Better luck next time.", bn))
|
||||||
|
p.SendMessage(game.roomID, fmt.Sprintf(
|
||||||
|
"⏰ **Sudden Death!** %s wins on points!\n%s\n%s's €%d added to the community pot. (Pot: €%d)\n\n%s",
|
||||||
|
bn, breakdown, playerName, int(game.wager), int(newPot), pickCommentary("sudden_death_lose")))
|
||||||
|
|
||||||
|
recordBotDefeat(game.playerID, "uno")
|
||||||
|
p.recordGame(game, "sudden_death_bot", potBefore)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cleanupGame(game)
|
||||||
|
}
|
||||||
|
|
||||||
// afterBotTurn is called when the bot's turn was skipped (player played skip/reverse/draw two).
|
// afterBotTurn is called when the bot's turn was skipped (player played skip/reverse/draw two).
|
||||||
// Shows the hand display for the player's next turn.
|
// Shows the hand display for the player's next turn.
|
||||||
func (p *UnoPlugin) afterBotTurn(game *unoGame) error {
|
func (p *UnoPlugin) afterBotTurn(game *unoGame) error {
|
||||||
|
if p.checkSuddenDeath(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return p.playerTurnOrAutoDraw(game)
|
return p.playerTurnOrAutoDraw(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1630,6 +1771,10 @@ func (p *UnoPlugin) sendHandDisplayStacking(game *unoGame) {
|
|||||||
// playerTurnOrAutoDraw shows the hand if the player has playable cards,
|
// playerTurnOrAutoDraw shows the hand if the player has playable cards,
|
||||||
// otherwise auto-draws and passes to the bot.
|
// otherwise auto-draws and passes to the bot.
|
||||||
func (p *UnoPlugin) playerTurnOrAutoDraw(game *unoGame) error {
|
func (p *UnoPlugin) playerTurnOrAutoDraw(game *unoGame) error {
|
||||||
|
if p.checkSuddenDeath(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// No Mercy stacking: if a stack is pending, show stack prompt
|
// No Mercy stacking: if a stack is pending, show stack prompt
|
||||||
if game.noMercy && game.stackMinValue > 0 {
|
if game.noMercy && game.stackMinValue > 0 {
|
||||||
if hasStackableCard(game.playerHand, game.topColor, game.stackMinValue) {
|
if hasStackableCard(game.playerHand, game.topColor, game.stackMinValue) {
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ type unoMultiGame struct {
|
|||||||
stackTotal int // cumulative draw penalty during stacking
|
stackTotal int // cumulative draw penalty during stacking
|
||||||
stackMinValue int // minimum draw value to stack (0 = not stacking)
|
stackMinValue int // minimum draw value to stack (0 = not stacking)
|
||||||
|
|
||||||
|
// Sudden death (long-game point scoring)
|
||||||
|
suddenDeath bool
|
||||||
|
suddenDeathTurn int
|
||||||
|
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
inactiveTimer *time.Timer // 10-minute game timeout
|
inactiveTimer *time.Timer // 10-minute game timeout
|
||||||
mu sync.Mutex // per-game lock
|
mu sync.Mutex // per-game lock
|
||||||
@@ -686,10 +690,17 @@ func (p *UnoPlugin) executeMultiTurn(game *unoMultiGame) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
if roomBuf.Len() > 0 {
|
||||||
|
p.SendMessage(game.roomID, roomBuf.String())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Human's turn — flush room buffer
|
// Human's turn — flush bot play buffer first so the turn announcement arrives
|
||||||
|
// before any auto-action happens.
|
||||||
if roomBuf.Len() > 0 {
|
if roomBuf.Len() > 0 {
|
||||||
p.SendMessage(game.roomID, roomBuf.String())
|
p.SendMessage(game.roomID, roomBuf.String())
|
||||||
roomBuf.Reset()
|
roomBuf.Reset()
|
||||||
@@ -837,6 +848,9 @@ func (p *UnoPlugin) advanceAndExecute(game *unoMultiGame) {
|
|||||||
game.currentIdx = game.nextActiveIdx()
|
game.currentIdx = game.nextActiveIdx()
|
||||||
game.turnID++
|
game.turnID++
|
||||||
game.turns++
|
game.turns++
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,6 +954,9 @@ func (p *UnoPlugin) handleMultiDMInput(ctx MessageContext, game *unoMultiGame) e
|
|||||||
game.turnID++
|
game.turnID++
|
||||||
game.turns++
|
game.turns++
|
||||||
p.SendMessage(game.roomID, absorbMsg+"\n"+p.nextTurnLabel(game))
|
p.SendMessage(game.roomID, absorbMsg+"\n"+p.nextTurnLabel(game))
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1185,6 +1202,9 @@ func (p *UnoPlugin) handleMultiColorChoice(game *unoMultiGame, player *unoMultiP
|
|||||||
game.currentIdx = nextIdx
|
game.currentIdx = nextIdx
|
||||||
game.currentIdx = game.nextActiveIdx()
|
game.currentIdx = game.nextActiveIdx()
|
||||||
game.turnID++
|
game.turnID++
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1554,6 +1574,9 @@ func (p *UnoPlugin) applyAndAnnounce(game *unoMultiGame, player *unoMultiPlayer,
|
|||||||
roomMsg.WriteString(fmt.Sprintf("\n It's %s's turn.", nextUpName))
|
roomMsg.WriteString(fmt.Sprintf("\n It's %s's turn.", nextUpName))
|
||||||
|
|
||||||
p.SendMessage(game.roomID, roomMsg.String())
|
p.SendMessage(game.roomID, roomMsg.String())
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1616,6 +1639,9 @@ func (p *UnoPlugin) resolveWD4Challenge(game *unoMultiGame, challenged bool) {
|
|||||||
fmt.Sprintf("🃏 %s accepts the Wild Draw Four. Draws 4 and is skipped!", victimName))
|
fmt.Sprintf("🃏 %s accepts the Wild Draw Four. Draws 4 and is skipped!", victimName))
|
||||||
game.currentIdx = game.nextActiveIdx()
|
game.currentIdx = game.nextActiveIdx()
|
||||||
game.turnID++
|
game.turnID++
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1638,6 +1664,9 @@ func (p *UnoPlugin) resolveWD4Challenge(game *unoMultiGame, challenged bool) {
|
|||||||
wd4Name, game.wd4PrevColor.Emoji(), game.wd4PrevColor, wd4Name))
|
wd4Name, game.wd4PrevColor.Emoji(), game.wd4PrevColor, wd4Name))
|
||||||
// Victim is NOT skipped — turn continues from victim
|
// Victim is NOT skipped — turn continues from victim
|
||||||
game.turnID++
|
game.turnID++
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
} else {
|
} else {
|
||||||
// Challenge fails — victim draws 6 (4 + 2 penalty)
|
// Challenge fails — victim draws 6 (4 + 2 penalty)
|
||||||
@@ -1649,6 +1678,9 @@ func (p *UnoPlugin) resolveWD4Challenge(game *unoMultiGame, challenged bool) {
|
|||||||
// Victim is skipped
|
// Victim is skipped
|
||||||
game.currentIdx = game.nextActiveIdx()
|
game.currentIdx = game.nextActiveIdx()
|
||||||
game.turnID++
|
game.turnID++
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1980,6 +2012,14 @@ func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next turn label (applyCardEffects already advanced currentIdx)
|
||||||
|
nextUp := game.currentPlayer()
|
||||||
|
nextUpName := p.DisplayName(nextUp.userID)
|
||||||
|
if nextUp.isBot {
|
||||||
|
nextUpName = unoBotName()
|
||||||
|
}
|
||||||
|
roomBuf.WriteString(fmt.Sprintf("\n It's %s's turn.", nextUpName))
|
||||||
|
|
||||||
game.turns++
|
game.turns++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2130,6 +2170,9 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
|||||||
game.currentIdx = nextIdx
|
game.currentIdx = nextIdx
|
||||||
game.currentIdx = game.nextActiveIdx()
|
game.currentIdx = game.nextActiveIdx()
|
||||||
game.turnID++
|
game.turnID++
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2344,9 +2387,166 @@ func (p *UnoPlugin) applyAutoEffects(game *unoMultiGame, player *unoMultiPlayer,
|
|||||||
p.startWD4Challenge(game, player)
|
p.startWD4Challenge(game, player)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if p.checkMultiSuddenDeath(game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.executeMultiTurn(game)
|
p.executeMultiTurn(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sudden Death (multiplayer — only when 2 active players remain)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// checkMultiSuddenDeath checks whether sudden death should be announced or resolved.
|
||||||
|
// Only applies when exactly 2 active players remain. Returns true if the game ended.
|
||||||
|
// Caller must hold game.mu.
|
||||||
|
func (p *UnoPlugin) checkMultiSuddenDeath(game *unoMultiGame) bool {
|
||||||
|
if game.done {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
active := game.activePlayers()
|
||||||
|
if len(active) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve
|
||||||
|
if game.suddenDeath && game.turns >= game.suddenDeathTurn {
|
||||||
|
p.multiSuddenDeathWinner(game, active)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce
|
||||||
|
if !game.suddenDeath && game.turns >= suddenDeathAnnounce {
|
||||||
|
game.suddenDeath = true
|
||||||
|
game.suddenDeathTurn = game.turns + suddenDeathCountdown
|
||||||
|
remaining := game.suddenDeathTurn - game.turns
|
||||||
|
|
||||||
|
p.SendMessage(game.roomID, fmt.Sprintf(
|
||||||
|
"⏰ **SUDDEN DEATH!** Match ends in %d turns — lowest hand value wins!\n\n%s",
|
||||||
|
remaining, pickCommentary("sudden_death")))
|
||||||
|
|
||||||
|
for _, pl := range active {
|
||||||
|
if !pl.isBot {
|
||||||
|
p.SendMessage(pl.dmRoomID, fmt.Sprintf(
|
||||||
|
"⏰ **Sudden Death!** %d turns remaining — dump your high-value cards!", remaining))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown reminders
|
||||||
|
if game.suddenDeath {
|
||||||
|
remaining := game.suddenDeathTurn - game.turns
|
||||||
|
switch remaining {
|
||||||
|
case 10:
|
||||||
|
p.SendMessage(game.roomID, "⏰ **10 turns remaining!**")
|
||||||
|
case 5:
|
||||||
|
p.SendMessage(game.roomID, "⏰ **5 turns remaining!**")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnoPlugin) multiSuddenDeathWinner(game *unoMultiGame, active []*unoMultiPlayer) {
|
||||||
|
if game.done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
game.done = true
|
||||||
|
|
||||||
|
if game.timer != nil {
|
||||||
|
game.timer.Stop()
|
||||||
|
}
|
||||||
|
if game.inactiveTimer != nil {
|
||||||
|
game.inactiveTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
a, b := active[0], active[1]
|
||||||
|
aScore := scoreHand(a.hand)
|
||||||
|
bScore := scoreHand(b.hand)
|
||||||
|
|
||||||
|
nameOf := func(pl *unoMultiPlayer) string {
|
||||||
|
if pl.isBot {
|
||||||
|
return unoBotName()
|
||||||
|
}
|
||||||
|
return p.DisplayName(pl.userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
breakdown := fmt.Sprintf("**Final Scores:**\n%s: %s\n%s: %s",
|
||||||
|
nameOf(a), formatHandScore(a.hand),
|
||||||
|
nameOf(b), formatHandScore(b.hand))
|
||||||
|
|
||||||
|
var winner, loser *unoMultiPlayer
|
||||||
|
switch {
|
||||||
|
case aScore < bScore:
|
||||||
|
winner, loser = a, b
|
||||||
|
case bScore < aScore:
|
||||||
|
winner, loser = b, a
|
||||||
|
case len(a.hand) < len(b.hand):
|
||||||
|
winner, loser = a, b
|
||||||
|
breakdown += "\n*Tiebreaker: fewer cards!*"
|
||||||
|
case len(b.hand) < len(a.hand):
|
||||||
|
winner, loser = b, a
|
||||||
|
breakdown += "\n*Tiebreaker: fewer cards!*"
|
||||||
|
default:
|
||||||
|
// Tiebreaker: player whose turn it is NOT wins
|
||||||
|
current := game.currentPlayer()
|
||||||
|
if current == a {
|
||||||
|
winner, loser = b, a
|
||||||
|
} else {
|
||||||
|
winner, loser = a, b
|
||||||
|
}
|
||||||
|
breakdown += "\n*Tiebreaker: dead even — advantage to the opponent!*"
|
||||||
|
}
|
||||||
|
_ = loser // used implicitly via winner != loser
|
||||||
|
|
||||||
|
p.SendMessage(game.roomID, fmt.Sprintf(
|
||||||
|
"⏰ **SUDDEN DEATH!** %s wins on points!\n%s\n\n%s",
|
||||||
|
nameOf(winner), breakdown, pickCommentary("sudden_death_win")))
|
||||||
|
|
||||||
|
if winner.isBot {
|
||||||
|
p.multiBotWins2(game)
|
||||||
|
} else {
|
||||||
|
p.multiPlayerWins2(game, winner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiPlayerWins2 / multiBotWins2 handle payout without duplicate done/timer logic
|
||||||
|
// (already handled by multiSuddenDeathWinner).
|
||||||
|
func (p *UnoPlugin) multiPlayerWins2(game *unoMultiGame, winner *unoMultiPlayer) {
|
||||||
|
humanCount := 0
|
||||||
|
for _, pl := range game.players {
|
||||||
|
if !pl.isBot {
|
||||||
|
humanCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalPot := game.ante * float64(humanCount)
|
||||||
|
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
|
||||||
|
|
||||||
|
p.recordMultiGame(game, winner.userID, "sudden_death_win")
|
||||||
|
p.cleanupMultiGame(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnoPlugin) multiBotWins2(game *unoMultiGame) {
|
||||||
|
humanCount := 0
|
||||||
|
for _, pl := range game.players {
|
||||||
|
if !pl.isBot {
|
||||||
|
humanCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalPot := game.ante * float64(humanCount)
|
||||||
|
p.addToPot(totalPot)
|
||||||
|
|
||||||
|
for _, pl := range game.players {
|
||||||
|
if !pl.isBot {
|
||||||
|
recordBotDefeat(pl.userID, "uno_multi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.recordMultiGame(game, id.UserID("bot"), "sudden_death_bot")
|
||||||
|
p.cleanupMultiGame(game)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Win / Forfeit / Cleanup
|
// Win / Forfeit / Cleanup
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -26,6 +26,57 @@ func cardDrawValue(v unoValue) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cardPointValue returns the point value of a card for sudden-death scoring.
|
||||||
|
func cardPointValue(c unoCard) int {
|
||||||
|
switch c.Value {
|
||||||
|
case unoZero:
|
||||||
|
return 0
|
||||||
|
case unoOne:
|
||||||
|
return 1
|
||||||
|
case unoTwo:
|
||||||
|
return 2
|
||||||
|
case unoThree:
|
||||||
|
return 3
|
||||||
|
case unoFour:
|
||||||
|
return 4
|
||||||
|
case unoFive:
|
||||||
|
return 5
|
||||||
|
case unoSix:
|
||||||
|
return 6
|
||||||
|
case unoSeven:
|
||||||
|
return 7
|
||||||
|
case unoEight:
|
||||||
|
return 8
|
||||||
|
case unoNine:
|
||||||
|
return 9
|
||||||
|
case unoSkip, unoReverse, unoDrawTwo:
|
||||||
|
return 20
|
||||||
|
case unoSkipEveryone, unoDrawFour, unoDiscardAll:
|
||||||
|
return 30
|
||||||
|
case unoWildCard, unoWildDrawFour:
|
||||||
|
return 50
|
||||||
|
case unoWildReverseDraw4, unoWildDrawSix, unoWildColorRoulette:
|
||||||
|
return 60
|
||||||
|
case unoWildDrawTen:
|
||||||
|
return 75
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scoreHand(hand []unoCard) int {
|
||||||
|
total := 0
|
||||||
|
for _, c := range hand {
|
||||||
|
total += cardPointValue(c)
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHandScore(hand []unoCard) string {
|
||||||
|
score := scoreHand(hand)
|
||||||
|
return fmt.Sprintf("%d cards, %d points", len(hand), score)
|
||||||
|
}
|
||||||
|
|
||||||
func isDrawCard(v unoValue) bool {
|
func isDrawCard(v unoValue) bool {
|
||||||
return cardDrawValue(v) > 0
|
return cardDrawValue(v) > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,3 +288,58 @@ func TestUnoColor_Emoji(t *testing.T) {
|
|||||||
t.Errorf("blue emoji: got %q", unoBlue.Emoji())
|
t.Errorf("blue emoji: got %q", unoBlue.Emoji())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Card point values (sudden death scoring)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestCardPointValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
card unoCard
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"zero", unoCard{unoRed, unoZero}, 0},
|
||||||
|
{"five", unoCard{unoBlue, unoFive}, 5},
|
||||||
|
{"nine", unoCard{unoGreen, unoNine}, 9},
|
||||||
|
{"skip", unoCard{unoRed, unoSkip}, 20},
|
||||||
|
{"reverse", unoCard{unoBlue, unoReverse}, 20},
|
||||||
|
{"draw two", unoCard{unoYellow, unoDrawTwo}, 20},
|
||||||
|
{"skip everyone", unoCard{unoRed, unoSkipEveryone}, 30},
|
||||||
|
{"draw four colored", unoCard{unoGreen, unoDrawFour}, 30},
|
||||||
|
{"discard all", unoCard{unoBlue, unoDiscardAll}, 30},
|
||||||
|
{"wild", unoCard{unoWild, unoWildCard}, 50},
|
||||||
|
{"wild draw four", unoCard{unoWild, unoWildDrawFour}, 50},
|
||||||
|
{"wild reverse draw 4", unoCard{unoWild, unoWildReverseDraw4}, 60},
|
||||||
|
{"wild draw six", unoCard{unoWild, unoWildDrawSix}, 60},
|
||||||
|
{"wild color roulette", unoCard{unoWild, unoWildColorRoulette}, 60},
|
||||||
|
{"wild draw ten", unoCard{unoWild, unoWildDrawTen}, 75},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := cardPointValue(tt.card); got != tt.want {
|
||||||
|
t.Errorf("cardPointValue(%v) = %d, want %d", tt.card, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoreHand(t *testing.T) {
|
||||||
|
hand := []unoCard{
|
||||||
|
{unoRed, unoFive}, // 5
|
||||||
|
{unoBlue, unoNine}, // 9
|
||||||
|
{unoGreen, unoSkip}, // 20
|
||||||
|
{unoWild, unoWildCard}, // 50
|
||||||
|
}
|
||||||
|
got := scoreHand(hand)
|
||||||
|
want := 84
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("scoreHand() = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoreHandEmpty(t *testing.T) {
|
||||||
|
if got := scoreHand(nil); got != 0 {
|
||||||
|
t.Errorf("scoreHand(nil) = %d, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user