mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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:
32
README.md
32
README.md
@@ -336,7 +336,7 @@ Rep is earned when someone thanks you. The bot detects this automatically.
|
||||
|
||||
### UNO (games channel only)
|
||||
|
||||
UNO can be played solo (vs GogoBee) or multiplayer (2-4 players + bot). All gameplay happens in DMs. The games channel is used for lobby management and public announcements.
|
||||
UNO can be played solo (vs GogoBee) or multiplayer (1-4 players + bots). All gameplay happens in DMs. The games channel is used for lobby management and public announcements.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
@@ -347,7 +347,9 @@ UNO can be played solo (vs GogoBee) or multiplayer (2-4 players + bot). All game
|
||||
| `!uno start nomercy €amount` | Multiplayer No Mercy lobby |
|
||||
| `!uno start nomercy 7-0 €amount` | Multiplayer No Mercy with 7-0 rule |
|
||||
| `!uno join` | Join an open lobby |
|
||||
| `!uno go` | Start the game (host only, 2+ players required) |
|
||||
| `!uno addbot` | Add a bot opponent to the lobby (max 2, funded from community pot) |
|
||||
| `!uno removebot` | Remove the last added bot from the lobby |
|
||||
| `!uno go` | Start the game (host only, 2+ participants required) |
|
||||
| `!uno leave` | Leave the lobby (refunds ante) |
|
||||
| `!uno cancel` | Cancel the lobby (host/admin, refunds all) |
|
||||
|
||||
@@ -385,6 +387,14 @@ Enabled by adding `7-0` to the command. When active:
|
||||
- **Play a 7** — swap hands with another player of your choice (in multiplayer, you pick the target)
|
||||
- **Play a 0** — all players pass their hand to the next player in play direction
|
||||
|
||||
#### Sudden Death
|
||||
|
||||
When a 2-player game (including bot matchups) exceeds 80 turns, a 20-turn countdown begins. At turn 100, both hands are scored by UNO card point values — lowest total wins. Tiebreaker: fewer cards, then advantage to the non-active player.
|
||||
|
||||
#### Multiplayer Bots
|
||||
|
||||
The default bot (GogoBee) is always present in multiplayer games. Up to 2 additional bots (WinBee, GwinBee) can be added via `!uno addbot` while the lobby is open. Addbot bots ante from the community pot, increasing the total pot. A single human can start a game if bots fill the remaining slots.
|
||||
|
||||
### Texas Hold'em (games channel only)
|
||||
|
||||
No-limit Texas Hold'em poker for 2-9 players. Buy-in is debited from your euro balance when you sit down; your remaining stack is cashed out when you leave. Private cards and coaching tips are delivered via DM — the room only sees start/end announcements.
|
||||
@@ -464,7 +474,7 @@ No economy integration — stats and leaderboard position are the reward.
|
||||
|
||||
### Adventure (DM-based idle RPG)
|
||||
|
||||
A daily DM-driven idle RPG where each player takes one action per day — dungeon, mine, forage, visit the shop, or rest. Outcomes resolve with flavor text and loot is credited to your euro balance. An evening summary posts to the games room. TwinBee is a permanent NPC adventurer who distributes rewards to active players.
|
||||
A daily DM-driven idle RPG where each player takes one action per day — dungeon, mine, fish, forage, visit the shop, or rest. Outcomes resolve with flavor text and loot is credited to your euro balance. An evening summary posts to the games room. TwinBee is a permanent NPC adventurer who distributes rewards to active players.
|
||||
|
||||
Characters auto-create on first `!adventure` command. All gameplay happens in DMs — reply to the bot's morning prompt with your choice. DM replies are only interpreted as adventure choices for 15 minutes after a menu is sent, so other DM-based games (UNO, Hold'em) won't conflict.
|
||||
|
||||
@@ -476,6 +486,7 @@ Characters auto-create on first `!adventure` command. All gameplay happens in DM
|
||||
| `!adventure buy <item>` | Buy equipment by name or tier shorthand (`3 sword`, `t4 boots`) |
|
||||
| `!adventure sell <item>` | Sell an inventory item (credits euro balance) |
|
||||
| `!adventure sell all` | Sell entire inventory |
|
||||
| `!adventure equip` | Equip masterwork gear from inventory |
|
||||
| `!adventure inventory` | List current inventory |
|
||||
| `!adventure leaderboard` | Top adventurers |
|
||||
| `!adventure revive @user` | Revive a dead player (admin) |
|
||||
@@ -486,27 +497,29 @@ Characters auto-create on first `!adventure` command. All gameplay happens in DM
|
||||
|
||||
#### Activities
|
||||
|
||||
Three activity types across 5 tiers of locations (15 total). Higher tiers require higher character level and equipment.
|
||||
Four activity types across 5 tiers of locations. Higher tiers require higher character level and equipment.
|
||||
|
||||
- **Dungeon** — combat XP, chance of death, best loot potential
|
||||
- **Mining** — mining XP, cave-in risk, ore and gem drops
|
||||
- **Fishing** — fishing XP, hazard risk, catch drops
|
||||
- **Foraging** — foraging XP, wildlife hazards, herb and reagent drops
|
||||
|
||||
#### Mechanics
|
||||
|
||||
- **Equipment** — 5 slots (weapon, armor, helmet, boots, tool) with tiered upgrades from the shop. Equipment degrades on bad outcomes and breaks at 0 condition.
|
||||
- **Masterwork gear** — rare skill-specific equipment drops from gathering activities (mining, fishing, foraging). 15 items across 5 tiers with decreasing drop rates (5% T1 down to 0.5% T5). Location-gated — drops only at matching tier. Auto-equips if better than current gear; otherwise goes to inventory for manual equipping via `!adventure equip`. Masterwork items provide a +5% skill bonus when the item's source skill matches the current activity. Character sheet marks masterwork items with a star.
|
||||
- **Treasures** — rare collectibles (up to 3) that provide passive bonuses (XP multipliers, death chance reduction, loot quality). Prompted to discard when at cap.
|
||||
- **Streaks** — consecutive days of activity grant escalating bonuses (XP, loot quality, death chance reduction). Resting or dying resets your streak.
|
||||
- **Grudge** — dying at a location marks it as your grudge. Returning there grants +10% success and +25% XP. Clears on success.
|
||||
- **Party bonus** — if two players independently visit the same location on the same day, both get +10% loot value.
|
||||
- **Death** — locked out until midnight UTC. You're automatically revived when the next day starts.
|
||||
- **Death** — locked out until midnight UTC. You're automatically revived when the next day starts. Death's Reprieve (surviving a lethal roll) sets all equipment to 1 condition instead of destroying it.
|
||||
- **Holidays** — on recognized holidays (~20/year across religious and cultural calendars), you get a second daily action. Hebrew and Islamic calendar support for floating holidays.
|
||||
- **TwinBee NPC** — takes a daily action (location tier capped by best player's combined level), distributes loot share to active players scaled quadratically by level, and occasionally gifts random buffs.
|
||||
- **Mid-day events** — random events can trigger between actions, delivering bonus loot, buffs, or narrative encounters.
|
||||
|
||||
#### Arena (`!arena`)
|
||||
|
||||
A multi-tier combat gauntlet independent of the daily adventure action. Fight through 5 tiers of 4 rounds each — 20 unique named monsters with escalating lethality. Earnings accumulate across rounds but are forfeited on death. After clearing a tier, choose to descend deeper (keep earnings at risk) or cash out. Death locks you out of both arena and adventure until midnight UTC.
|
||||
A multi-tier combat gauntlet independent of the daily adventure action. Fight through 5 tiers of 4 rounds each — 20 unique named monsters with escalating lethality. Earnings accumulate across rounds but are forfeited on death. After clearing a tier, choose to descend deeper (keep earnings at risk) or cash out. Death locks you out of both arena and adventure until midnight UTC. Each fight produces a turn-based combat log (Dragon Quest style) with fabricated HP pools and action narration — the outcome is determined by the roll, the log is assembled backward from the result. Arena losses award +60 participation XP.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
@@ -938,8 +951,9 @@ gogobee/
|
||||
│ │ ├── hangman.go # Collaborative Hangman
|
||||
│ │ ├── blackjack.go # Multiplayer Blackjack
|
||||
│ │ ├── uno.go # Solo UNO vs bot (DM-based)
|
||||
│ │ ├── uno_multi.go # Multiplayer UNO (lobby + DM turns)
|
||||
│ │ ├── uno_multi.go # Multiplayer UNO (lobby + DM turns + addbot)
|
||||
│ │ ├── uno_nomercy.go # No Mercy mode (deck, stacking, mercy rule, 7-0, bot AI)
|
||||
│ │ ├── uno_test.go # UNO tests (sudden death, scoring)
|
||||
│ │ ├── holdem.go # Texas Hold'em plugin (commands, game lifecycle, NPC)
|
||||
│ │ ├── holdem_game.go # Game state, player types, deck, position labels
|
||||
│ │ ├── holdem_betting.go # Blinds, action validation, side pots
|
||||
@@ -963,10 +977,12 @@ gogobee/
|
||||
│ │ ├── adventure_events.go # Mid-day random events
|
||||
│ │ ├── adventure_scheduler.go # Morning DM, evening summary, midnight reset tickers
|
||||
│ │ ├── adventure_holidays.go # Holiday detection (Hebrew/Islamic calendar, fixed dates)
|
||||
│ │ ├── adventure_masterwork.go # Masterwork gear (15 tiered items, drop logic, equip command)
|
||||
│ │ ├── adventure_arena.go # Arena commands, combat resolution, DB CRUD, auto-cashout
|
||||
│ │ ├── adventure_arena_combat.go # Arena combat log generator (action pools, HP narration)
|
||||
│ │ ├── adventure_arena_monsters.go # 5 tiers, 20 monsters, death messages
|
||||
│ │ ├── adventure_arena_render.go # Arena UI rendering (menus, stats, leaderboard)
|
||||
│ │ ├── adventure_flavor_*.go # Flavor text pools (dungeon, mining, treasure, twinbee, events, closing)
|
||||
│ │ ├── adventure_flavor_*.go # Flavor text pools (dungeon, mining, fishing, treasure, twinbee, events, closing)
|
||||
│ │ ├── esteemed.go # Satirical esteemed member posts
|
||||
│ │ ├── moderation.go # Moderation system (strikes, word list, flood detection)
|
||||
│ │ └── ratelimits.go # Rate limiting
|
||||
|
||||
@@ -19,7 +19,7 @@ type MasterworkDef struct {
|
||||
Tier int // 1-5, matches location tier
|
||||
Name string
|
||||
Description string // character sheet / trade listing
|
||||
DropRate float64 // per-tier: 0.05, 0.04, 0.03, 0.02, 0.015
|
||||
DropRate float64 // per-tier: 0.05, 0.04, 0.03, 0.02, 0.005
|
||||
}
|
||||
|
||||
var masterworkDefs = []MasterworkDef{
|
||||
@@ -46,7 +46,7 @@ var masterworkDefs = []MasterworkDef{
|
||||
},
|
||||
{
|
||||
Slot: SlotWeapon, Activity: AdvActivityMining, SkillSource: "mining", Tier: 5,
|
||||
Name: "The Motherload", DropRate: 0.015,
|
||||
Name: "The Motherload", DropRate: 0.005,
|
||||
Description: "There is no good explanation for why this was in the rock. The rock isn't talking. You're not asking. You've decided to stop thinking about it and just use it.",
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@ var masterworkDefs = []MasterworkDef{
|
||||
},
|
||||
{
|
||||
Slot: SlotArmor, Activity: AdvActivityFishing, SkillSource: "fishing", Tier: 5,
|
||||
Name: "The Deepforged Carapace", DropRate: 0.015,
|
||||
Name: "The Deepforged Carapace", DropRate: 0.005,
|
||||
Description: "This came up on the line from the Abyssal Trench and the line almost didn't hold. It is not from a fish. It is not from anything you have seen or want to see. It fits perfectly. You find this more unsettling than the alternative.",
|
||||
},
|
||||
|
||||
@@ -100,7 +100,7 @@ var masterworkDefs = []MasterworkDef{
|
||||
},
|
||||
{
|
||||
Slot: SlotBoots, Activity: AdvActivityForaging, SkillSource: "foraging", Tier: 5,
|
||||
Name: "The Last Step", DropRate: 0.015,
|
||||
Name: "The Last Step", DropRate: 0.005,
|
||||
Description: "Found at the edge of the Fungal Dark where the light stops. You almost didn't go that far. Something about wearing them suggests you will always go that far, from now on. This is either good news or the other kind.",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -399,15 +399,15 @@ func (p *StatsPlugin) handleSuperStats(ctx MessageContext) error {
|
||||
sb.WriteString("\n")
|
||||
|
||||
// ── Adventure ──
|
||||
var combatLv, miningLv, forageLv, combatXP, miningXP, forageXP int
|
||||
var combatLv, miningLv, fishingLv, forageLv, combatXP, miningXP, fishingXP, forageXP int
|
||||
var alive bool
|
||||
var streak, bestStreak int
|
||||
err = d.QueryRow(
|
||||
`SELECT combat_level, mining_skill, foraging_skill,
|
||||
combat_xp, mining_xp, foraging_xp,
|
||||
`SELECT combat_level, mining_skill, fishing_skill, foraging_skill,
|
||||
combat_xp, mining_xp, fishing_xp, foraging_xp,
|
||||
alive, current_streak, best_streak
|
||||
FROM adventure_characters WHERE user_id = ?`, uid,
|
||||
).Scan(&combatLv, &miningLv, &forageLv, &combatXP, &miningXP, &forageXP,
|
||||
).Scan(&combatLv, &miningLv, &fishingLv, &forageLv, &combatXP, &miningXP, &fishingXP, &forageXP,
|
||||
&alive, &streak, &bestStreak)
|
||||
if err == nil {
|
||||
status := "Alive"
|
||||
@@ -415,8 +415,8 @@ func (p *StatsPlugin) handleSuperStats(ctx MessageContext) error {
|
||||
status = "💀 Dead"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("⚔️ **Adventure:** %s\n", status))
|
||||
sb.WriteString(fmt.Sprintf(" Combat Lv.%d (%d XP) · Mining Lv.%d (%d XP) · Forage Lv.%d (%d XP)\n",
|
||||
combatLv, combatXP, miningLv, miningXP, forageLv, forageXP))
|
||||
sb.WriteString(fmt.Sprintf(" Combat Lv.%d (%d XP) · Mining Lv.%d (%d XP) · Fishing Lv.%d (%d XP) · Forage Lv.%d (%d XP)\n",
|
||||
combatLv, combatXP, miningLv, miningXP, fishingLv, fishingXP, forageLv, forageXP))
|
||||
if streak > 0 || bestStreak > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" 🔥 Streak: %d days (best: %d)\n", streak, bestStreak))
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ func (p *UnoPlugin) Name() string { return "uno" }
|
||||
|
||||
func (p *UnoPlugin) Commands() []CommandDef {
|
||||
return []CommandDef{
|
||||
{Name: "uno", Description: "Solo or multiplayer Uno", Usage: "!uno [nomercy [7-0]] €amount | !uno start [nomercy [7-0]] €amount | !uno join | !uno go", Category: "Games"},
|
||||
{Name: "uno", Description: "Solo or multiplayer Uno", Usage: "!uno [nomercy [7-0]] €amount | !uno start [nomercy [7-0]] €amount | !uno join | !uno addbot | !uno removebot | !uno go | !uno leave | !uno cancel", Category: "Games"},
|
||||
{Name: "uno_pot", Description: "Show the community pot balance", Usage: "!uno_pot", Category: "Games"},
|
||||
}
|
||||
}
|
||||
@@ -447,6 +447,10 @@ func (p *UnoPlugin) OnMessage(ctx MessageContext) error {
|
||||
return p.handleMultiLeave(ctx)
|
||||
case lower == "cancel":
|
||||
return p.handleMultiCancel(ctx)
|
||||
case lower == "addbot":
|
||||
return p.handleMultiAddBot(ctx)
|
||||
case lower == "removebot":
|
||||
return p.handleMultiRemoveBot(ctx)
|
||||
}
|
||||
|
||||
// Solo challenge: !uno [nomercy [7-0]] €amount
|
||||
|
||||
@@ -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()
|
||||
@@ -576,22 +664,32 @@ func (p *UnoPlugin) initMultiGame(players []playerDMPair, roomID id.RoomID, ante
|
||||
cardIdx += cardsPerPlayer
|
||||
unshuffled = append(unshuffled, &unoMultiPlayer{
|
||||
userID: pd.userID,
|
||||
displayName: p.DisplayName(pd.userID),
|
||||
dmRoomID: pd.dmRoomID,
|
||||
hand: hand,
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
// Bot
|
||||
|
||||
// Default bot (always present)
|
||||
addBot := func(name string) {
|
||||
botHand := make([]unoCard, cardsPerPlayer)
|
||||
copy(botHand, deck[cardIdx:cardIdx+cardsPerPlayer])
|
||||
cardIdx += cardsPerPlayer
|
||||
bot := &unoMultiPlayer{
|
||||
userID: id.UserID(unoBotName()),
|
||||
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 {
|
||||
|
||||
@@ -201,10 +201,7 @@ func (p *UnoPlugin) checkMultiMercyElimination(game *unoMultiGame, player *unoMu
|
||||
}
|
||||
|
||||
player.active = false
|
||||
name := p.DisplayName(player.userID)
|
||||
if player.isBot {
|
||||
name = unoBotName()
|
||||
}
|
||||
name := p.multiName(player)
|
||||
|
||||
// Shuffle cards back into draw pile
|
||||
game.drawPile = append(game.drawPile, player.hand...)
|
||||
@@ -226,7 +223,7 @@ func (p *UnoPlugin) checkMultiMercyElimination(game *unoMultiGame, player *unoMu
|
||||
if len(active) == 1 {
|
||||
winner := active[0]
|
||||
if winner.isBot {
|
||||
p.multiBotWins(game)
|
||||
p.multiBotWins(game, winner)
|
||||
} else {
|
||||
p.multiPlayerWins(game, winner)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user