Add UNO addbot, fix audit issues, add fishing to superstats, update README

UNO addbot:
- !uno addbot / !uno removebot for lobby bot management (WinBee, GwinBee)
- Bots ante from community pot, increasing the total pot
- Single human can start a game if bots fill remaining slots
- Per-bot display names throughout (replaces global unoBotName)

Audit fixes:
- Lobby display now always shows both humans and bots (was omitting bots on join)
- Slot check accounts for bots (was only counting humans)
- Color Roulette bot victim name fixed (was showing acting bot's name)
- Lobby display built under lock to prevent stale reads
- Bot name list separated from env var to prevent duplicates
- Extracted shared buildLobbyDisplay helper
- Inlined unnecessary nameOf closure in sudden death

Other:
- T5 masterwork drop rate reduced from 1.5% to 0.5%
- Added fishing stats to !superstatsexplusalpha
- Updated README with all new features

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-03-29 20:51:28 -07:00
parent 0a1359de46
commit 06153880e0
6 changed files with 232 additions and 195 deletions

View File

@@ -29,6 +29,7 @@ const (
type unoMultiPlayer struct {
userID id.UserID
displayName string // set at creation; bots use their assigned name
dmRoomID id.RoomID
hand []unoCard
calledUno bool
@@ -67,6 +68,9 @@ type unoMultiGame struct {
stackTotal int // cumulative draw penalty during stacking
stackMinValue int // minimum draw value to stack (0 = not stacking)
// Antes paid by addbot bots (funded from community pot)
botAntes int
// Sudden death (long-game point scoring)
suddenDeath bool
suddenDeathTurn int
@@ -81,16 +85,66 @@ type unoMultiLobby struct {
creator id.UserID
ante float64
players []id.UserID
botNames []string // bots added via !uno addbot
createdAt time.Time
timer *time.Timer
noMercy bool
sevenZeroRule bool
}
// unoExtraBotNames are the names available for !uno addbot (in order).
// The default bot name comes from unoBotName() and is always present.
var unoExtraBotNames = []string{"WinBee", "GwinBee"}
// lobbySlots returns the number of lobby slots taken (humans + addbot bots).
func (l *unoMultiLobby) lobbySlots() int {
return len(l.players) + len(l.botNames)
}
// buildLobbyDisplay renders the lobby message. Caller should hold p.mu or
// have a stable snapshot of the lobby fields.
func (p *UnoPlugin) buildLobbyDisplay(lobby *unoMultiLobby) string {
var sb strings.Builder
modeTag := ""
if lobby.noMercy {
modeTag = " 🔥 NO MERCY"
if lobby.sevenZeroRule {
modeTag += " (7-0)"
}
}
slots := lobby.lobbySlots()
sb.WriteString(fmt.Sprintf("🃏 **UNO Lobby**%s — Ante: €%d\nPlayers (%d/4):\n", modeTag, int(lobby.ante), slots))
idx := 1
for _, uid := range lobby.players {
name := p.DisplayName(uid)
label := ""
if uid == lobby.creator {
label = " (host)"
}
sb.WriteString(fmt.Sprintf(" %d. %s%s\n", idx, name, label))
idx++
}
for _, bn := range lobby.botNames {
sb.WriteString(fmt.Sprintf(" %d. 🤖 %s\n", idx, bn))
idx++
}
sb.WriteString("\nType `!uno join` to join, `!uno addbot` for a bot, or `!uno go` to start!")
return sb.String()
}
// ---------------------------------------------------------------------------
// Game helpers
// ---------------------------------------------------------------------------
// multiName returns the display name for a multiplayer UNO player.
// Bots use their assigned displayName; humans use the Matrix display name.
func (p *UnoPlugin) multiName(pl *unoMultiPlayer) string {
if pl.isBot {
return pl.displayName
}
return p.DisplayName(pl.userID)
}
func (g *unoMultiGame) currentPlayer() *unoMultiPlayer {
return g.players[g.currentIdx]
}
@@ -281,19 +335,10 @@ func (p *UnoPlugin) handleMultiStart(ctx MessageContext, amountStr string, noMer
})
p.lobbies[ctx.RoomID] = lobby
msg := p.buildLobbyDisplay(lobby)
p.mu.Unlock()
creatorName := p.DisplayName(ctx.Sender)
modeTag := ""
if noMercy {
modeTag = " 🔥 NO MERCY"
if sevenZeroRule {
modeTag += " (7-0)"
}
}
return p.SendMessage(ctx.RoomID, fmt.Sprintf(
"🃏 **UNO Lobby**%s — Ante: €%d\nPlayers (1/4):\n 1. %s (host)\n\nType `!uno join` to join or `!uno go` to start!",
modeTag, int(amount), creatorName))
return p.SendMessage(ctx.RoomID, msg)
}
func (p *UnoPlugin) handleMultiJoin(ctx MessageContext) error {
@@ -316,9 +361,9 @@ func (p *UnoPlugin) handleMultiJoin(ctx MessageContext) error {
}
}
if len(lobby.players) >= 4 {
if lobby.lobbySlots() >= 4 {
p.mu.Unlock()
return p.SendReply(ctx.RoomID, ctx.EventID, "Lobby is full! (4/4 players)")
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Lobby is full! (%d/4)", lobby.lobbySlots()))
}
// Check player isn't in another game
@@ -344,30 +389,10 @@ func (p *UnoPlugin) handleMultiJoin(ctx MessageContext) error {
}
lobby.players = append(lobby.players, ctx.Sender)
count := len(lobby.players)
msg := p.buildLobbyDisplay(lobby)
p.mu.Unlock()
// Build player list
var sb strings.Builder
lobbyModeTag := ""
if lobby.noMercy {
lobbyModeTag = " 🔥 NO MERCY"
if lobby.sevenZeroRule {
lobbyModeTag += " (7-0)"
}
}
sb.WriteString(fmt.Sprintf("🃏 **UNO Lobby**%s — Ante: €%d\nPlayers (%d/4):\n", lobbyModeTag, int(lobby.ante), count))
for i, uid := range lobby.players {
name := p.DisplayName(uid)
label := ""
if uid == lobby.creator {
label = " (host)"
}
sb.WriteString(fmt.Sprintf(" %d. %s%s\n", i+1, name, label))
}
sb.WriteString("\nType `!uno join` to join or `!uno go` to start!")
return p.SendMessage(ctx.RoomID, sb.String())
return p.SendMessage(ctx.RoomID, msg)
}
func (p *UnoPlugin) handleMultiLeave(ctx MessageContext) error {
@@ -408,7 +433,7 @@ func (p *UnoPlugin) handleMultiLeave(ctx MessageContext) error {
// Refund the leaving player
p.euro.Credit(ctx.Sender, lobby.ante, "uno_multi_refund")
name := p.DisplayName(ctx.Sender)
return p.SendMessage(ctx.RoomID, fmt.Sprintf("🃏 **%s** left the lobby. (%d/4 players)", name, len(lobby.players)))
return p.SendMessage(ctx.RoomID, fmt.Sprintf("🃏 **%s** left the lobby. (%d/4)", name, lobby.lobbySlots()))
}
func (p *UnoPlugin) handleMultiCancel(ctx MessageContext) error {
@@ -437,6 +462,63 @@ func (p *UnoPlugin) handleMultiCancel(ctx MessageContext) error {
return p.SendMessage(ctx.RoomID, "🃏 Lobby cancelled — all antes refunded.")
}
func (p *UnoPlugin) handleMultiAddBot(ctx MessageContext) error {
if !isGamesRoom(ctx.RoomID) {
return p.SendReply(ctx.RoomID, ctx.EventID, "Use this command in the games channel!")
}
p.mu.Lock()
lobby, exists := p.lobbies[ctx.RoomID]
if !exists {
p.mu.Unlock()
return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open. Start one with `!uno start €amount`.")
}
if lobby.lobbySlots() >= 4 {
p.mu.Unlock()
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Lobby is full! (%d/4)", lobby.lobbySlots()))
}
botIdx := len(lobby.botNames)
if botIdx >= len(unoExtraBotNames) {
p.mu.Unlock()
return p.SendReply(ctx.RoomID, ctx.EventID,
fmt.Sprintf("Maximum %d extra bots already added.", len(unoExtraBotNames)))
}
lobby.botNames = append(lobby.botNames, unoExtraBotNames[botIdx])
msg := p.buildLobbyDisplay(lobby)
p.mu.Unlock()
return p.SendMessage(ctx.RoomID, msg)
}
func (p *UnoPlugin) handleMultiRemoveBot(ctx MessageContext) error {
if !isGamesRoom(ctx.RoomID) {
return p.SendReply(ctx.RoomID, ctx.EventID, "Use this command in the games channel!")
}
p.mu.Lock()
lobby, exists := p.lobbies[ctx.RoomID]
if !exists {
p.mu.Unlock()
return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open.")
}
if len(lobby.botNames) == 0 {
p.mu.Unlock()
return p.SendReply(ctx.RoomID, ctx.EventID, "No bots to remove.")
}
removed := lobby.botNames[len(lobby.botNames)-1]
lobby.botNames = lobby.botNames[:len(lobby.botNames)-1]
msg := p.buildLobbyDisplay(lobby)
p.mu.Unlock()
p.SendMessage(ctx.RoomID, fmt.Sprintf("🤖 %s removed from the lobby.", removed))
return p.SendMessage(ctx.RoomID, msg)
}
func (p *UnoPlugin) lobbyExpired(roomID id.RoomID) {
p.mu.Lock()
lobby, exists := p.lobbies[roomID]
@@ -469,13 +551,16 @@ func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
return p.SendReply(ctx.RoomID, ctx.EventID, "Only the host can start the game.")
}
if len(lobby.players) < 2 {
// Need at least 2 total participants (humans + lobby bots + default bot)
totalParticipants := len(lobby.players) + len(lobby.botNames) + 1 // +1 for default bot
if totalParticipants < 2 {
p.mu.Unlock()
return p.SendReply(ctx.RoomID, ctx.EventID, "Need at least 2 players to start.")
}
lobby.timer.Stop()
players := lobby.players
lobbyBots := lobby.botNames
ante := lobby.ante
roomID := lobby.roomID
noMercy := lobby.noMercy
@@ -498,8 +583,14 @@ func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
resolved = append(resolved, playerDMPair{uid, dmRoom})
}
// Debit bot antes from community pot
for range lobbyBots {
p.claimFromPot(ante)
}
// Build game
game := p.initMultiGame(resolved, roomID, ante, noMercy, sevenZeroRule)
game := p.initMultiGame(resolved, lobbyBots, roomID, ante, noMercy, sevenZeroRule)
game.botAntes = len(lobbyBots)
p.mu.Lock()
p.multiGames[game.id] = game
@@ -512,7 +603,6 @@ func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
// Announce
var sb strings.Builder
bn := unoBotName()
modeTag := ""
if noMercy {
modeTag = " 🔥 NO MERCY"
@@ -520,12 +610,10 @@ func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
modeTag += " (7-0)"
}
}
sb.WriteString(fmt.Sprintf("🃏 **Multiplayer UNO!**%s Pot: €%d\n\nPlayers:\n", modeTag, int(ante)*len(players)))
totalPot := int(ante) * (len(players) + len(lobbyBots))
sb.WriteString(fmt.Sprintf("🃏 **Multiplayer UNO!**%s Pot: €%d\n\nPlayers:\n", modeTag, totalPot))
for i, pl := range game.players {
name := p.DisplayName(pl.userID)
if pl.isBot {
name = bn
}
name := p.multiName(pl)
marker := ""
if i == game.currentIdx {
marker = " ← first turn"
@@ -558,7 +646,7 @@ type playerDMPair struct {
dmRoomID id.RoomID
}
func (p *UnoPlugin) initMultiGame(players []playerDMPair, roomID id.RoomID, ante float64, noMercy, sevenZeroRule bool) *unoMultiGame {
func (p *UnoPlugin) initMultiGame(players []playerDMPair, lobbyBots []string, roomID id.RoomID, ante float64, noMercy, sevenZeroRule bool) *unoMultiGame {
var deck []unoCard
if noMercy {
deck = newNoMercyDeck()
@@ -575,23 +663,33 @@ func (p *UnoPlugin) initMultiGame(players []playerDMPair, roomID id.RoomID, ante
copy(hand, deck[cardIdx:cardIdx+cardsPerPlayer])
cardIdx += cardsPerPlayer
unshuffled = append(unshuffled, &unoMultiPlayer{
userID: pd.userID,
dmRoomID: pd.dmRoomID,
hand: hand,
active: true,
userID: pd.userID,
displayName: p.DisplayName(pd.userID),
dmRoomID: pd.dmRoomID,
hand: hand,
active: true,
})
}
// Bot
botHand := make([]unoCard, cardsPerPlayer)
copy(botHand, deck[cardIdx:cardIdx+cardsPerPlayer])
cardIdx += cardsPerPlayer
bot := &unoMultiPlayer{
userID: id.UserID(unoBotName()),
hand: botHand,
isBot: true,
active: true,
// Default bot (always present)
addBot := func(name string) {
botHand := make([]unoCard, cardsPerPlayer)
copy(botHand, deck[cardIdx:cardIdx+cardsPerPlayer])
cardIdx += cardsPerPlayer
unshuffled = append(unshuffled, &unoMultiPlayer{
userID: id.UserID(name),
displayName: name,
hand: botHand,
isBot: true,
active: true,
})
}
addBot(unoBotName())
// Additional bots from lobby
for _, bn := range lobbyBots {
addBot(bn)
}
unshuffled = append(unshuffled, bot)
// Shuffle turn order
rand.Shuffle(len(unshuffled), func(i, j int) { unshuffled[i], unshuffled[j] = unshuffled[j], unshuffled[i] })
@@ -856,12 +954,7 @@ func (p *UnoPlugin) advanceAndExecute(game *unoMultiGame) {
// nextTurnLabel returns "It's X's turn." for the current player.
func (p *UnoPlugin) nextTurnLabel(game *unoMultiGame) string {
next := game.currentPlayer()
name := p.DisplayName(next.userID)
if next.isBot {
name = unoBotName()
}
return fmt.Sprintf("It's %s's turn.", name)
return fmt.Sprintf("It's %s's turn.", p.multiName(game.currentPlayer()))
}
// ---------------------------------------------------------------------------
@@ -1087,10 +1180,7 @@ func (p *UnoPlugin) handleMultiPlayerPlay(game *unoMultiGame, player *unoMultiPl
if pl == player || !pl.active {
continue
}
name := p.DisplayName(pl.userID)
if pl.isBot {
name = unoBotName()
}
name := p.multiName(pl)
sb.WriteString(fmt.Sprintf("%d. %s (%d cards)\n", i, name, len(pl.hand)))
i++
}
@@ -1104,10 +1194,7 @@ func (p *UnoPlugin) handleMultiPlayerPlay(game *unoMultiGame, player *unoMultiPl
if pl != player && pl.active {
swapHandsMulti(player, pl)
name := p.DisplayName(player.userID)
otherName := p.DisplayName(pl.userID)
if pl.isBot {
otherName = unoBotName()
}
otherName := p.multiName(pl)
p.SendMessage(game.roomID, fmt.Sprintf("🔄 %s swaps hands with %s! %s",
name, otherName, pickNoMercyCommentary("hand_swap")))
if !pl.isBot {
@@ -1179,10 +1266,7 @@ func (p *UnoPlugin) handleMultiColorChoice(game *unoMultiGame, player *unoMultiP
name := p.DisplayName(player.userID)
nextIdx := game.nextActiveIdx()
target := game.players[nextIdx]
targetName := p.DisplayName(target.userID)
if target.isBot {
targetName = unoBotName()
}
targetName := p.multiName(target)
flipped := p.executeColorRouletteMulti(game, target, color)
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s plays: %s (chose %s %s)\n🎰 Color Roulette! %s flips %d cards until finding %s.\n%s",
@@ -1377,10 +1461,7 @@ func (p *UnoPlugin) applyCardEffects(game *unoMultiGame, card unoCard) cardEffec
nextIdx := game.nextActiveIdx()
nextPlayer := game.players[nextIdx]
nextName := p.DisplayName(nextPlayer.userID)
if nextPlayer.isBot {
nextName = unoBotName()
}
nextName := p.multiName(nextPlayer)
switch card.Value {
case unoSkip:
@@ -1395,10 +1476,7 @@ func (p *UnoPlugin) applyCardEffects(game *unoMultiGame, card unoCard) cardEffec
var skippedNames []string
for _, pl := range game.players {
if pl != game.currentPlayer() && pl.active {
name := p.DisplayName(pl.userID)
if pl.isBot {
name = unoBotName()
}
name := p.multiName(pl)
skippedNames = append(skippedNames, name)
}
}
@@ -1473,10 +1551,7 @@ func (p *UnoPlugin) applyCardEffects(game *unoMultiGame, card unoCard) cardEffec
// After reverse, get next player in new direction
nextIdx = game.nextActiveIdx()
nextPlayer = game.players[nextIdx]
nextName = p.DisplayName(nextPlayer.userID)
if nextPlayer.isBot {
nextName = unoBotName()
}
nextName = p.multiName(nextPlayer)
result.skippedName = nextName
result.drawnCount = game.stackTotal
game.currentIdx = game.nextActiveIdx()
@@ -1566,11 +1641,7 @@ func (p *UnoPlugin) applyAndAnnounce(game *unoMultiGame, player *unoMultiPlayer,
}
// Next player
nextUp := game.currentPlayer()
nextUpName := p.DisplayName(nextUp.userID)
if nextUp.isBot {
nextUpName = unoBotName()
}
nextUpName := p.multiName(game.currentPlayer())
roomMsg.WriteString(fmt.Sprintf("\n It's %s's turn.", nextUpName))
p.SendMessage(game.roomID, roomMsg.String())
@@ -1596,10 +1667,7 @@ func (p *UnoPlugin) startWD4Challenge(game *unoMultiGame, wd4Player *unoMultiPla
game.currentIdx = nextIdx
game.turnID++
playerName := p.DisplayName(wd4Player.userID)
if wd4Player.isBot {
playerName = unoBotName()
}
playerName := p.multiName(wd4Player)
if victim.isBot {
// Bot decides whether to challenge
@@ -1617,14 +1685,8 @@ func (p *UnoPlugin) startWD4Challenge(game *unoMultiGame, wd4Player *unoMultiPla
func (p *UnoPlugin) resolveWD4Challenge(game *unoMultiGame, challenged bool) {
wd4Player := game.wd4Player
victim := game.wd4Victim
wd4Name := p.DisplayName(wd4Player.userID)
if wd4Player.isBot {
wd4Name = unoBotName()
}
victimName := p.DisplayName(victim.userID)
if victim.isBot {
victimName = unoBotName()
}
wd4Name := p.multiName(wd4Player)
victimName := p.multiName(victim)
// Clear challenge state
game.wd4Player = nil
@@ -1699,7 +1761,7 @@ func (p *UnoPlugin) botHandleWD4Challenge(game *unoMultiGame) {
threshold = 30
}
challenged := rand.IntN(100) < threshold
bn := unoBotName()
bn := game.currentPlayer().displayName
if challenged {
p.SendMessage(game.roomID, fmt.Sprintf("⚡ %s challenges the Wild Draw Four!", bn))
@@ -1716,7 +1778,7 @@ func (p *UnoPlugin) botHandleWD4Challenge(game *unoMultiGame) {
func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
bot := game.currentPlayer()
bn := unoBotName()
bn := bot.displayName
// No Mercy stacking: bot must stack or absorb
if game.noMercy && game.stackMinValue > 0 {
@@ -1931,7 +1993,7 @@ func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
p.SendMessage(game.roomID, roomBuf.String())
roomBuf.Reset()
}
p.multiBotWins(game)
p.multiBotWins(game, bot)
return
}
@@ -1976,10 +2038,7 @@ func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
// applyCardEffects already advanced currentIdx to the victim
if game.noMercy && card.Value == unoWildColorRoulette {
target := game.players[game.currentIdx]
targetName := p.DisplayName(target.userID)
if target.isBot {
targetName = bn
}
targetName := p.multiName(target)
flipped := p.executeColorRouletteMulti(game, target, game.topColor)
roomBuf.WriteString(fmt.Sprintf("\n 🎰 Color Roulette! %s flips %d cards until finding %s %s.",
targetName, len(flipped), game.topColor.Emoji(), game.topColor))
@@ -2013,11 +2072,7 @@ 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()
}
nextUpName := p.multiName(game.currentPlayer())
roomBuf.WriteString(fmt.Sprintf("\n It's %s's turn.", nextUpName))
game.turns++
@@ -2151,10 +2206,7 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
if game.noMercy && pendingCard != nil && pendingCard.Value == unoWildColorRoulette {
nextIdx := game.nextActiveIdx()
target := game.players[nextIdx]
targetName := p.DisplayName(target.userID)
if target.isBot {
targetName = unoBotName()
}
targetName := p.multiName(target)
flipped := p.executeColorRouletteMulti(game, target, color)
p.SendMessage(game.roomID, fmt.Sprintf("🎰 Color Roulette! %s flips %d cards until finding %s.\n%s",
targetName, len(flipped), color, pickNoMercyCommentary("color_roulette")))
@@ -2234,10 +2286,7 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
target := botChooseSwapTarget(game, player)
if target != nil {
swapHandsMulti(player, target)
targetName := p.DisplayName(target.userID)
if target.isBot {
targetName = unoBotName()
}
targetName := p.multiName(target)
p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Swapped hands with %s.", targetName))
p.SendMessage(game.roomID, fmt.Sprintf("🔄 *%s was auto-played.* Swaps hands with %s!", name, targetName))
}
@@ -2466,16 +2515,9 @@ func (p *UnoPlugin) multiSuddenDeathWinner(game *unoMultiGame, active []*unoMult
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))
p.multiName(a), formatHandScore(a.hand),
p.multiName(b), formatHandScore(b.hand))
var winner, loser *unoMultiPlayer
switch {
@@ -2503,7 +2545,7 @@ func (p *UnoPlugin) multiSuddenDeathWinner(game *unoMultiGame, active []*unoMult
p.SendMessage(game.roomID, fmt.Sprintf(
"⏰ **SUDDEN DEATH!** %s wins on points!\n%s\n\n%s",
nameOf(winner), breakdown, pickCommentary("sudden_death_win")))
p.multiName(winner), breakdown, pickCommentary("sudden_death_win")))
if winner.isBot {
p.multiBotWins2(game)
@@ -2521,7 +2563,7 @@ func (p *UnoPlugin) multiPlayerWins2(game *unoMultiGame, winner *unoMultiPlayer)
humanCount++
}
}
totalPot := game.ante * float64(humanCount)
totalPot := game.ante * float64(humanCount+game.botAntes)
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
p.recordMultiGame(game, winner.userID, "sudden_death_win")
@@ -2535,7 +2577,7 @@ func (p *UnoPlugin) multiBotWins2(game *unoMultiGame) {
humanCount++
}
}
totalPot := game.ante * float64(humanCount)
totalPot := game.ante * float64(humanCount+game.botAntes)
p.addToPot(totalPot)
for _, pl := range game.players {
@@ -2571,7 +2613,7 @@ func (p *UnoPlugin) multiPlayerWins(game *unoMultiGame, winner *unoMultiPlayer)
humanCount++
}
}
totalPot := game.ante * float64(humanCount)
totalPot := game.ante * float64(humanCount+game.botAntes)
name := p.DisplayName(winner.userID)
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
@@ -2584,7 +2626,7 @@ func (p *UnoPlugin) multiPlayerWins(game *unoMultiGame, winner *unoMultiPlayer)
p.cleanupMultiGame(game)
}
func (p *UnoPlugin) multiBotWins(game *unoMultiGame) {
func (p *UnoPlugin) multiBotWins(game *unoMultiGame, winner *unoMultiPlayer) {
if game.done {
return
}
@@ -2603,12 +2645,12 @@ func (p *UnoPlugin) multiBotWins(game *unoMultiGame) {
humanCount++
}
}
totalPot := game.ante * float64(humanCount)
totalPot := game.ante * float64(humanCount+game.botAntes)
// Pot goes to community
p.addToPot(totalPot)
bn := unoBotName()
bn := winner.displayName
p.SendMessage(game.roomID, fmt.Sprintf(
"💀 **%s wins Multiplayer UNO!**\n€%d goes to the community pot.\n\n%s",
bn, int(totalPot), pickCommentary("bot_win")))
@@ -2638,7 +2680,7 @@ func (p *UnoPlugin) multiPlayerForfeit(game *unoMultiGame, player *unoMultiPlaye
winner := active[0]
if winner.isBot {
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. All humans out!", name))
p.multiBotWins(game)
p.multiBotWins(game, winner)
} else {
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. Only one player remains!", name))
p.multiPlayerWins(game, winner)
@@ -2682,7 +2724,7 @@ func (p *UnoPlugin) recordMultiGame(game *unoMultiGame, winnerID id.UserID, resu
}
humanCount := len(playerIDs)
totalPot := game.ante * float64(humanCount)
totalPot := game.ante * float64(humanCount+game.botAntes)
db.Exec("uno_multi: record game",
`INSERT INTO uno_multi_games (room_id, ante, pot_total, winner_id, player_ids, result, turns, started_at, ended_at)
@@ -2700,12 +2742,7 @@ func (p *UnoPlugin) recordMultiGame(game *unoMultiGame, winnerID id.UserID, resu
func (p *UnoPlugin) sendMultiStatus(game *unoMultiGame, player *unoMultiPlayer) {
var sb strings.Builder
bn := unoBotName()
current := game.currentPlayer()
currentName := p.DisplayName(current.userID)
if current.isBot {
currentName = bn
}
currentName := p.multiName(game.currentPlayer())
sb.WriteString(fmt.Sprintf("🃏 **UNO Status**\nDiscard pile: %s\nCurrent turn: %s\n\n",
game.discardTop.DisplayWithColor(game.topColor), currentName))
@@ -2725,11 +2762,7 @@ func (p *UnoPlugin) sendMultiStatus(game *unoMultiGame, player *unoMultiPlayer)
if pl == player || !pl.active {
continue
}
name := p.DisplayName(pl.userID)
if pl.isBot {
name = bn
}
counts = append(counts, fmt.Sprintf("%s (%d)", name, len(pl.hand)))
counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
}
sb.WriteString(strings.Join(counts, " | "))
@@ -2752,16 +2785,11 @@ func (p *UnoPlugin) sendMultiHandDisplay(game *unoMultiGame, player *unoMultiPla
// Card counts for all opponents
sb.WriteString("\nCard counts: ")
counts := make([]string, 0)
bn := unoBotName()
for _, pl := range game.players {
if pl == player || !pl.active {
continue
}
name := p.DisplayName(pl.userID)
if pl.isBot {
name = bn
}
counts = append(counts, fmt.Sprintf("%s (%d)", name, len(pl.hand)))
counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
}
sb.WriteString(strings.Join(counts, " | "))
@@ -2790,16 +2818,11 @@ func (p *UnoPlugin) sendMultiHandDisplayStacking(game *unoMultiGame, player *uno
sb.WriteString("\nCard counts: ")
counts := make([]string, 0)
bn := unoBotName()
for _, pl := range game.players {
if pl == player || !pl.active {
continue
}
name := p.DisplayName(pl.userID)
if pl.isBot {
name = bn
}
counts = append(counts, fmt.Sprintf("%s (%d)", name, len(pl.hand)))
counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
}
sb.WriteString(strings.Join(counts, " | "))
@@ -2840,10 +2863,7 @@ func (p *UnoPlugin) handleMultiSwapChoice(game *unoMultiGame, player *unoMultiPl
game.phase = unoMultiPhasePlay
name := p.DisplayName(player.userID)
targetName := p.DisplayName(target.userID)
if target.isBot {
targetName = unoBotName()
}
targetName := p.multiName(target)
p.SendMessage(player.dmRoomID, fmt.Sprintf("Swapped hands with %s! You now have %d cards.", targetName, len(player.hand)))
if !target.isBot {