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 (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 |
|
| 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 €amount` | Multiplayer No Mercy lobby |
|
||||||
| `!uno start nomercy 7-0 €amount` | Multiplayer No Mercy with 7-0 rule |
|
| `!uno start nomercy 7-0 €amount` | Multiplayer No Mercy with 7-0 rule |
|
||||||
| `!uno join` | Join an open lobby |
|
| `!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 leave` | Leave the lobby (refunds ante) |
|
||||||
| `!uno cancel` | Cancel the lobby (host/admin, refunds all) |
|
| `!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 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
|
- **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)
|
### 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.
|
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)
|
### 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.
|
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 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 <item>` | Sell an inventory item (credits euro balance) |
|
||||||
| `!adventure sell all` | Sell entire inventory |
|
| `!adventure sell all` | Sell entire inventory |
|
||||||
|
| `!adventure equip` | Equip masterwork gear from inventory |
|
||||||
| `!adventure inventory` | List current inventory |
|
| `!adventure inventory` | List current inventory |
|
||||||
| `!adventure leaderboard` | Top adventurers |
|
| `!adventure leaderboard` | Top adventurers |
|
||||||
| `!adventure revive @user` | Revive a dead player (admin) |
|
| `!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
|
#### 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
|
- **Dungeon** — combat XP, chance of death, best loot potential
|
||||||
- **Mining** — mining XP, cave-in risk, ore and gem drops
|
- **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
|
- **Foraging** — foraging XP, wildlife hazards, herb and reagent drops
|
||||||
|
|
||||||
#### Mechanics
|
#### 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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **Mid-day events** — random events can trigger between actions, delivering bonus loot, buffs, or narrative encounters.
|
||||||
|
|
||||||
#### Arena (`!arena`)
|
#### 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 |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
@@ -938,8 +951,9 @@ gogobee/
|
|||||||
│ │ ├── hangman.go # Collaborative Hangman
|
│ │ ├── hangman.go # Collaborative Hangman
|
||||||
│ │ ├── blackjack.go # Multiplayer Blackjack
|
│ │ ├── blackjack.go # Multiplayer Blackjack
|
||||||
│ │ ├── uno.go # Solo UNO vs bot (DM-based)
|
│ │ ├── 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_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.go # Texas Hold'em plugin (commands, game lifecycle, NPC)
|
||||||
│ │ ├── holdem_game.go # Game state, player types, deck, position labels
|
│ │ ├── holdem_game.go # Game state, player types, deck, position labels
|
||||||
│ │ ├── holdem_betting.go # Blinds, action validation, side pots
|
│ │ ├── holdem_betting.go # Blinds, action validation, side pots
|
||||||
@@ -963,10 +977,12 @@ gogobee/
|
|||||||
│ │ ├── adventure_events.go # Mid-day random events
|
│ │ ├── adventure_events.go # Mid-day random events
|
||||||
│ │ ├── adventure_scheduler.go # Morning DM, evening summary, midnight reset tickers
|
│ │ ├── adventure_scheduler.go # Morning DM, evening summary, midnight reset tickers
|
||||||
│ │ ├── adventure_holidays.go # Holiday detection (Hebrew/Islamic calendar, fixed dates)
|
│ │ ├── 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.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_monsters.go # 5 tiers, 20 monsters, death messages
|
||||||
│ │ ├── adventure_arena_render.go # Arena UI rendering (menus, stats, leaderboard)
|
│ │ ├── 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
|
│ │ ├── esteemed.go # Satirical esteemed member posts
|
||||||
│ │ ├── moderation.go # Moderation system (strikes, word list, flood detection)
|
│ │ ├── moderation.go # Moderation system (strikes, word list, flood detection)
|
||||||
│ │ └── ratelimits.go # Rate limiting
|
│ │ └── ratelimits.go # Rate limiting
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type MasterworkDef struct {
|
|||||||
Tier int // 1-5, matches location tier
|
Tier int // 1-5, matches location tier
|
||||||
Name string
|
Name string
|
||||||
Description string // character sheet / trade listing
|
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{
|
var masterworkDefs = []MasterworkDef{
|
||||||
@@ -46,7 +46,7 @@ var masterworkDefs = []MasterworkDef{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Slot: SlotWeapon, Activity: AdvActivityMining, SkillSource: "mining", Tier: 5,
|
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.",
|
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,
|
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.",
|
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,
|
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.",
|
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")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// ── Adventure ──
|
// ── Adventure ──
|
||||||
var combatLv, miningLv, forageLv, combatXP, miningXP, forageXP int
|
var combatLv, miningLv, fishingLv, forageLv, combatXP, miningXP, fishingXP, forageXP int
|
||||||
var alive bool
|
var alive bool
|
||||||
var streak, bestStreak int
|
var streak, bestStreak int
|
||||||
err = d.QueryRow(
|
err = d.QueryRow(
|
||||||
`SELECT combat_level, mining_skill, foraging_skill,
|
`SELECT combat_level, mining_skill, fishing_skill, foraging_skill,
|
||||||
combat_xp, mining_xp, foraging_xp,
|
combat_xp, mining_xp, fishing_xp, foraging_xp,
|
||||||
alive, current_streak, best_streak
|
alive, current_streak, best_streak
|
||||||
FROM adventure_characters WHERE user_id = ?`, uid,
|
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)
|
&alive, &streak, &bestStreak)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
status := "Alive"
|
status := "Alive"
|
||||||
@@ -415,8 +415,8 @@ func (p *StatsPlugin) handleSuperStats(ctx MessageContext) error {
|
|||||||
status = "💀 Dead"
|
status = "💀 Dead"
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("⚔️ **Adventure:** %s\n", status))
|
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",
|
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, forageLv, forageXP))
|
combatLv, combatXP, miningLv, miningXP, fishingLv, fishingXP, forageLv, forageXP))
|
||||||
if streak > 0 || bestStreak > 0 {
|
if streak > 0 || bestStreak > 0 {
|
||||||
sb.WriteString(fmt.Sprintf(" 🔥 Streak: %d days (best: %d)\n", streak, bestStreak))
|
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 {
|
func (p *UnoPlugin) Commands() []CommandDef {
|
||||||
return []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"},
|
{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)
|
return p.handleMultiLeave(ctx)
|
||||||
case lower == "cancel":
|
case lower == "cancel":
|
||||||
return p.handleMultiCancel(ctx)
|
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
|
// Solo challenge: !uno [nomercy [7-0]] €amount
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
|
|
||||||
type unoMultiPlayer struct {
|
type unoMultiPlayer struct {
|
||||||
userID id.UserID
|
userID id.UserID
|
||||||
|
displayName string // set at creation; bots use their assigned name
|
||||||
dmRoomID id.RoomID
|
dmRoomID id.RoomID
|
||||||
hand []unoCard
|
hand []unoCard
|
||||||
calledUno bool
|
calledUno bool
|
||||||
@@ -67,6 +68,9 @@ 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)
|
||||||
|
|
||||||
|
// Antes paid by addbot bots (funded from community pot)
|
||||||
|
botAntes int
|
||||||
|
|
||||||
// Sudden death (long-game point scoring)
|
// Sudden death (long-game point scoring)
|
||||||
suddenDeath bool
|
suddenDeath bool
|
||||||
suddenDeathTurn int
|
suddenDeathTurn int
|
||||||
@@ -81,16 +85,66 @@ type unoMultiLobby struct {
|
|||||||
creator id.UserID
|
creator id.UserID
|
||||||
ante float64
|
ante float64
|
||||||
players []id.UserID
|
players []id.UserID
|
||||||
|
botNames []string // bots added via !uno addbot
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
noMercy bool
|
noMercy bool
|
||||||
sevenZeroRule 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
|
// 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 {
|
func (g *unoMultiGame) currentPlayer() *unoMultiPlayer {
|
||||||
return g.players[g.currentIdx]
|
return g.players[g.currentIdx]
|
||||||
}
|
}
|
||||||
@@ -281,19 +335,10 @@ func (p *UnoPlugin) handleMultiStart(ctx MessageContext, amountStr string, noMer
|
|||||||
})
|
})
|
||||||
|
|
||||||
p.lobbies[ctx.RoomID] = lobby
|
p.lobbies[ctx.RoomID] = lobby
|
||||||
|
msg := p.buildLobbyDisplay(lobby)
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
creatorName := p.DisplayName(ctx.Sender)
|
return p.SendMessage(ctx.RoomID, msg)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UnoPlugin) handleMultiJoin(ctx MessageContext) error {
|
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()
|
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
|
// 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)
|
lobby.players = append(lobby.players, ctx.Sender)
|
||||||
count := len(lobby.players)
|
msg := p.buildLobbyDisplay(lobby)
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
// Build player list
|
return p.SendMessage(ctx.RoomID, msg)
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UnoPlugin) handleMultiLeave(ctx MessageContext) error {
|
func (p *UnoPlugin) handleMultiLeave(ctx MessageContext) error {
|
||||||
@@ -408,7 +433,7 @@ func (p *UnoPlugin) handleMultiLeave(ctx MessageContext) error {
|
|||||||
// Refund the leaving player
|
// Refund the leaving player
|
||||||
p.euro.Credit(ctx.Sender, lobby.ante, "uno_multi_refund")
|
p.euro.Credit(ctx.Sender, lobby.ante, "uno_multi_refund")
|
||||||
name := p.DisplayName(ctx.Sender)
|
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 {
|
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.")
|
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) {
|
func (p *UnoPlugin) lobbyExpired(roomID id.RoomID) {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
lobby, exists := p.lobbies[roomID]
|
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.")
|
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()
|
p.mu.Unlock()
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Need at least 2 players to start.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Need at least 2 players to start.")
|
||||||
}
|
}
|
||||||
|
|
||||||
lobby.timer.Stop()
|
lobby.timer.Stop()
|
||||||
players := lobby.players
|
players := lobby.players
|
||||||
|
lobbyBots := lobby.botNames
|
||||||
ante := lobby.ante
|
ante := lobby.ante
|
||||||
roomID := lobby.roomID
|
roomID := lobby.roomID
|
||||||
noMercy := lobby.noMercy
|
noMercy := lobby.noMercy
|
||||||
@@ -498,8 +583,14 @@ func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
|
|||||||
resolved = append(resolved, playerDMPair{uid, dmRoom})
|
resolved = append(resolved, playerDMPair{uid, dmRoom})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debit bot antes from community pot
|
||||||
|
for range lobbyBots {
|
||||||
|
p.claimFromPot(ante)
|
||||||
|
}
|
||||||
|
|
||||||
// Build game
|
// 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.mu.Lock()
|
||||||
p.multiGames[game.id] = game
|
p.multiGames[game.id] = game
|
||||||
@@ -512,7 +603,6 @@ func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
|
|||||||
|
|
||||||
// Announce
|
// Announce
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
bn := unoBotName()
|
|
||||||
modeTag := ""
|
modeTag := ""
|
||||||
if noMercy {
|
if noMercy {
|
||||||
modeTag = " 🔥 NO MERCY"
|
modeTag = " 🔥 NO MERCY"
|
||||||
@@ -520,12 +610,10 @@ func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
|
|||||||
modeTag += " (7-0)"
|
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 {
|
for i, pl := range game.players {
|
||||||
name := p.DisplayName(pl.userID)
|
name := p.multiName(pl)
|
||||||
if pl.isBot {
|
|
||||||
name = bn
|
|
||||||
}
|
|
||||||
marker := ""
|
marker := ""
|
||||||
if i == game.currentIdx {
|
if i == game.currentIdx {
|
||||||
marker = " ← first turn"
|
marker = " ← first turn"
|
||||||
@@ -558,7 +646,7 @@ type playerDMPair struct {
|
|||||||
dmRoomID id.RoomID
|
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
|
var deck []unoCard
|
||||||
if noMercy {
|
if noMercy {
|
||||||
deck = newNoMercyDeck()
|
deck = newNoMercyDeck()
|
||||||
@@ -575,23 +663,33 @@ func (p *UnoPlugin) initMultiGame(players []playerDMPair, roomID id.RoomID, ante
|
|||||||
copy(hand, deck[cardIdx:cardIdx+cardsPerPlayer])
|
copy(hand, deck[cardIdx:cardIdx+cardsPerPlayer])
|
||||||
cardIdx += cardsPerPlayer
|
cardIdx += cardsPerPlayer
|
||||||
unshuffled = append(unshuffled, &unoMultiPlayer{
|
unshuffled = append(unshuffled, &unoMultiPlayer{
|
||||||
userID: pd.userID,
|
userID: pd.userID,
|
||||||
dmRoomID: pd.dmRoomID,
|
displayName: p.DisplayName(pd.userID),
|
||||||
hand: hand,
|
dmRoomID: pd.dmRoomID,
|
||||||
active: true,
|
hand: hand,
|
||||||
|
active: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Bot
|
|
||||||
botHand := make([]unoCard, cardsPerPlayer)
|
// Default bot (always present)
|
||||||
copy(botHand, deck[cardIdx:cardIdx+cardsPerPlayer])
|
addBot := func(name string) {
|
||||||
cardIdx += cardsPerPlayer
|
botHand := make([]unoCard, cardsPerPlayer)
|
||||||
bot := &unoMultiPlayer{
|
copy(botHand, deck[cardIdx:cardIdx+cardsPerPlayer])
|
||||||
userID: id.UserID(unoBotName()),
|
cardIdx += cardsPerPlayer
|
||||||
hand: botHand,
|
unshuffled = append(unshuffled, &unoMultiPlayer{
|
||||||
isBot: true,
|
userID: id.UserID(name),
|
||||||
active: true,
|
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
|
// Shuffle turn order
|
||||||
rand.Shuffle(len(unshuffled), func(i, j int) { unshuffled[i], unshuffled[j] = unshuffled[j], unshuffled[i] })
|
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.
|
// nextTurnLabel returns "It's X's turn." for the current player.
|
||||||
func (p *UnoPlugin) nextTurnLabel(game *unoMultiGame) string {
|
func (p *UnoPlugin) nextTurnLabel(game *unoMultiGame) string {
|
||||||
next := game.currentPlayer()
|
return fmt.Sprintf("It's %s's turn.", p.multiName(game.currentPlayer()))
|
||||||
name := p.DisplayName(next.userID)
|
|
||||||
if next.isBot {
|
|
||||||
name = unoBotName()
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("It's %s's turn.", name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1087,10 +1180,7 @@ func (p *UnoPlugin) handleMultiPlayerPlay(game *unoMultiGame, player *unoMultiPl
|
|||||||
if pl == player || !pl.active {
|
if pl == player || !pl.active {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := p.DisplayName(pl.userID)
|
name := p.multiName(pl)
|
||||||
if pl.isBot {
|
|
||||||
name = unoBotName()
|
|
||||||
}
|
|
||||||
sb.WriteString(fmt.Sprintf("%d. %s (%d cards)\n", i, name, len(pl.hand)))
|
sb.WriteString(fmt.Sprintf("%d. %s (%d cards)\n", i, name, len(pl.hand)))
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
@@ -1104,10 +1194,7 @@ func (p *UnoPlugin) handleMultiPlayerPlay(game *unoMultiGame, player *unoMultiPl
|
|||||||
if pl != player && pl.active {
|
if pl != player && pl.active {
|
||||||
swapHandsMulti(player, pl)
|
swapHandsMulti(player, pl)
|
||||||
name := p.DisplayName(player.userID)
|
name := p.DisplayName(player.userID)
|
||||||
otherName := p.DisplayName(pl.userID)
|
otherName := p.multiName(pl)
|
||||||
if pl.isBot {
|
|
||||||
otherName = unoBotName()
|
|
||||||
}
|
|
||||||
p.SendMessage(game.roomID, fmt.Sprintf("🔄 %s swaps hands with %s! %s",
|
p.SendMessage(game.roomID, fmt.Sprintf("🔄 %s swaps hands with %s! %s",
|
||||||
name, otherName, pickNoMercyCommentary("hand_swap")))
|
name, otherName, pickNoMercyCommentary("hand_swap")))
|
||||||
if !pl.isBot {
|
if !pl.isBot {
|
||||||
@@ -1179,10 +1266,7 @@ func (p *UnoPlugin) handleMultiColorChoice(game *unoMultiGame, player *unoMultiP
|
|||||||
name := p.DisplayName(player.userID)
|
name := p.DisplayName(player.userID)
|
||||||
nextIdx := game.nextActiveIdx()
|
nextIdx := game.nextActiveIdx()
|
||||||
target := game.players[nextIdx]
|
target := game.players[nextIdx]
|
||||||
targetName := p.DisplayName(target.userID)
|
targetName := p.multiName(target)
|
||||||
if target.isBot {
|
|
||||||
targetName = unoBotName()
|
|
||||||
}
|
|
||||||
|
|
||||||
flipped := p.executeColorRouletteMulti(game, target, color)
|
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",
|
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()
|
nextIdx := game.nextActiveIdx()
|
||||||
nextPlayer := game.players[nextIdx]
|
nextPlayer := game.players[nextIdx]
|
||||||
nextName := p.DisplayName(nextPlayer.userID)
|
nextName := p.multiName(nextPlayer)
|
||||||
if nextPlayer.isBot {
|
|
||||||
nextName = unoBotName()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch card.Value {
|
switch card.Value {
|
||||||
case unoSkip:
|
case unoSkip:
|
||||||
@@ -1395,10 +1476,7 @@ func (p *UnoPlugin) applyCardEffects(game *unoMultiGame, card unoCard) cardEffec
|
|||||||
var skippedNames []string
|
var skippedNames []string
|
||||||
for _, pl := range game.players {
|
for _, pl := range game.players {
|
||||||
if pl != game.currentPlayer() && pl.active {
|
if pl != game.currentPlayer() && pl.active {
|
||||||
name := p.DisplayName(pl.userID)
|
name := p.multiName(pl)
|
||||||
if pl.isBot {
|
|
||||||
name = unoBotName()
|
|
||||||
}
|
|
||||||
skippedNames = append(skippedNames, name)
|
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
|
// After reverse, get next player in new direction
|
||||||
nextIdx = game.nextActiveIdx()
|
nextIdx = game.nextActiveIdx()
|
||||||
nextPlayer = game.players[nextIdx]
|
nextPlayer = game.players[nextIdx]
|
||||||
nextName = p.DisplayName(nextPlayer.userID)
|
nextName = p.multiName(nextPlayer)
|
||||||
if nextPlayer.isBot {
|
|
||||||
nextName = unoBotName()
|
|
||||||
}
|
|
||||||
result.skippedName = nextName
|
result.skippedName = nextName
|
||||||
result.drawnCount = game.stackTotal
|
result.drawnCount = game.stackTotal
|
||||||
game.currentIdx = game.nextActiveIdx()
|
game.currentIdx = game.nextActiveIdx()
|
||||||
@@ -1566,11 +1641,7 @@ func (p *UnoPlugin) applyAndAnnounce(game *unoMultiGame, player *unoMultiPlayer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next player
|
// Next player
|
||||||
nextUp := game.currentPlayer()
|
nextUpName := p.multiName(game.currentPlayer())
|
||||||
nextUpName := p.DisplayName(nextUp.userID)
|
|
||||||
if nextUp.isBot {
|
|
||||||
nextUpName = unoBotName()
|
|
||||||
}
|
|
||||||
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())
|
||||||
@@ -1596,10 +1667,7 @@ func (p *UnoPlugin) startWD4Challenge(game *unoMultiGame, wd4Player *unoMultiPla
|
|||||||
game.currentIdx = nextIdx
|
game.currentIdx = nextIdx
|
||||||
game.turnID++
|
game.turnID++
|
||||||
|
|
||||||
playerName := p.DisplayName(wd4Player.userID)
|
playerName := p.multiName(wd4Player)
|
||||||
if wd4Player.isBot {
|
|
||||||
playerName = unoBotName()
|
|
||||||
}
|
|
||||||
|
|
||||||
if victim.isBot {
|
if victim.isBot {
|
||||||
// Bot decides whether to challenge
|
// 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) {
|
func (p *UnoPlugin) resolveWD4Challenge(game *unoMultiGame, challenged bool) {
|
||||||
wd4Player := game.wd4Player
|
wd4Player := game.wd4Player
|
||||||
victim := game.wd4Victim
|
victim := game.wd4Victim
|
||||||
wd4Name := p.DisplayName(wd4Player.userID)
|
wd4Name := p.multiName(wd4Player)
|
||||||
if wd4Player.isBot {
|
victimName := p.multiName(victim)
|
||||||
wd4Name = unoBotName()
|
|
||||||
}
|
|
||||||
victimName := p.DisplayName(victim.userID)
|
|
||||||
if victim.isBot {
|
|
||||||
victimName = unoBotName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear challenge state
|
// Clear challenge state
|
||||||
game.wd4Player = nil
|
game.wd4Player = nil
|
||||||
@@ -1699,7 +1761,7 @@ func (p *UnoPlugin) botHandleWD4Challenge(game *unoMultiGame) {
|
|||||||
threshold = 30
|
threshold = 30
|
||||||
}
|
}
|
||||||
challenged := rand.IntN(100) < threshold
|
challenged := rand.IntN(100) < threshold
|
||||||
bn := unoBotName()
|
bn := game.currentPlayer().displayName
|
||||||
|
|
||||||
if challenged {
|
if challenged {
|
||||||
p.SendMessage(game.roomID, fmt.Sprintf("⚡ %s challenges the Wild Draw Four!", bn))
|
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) {
|
func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
|
||||||
bot := game.currentPlayer()
|
bot := game.currentPlayer()
|
||||||
bn := unoBotName()
|
bn := bot.displayName
|
||||||
|
|
||||||
// No Mercy stacking: bot must stack or absorb
|
// No Mercy stacking: bot must stack or absorb
|
||||||
if game.noMercy && game.stackMinValue > 0 {
|
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())
|
p.SendMessage(game.roomID, roomBuf.String())
|
||||||
roomBuf.Reset()
|
roomBuf.Reset()
|
||||||
}
|
}
|
||||||
p.multiBotWins(game)
|
p.multiBotWins(game, bot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1976,10 +2038,7 @@ func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
|
|||||||
// applyCardEffects already advanced currentIdx to the victim
|
// applyCardEffects already advanced currentIdx to the victim
|
||||||
if game.noMercy && card.Value == unoWildColorRoulette {
|
if game.noMercy && card.Value == unoWildColorRoulette {
|
||||||
target := game.players[game.currentIdx]
|
target := game.players[game.currentIdx]
|
||||||
targetName := p.DisplayName(target.userID)
|
targetName := p.multiName(target)
|
||||||
if target.isBot {
|
|
||||||
targetName = bn
|
|
||||||
}
|
|
||||||
flipped := p.executeColorRouletteMulti(game, target, game.topColor)
|
flipped := p.executeColorRouletteMulti(game, target, game.topColor)
|
||||||
roomBuf.WriteString(fmt.Sprintf("\n 🎰 Color Roulette! %s flips %d cards until finding %s %s.",
|
roomBuf.WriteString(fmt.Sprintf("\n 🎰 Color Roulette! %s flips %d cards until finding %s %s.",
|
||||||
targetName, len(flipped), game.topColor.Emoji(), game.topColor))
|
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)
|
// Next turn label (applyCardEffects already advanced currentIdx)
|
||||||
nextUp := game.currentPlayer()
|
nextUpName := p.multiName(game.currentPlayer())
|
||||||
nextUpName := p.DisplayName(nextUp.userID)
|
|
||||||
if nextUp.isBot {
|
|
||||||
nextUpName = unoBotName()
|
|
||||||
}
|
|
||||||
roomBuf.WriteString(fmt.Sprintf("\n It's %s's turn.", nextUpName))
|
roomBuf.WriteString(fmt.Sprintf("\n It's %s's turn.", nextUpName))
|
||||||
|
|
||||||
game.turns++
|
game.turns++
|
||||||
@@ -2151,10 +2206,7 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
|||||||
if game.noMercy && pendingCard != nil && pendingCard.Value == unoWildColorRoulette {
|
if game.noMercy && pendingCard != nil && pendingCard.Value == unoWildColorRoulette {
|
||||||
nextIdx := game.nextActiveIdx()
|
nextIdx := game.nextActiveIdx()
|
||||||
target := game.players[nextIdx]
|
target := game.players[nextIdx]
|
||||||
targetName := p.DisplayName(target.userID)
|
targetName := p.multiName(target)
|
||||||
if target.isBot {
|
|
||||||
targetName = unoBotName()
|
|
||||||
}
|
|
||||||
flipped := p.executeColorRouletteMulti(game, target, color)
|
flipped := p.executeColorRouletteMulti(game, target, color)
|
||||||
p.SendMessage(game.roomID, fmt.Sprintf("🎰 Color Roulette! %s flips %d cards until finding %s.\n%s",
|
p.SendMessage(game.roomID, fmt.Sprintf("🎰 Color Roulette! %s flips %d cards until finding %s.\n%s",
|
||||||
targetName, len(flipped), color, pickNoMercyCommentary("color_roulette")))
|
targetName, len(flipped), color, pickNoMercyCommentary("color_roulette")))
|
||||||
@@ -2234,10 +2286,7 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
|||||||
target := botChooseSwapTarget(game, player)
|
target := botChooseSwapTarget(game, player)
|
||||||
if target != nil {
|
if target != nil {
|
||||||
swapHandsMulti(player, target)
|
swapHandsMulti(player, target)
|
||||||
targetName := p.DisplayName(target.userID)
|
targetName := p.multiName(target)
|
||||||
if target.isBot {
|
|
||||||
targetName = unoBotName()
|
|
||||||
}
|
|
||||||
p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Swapped hands with %s.", targetName))
|
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))
|
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)
|
aScore := scoreHand(a.hand)
|
||||||
bScore := scoreHand(b.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",
|
breakdown := fmt.Sprintf("**Final Scores:**\n%s: %s\n%s: %s",
|
||||||
nameOf(a), formatHandScore(a.hand),
|
p.multiName(a), formatHandScore(a.hand),
|
||||||
nameOf(b), formatHandScore(b.hand))
|
p.multiName(b), formatHandScore(b.hand))
|
||||||
|
|
||||||
var winner, loser *unoMultiPlayer
|
var winner, loser *unoMultiPlayer
|
||||||
switch {
|
switch {
|
||||||
@@ -2503,7 +2545,7 @@ func (p *UnoPlugin) multiSuddenDeathWinner(game *unoMultiGame, active []*unoMult
|
|||||||
|
|
||||||
p.SendMessage(game.roomID, fmt.Sprintf(
|
p.SendMessage(game.roomID, fmt.Sprintf(
|
||||||
"⏰ **SUDDEN DEATH!** %s wins on points!\n%s\n\n%s",
|
"⏰ **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 {
|
if winner.isBot {
|
||||||
p.multiBotWins2(game)
|
p.multiBotWins2(game)
|
||||||
@@ -2521,7 +2563,7 @@ func (p *UnoPlugin) multiPlayerWins2(game *unoMultiGame, winner *unoMultiPlayer)
|
|||||||
humanCount++
|
humanCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalPot := game.ante * float64(humanCount)
|
totalPot := game.ante * float64(humanCount+game.botAntes)
|
||||||
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
|
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
|
||||||
|
|
||||||
p.recordMultiGame(game, winner.userID, "sudden_death_win")
|
p.recordMultiGame(game, winner.userID, "sudden_death_win")
|
||||||
@@ -2535,7 +2577,7 @@ func (p *UnoPlugin) multiBotWins2(game *unoMultiGame) {
|
|||||||
humanCount++
|
humanCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalPot := game.ante * float64(humanCount)
|
totalPot := game.ante * float64(humanCount+game.botAntes)
|
||||||
p.addToPot(totalPot)
|
p.addToPot(totalPot)
|
||||||
|
|
||||||
for _, pl := range game.players {
|
for _, pl := range game.players {
|
||||||
@@ -2571,7 +2613,7 @@ func (p *UnoPlugin) multiPlayerWins(game *unoMultiGame, winner *unoMultiPlayer)
|
|||||||
humanCount++
|
humanCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalPot := game.ante * float64(humanCount)
|
totalPot := game.ante * float64(humanCount+game.botAntes)
|
||||||
|
|
||||||
name := p.DisplayName(winner.userID)
|
name := p.DisplayName(winner.userID)
|
||||||
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
|
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
|
||||||
@@ -2584,7 +2626,7 @@ func (p *UnoPlugin) multiPlayerWins(game *unoMultiGame, winner *unoMultiPlayer)
|
|||||||
p.cleanupMultiGame(game)
|
p.cleanupMultiGame(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UnoPlugin) multiBotWins(game *unoMultiGame) {
|
func (p *UnoPlugin) multiBotWins(game *unoMultiGame, winner *unoMultiPlayer) {
|
||||||
if game.done {
|
if game.done {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2603,12 +2645,12 @@ func (p *UnoPlugin) multiBotWins(game *unoMultiGame) {
|
|||||||
humanCount++
|
humanCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalPot := game.ante * float64(humanCount)
|
totalPot := game.ante * float64(humanCount+game.botAntes)
|
||||||
|
|
||||||
// Pot goes to community
|
// Pot goes to community
|
||||||
p.addToPot(totalPot)
|
p.addToPot(totalPot)
|
||||||
|
|
||||||
bn := unoBotName()
|
bn := winner.displayName
|
||||||
p.SendMessage(game.roomID, fmt.Sprintf(
|
p.SendMessage(game.roomID, fmt.Sprintf(
|
||||||
"💀 **%s wins Multiplayer UNO!**\n€%d goes to the community pot.\n\n%s",
|
"💀 **%s wins Multiplayer UNO!**\n€%d goes to the community pot.\n\n%s",
|
||||||
bn, int(totalPot), pickCommentary("bot_win")))
|
bn, int(totalPot), pickCommentary("bot_win")))
|
||||||
@@ -2638,7 +2680,7 @@ func (p *UnoPlugin) multiPlayerForfeit(game *unoMultiGame, player *unoMultiPlaye
|
|||||||
winner := active[0]
|
winner := active[0]
|
||||||
if winner.isBot {
|
if winner.isBot {
|
||||||
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. All humans out!", name))
|
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. All humans out!", name))
|
||||||
p.multiBotWins(game)
|
p.multiBotWins(game, winner)
|
||||||
} else {
|
} else {
|
||||||
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. Only one player remains!", name))
|
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. Only one player remains!", name))
|
||||||
p.multiPlayerWins(game, winner)
|
p.multiPlayerWins(game, winner)
|
||||||
@@ -2682,7 +2724,7 @@ func (p *UnoPlugin) recordMultiGame(game *unoMultiGame, winnerID id.UserID, resu
|
|||||||
}
|
}
|
||||||
|
|
||||||
humanCount := len(playerIDs)
|
humanCount := len(playerIDs)
|
||||||
totalPot := game.ante * float64(humanCount)
|
totalPot := game.ante * float64(humanCount+game.botAntes)
|
||||||
|
|
||||||
db.Exec("uno_multi: record game",
|
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)
|
`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) {
|
func (p *UnoPlugin) sendMultiStatus(game *unoMultiGame, player *unoMultiPlayer) {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
bn := unoBotName()
|
currentName := p.multiName(game.currentPlayer())
|
||||||
current := game.currentPlayer()
|
|
||||||
currentName := p.DisplayName(current.userID)
|
|
||||||
if current.isBot {
|
|
||||||
currentName = bn
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString(fmt.Sprintf("🃏 **UNO Status**\nDiscard pile: %s\nCurrent turn: %s\n\n",
|
sb.WriteString(fmt.Sprintf("🃏 **UNO Status**\nDiscard pile: %s\nCurrent turn: %s\n\n",
|
||||||
game.discardTop.DisplayWithColor(game.topColor), currentName))
|
game.discardTop.DisplayWithColor(game.topColor), currentName))
|
||||||
@@ -2725,11 +2762,7 @@ func (p *UnoPlugin) sendMultiStatus(game *unoMultiGame, player *unoMultiPlayer)
|
|||||||
if pl == player || !pl.active {
|
if pl == player || !pl.active {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := p.DisplayName(pl.userID)
|
counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
|
||||||
if pl.isBot {
|
|
||||||
name = bn
|
|
||||||
}
|
|
||||||
counts = append(counts, fmt.Sprintf("%s (%d)", name, len(pl.hand)))
|
|
||||||
}
|
}
|
||||||
sb.WriteString(strings.Join(counts, " | "))
|
sb.WriteString(strings.Join(counts, " | "))
|
||||||
|
|
||||||
@@ -2752,16 +2785,11 @@ func (p *UnoPlugin) sendMultiHandDisplay(game *unoMultiGame, player *unoMultiPla
|
|||||||
// Card counts for all opponents
|
// Card counts for all opponents
|
||||||
sb.WriteString("\nCard counts: ")
|
sb.WriteString("\nCard counts: ")
|
||||||
counts := make([]string, 0)
|
counts := make([]string, 0)
|
||||||
bn := unoBotName()
|
|
||||||
for _, pl := range game.players {
|
for _, pl := range game.players {
|
||||||
if pl == player || !pl.active {
|
if pl == player || !pl.active {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := p.DisplayName(pl.userID)
|
counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
|
||||||
if pl.isBot {
|
|
||||||
name = bn
|
|
||||||
}
|
|
||||||
counts = append(counts, fmt.Sprintf("%s (%d)", name, len(pl.hand)))
|
|
||||||
}
|
}
|
||||||
sb.WriteString(strings.Join(counts, " | "))
|
sb.WriteString(strings.Join(counts, " | "))
|
||||||
|
|
||||||
@@ -2790,16 +2818,11 @@ func (p *UnoPlugin) sendMultiHandDisplayStacking(game *unoMultiGame, player *uno
|
|||||||
|
|
||||||
sb.WriteString("\nCard counts: ")
|
sb.WriteString("\nCard counts: ")
|
||||||
counts := make([]string, 0)
|
counts := make([]string, 0)
|
||||||
bn := unoBotName()
|
|
||||||
for _, pl := range game.players {
|
for _, pl := range game.players {
|
||||||
if pl == player || !pl.active {
|
if pl == player || !pl.active {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := p.DisplayName(pl.userID)
|
counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
|
||||||
if pl.isBot {
|
|
||||||
name = bn
|
|
||||||
}
|
|
||||||
counts = append(counts, fmt.Sprintf("%s (%d)", name, len(pl.hand)))
|
|
||||||
}
|
}
|
||||||
sb.WriteString(strings.Join(counts, " | "))
|
sb.WriteString(strings.Join(counts, " | "))
|
||||||
|
|
||||||
@@ -2840,10 +2863,7 @@ func (p *UnoPlugin) handleMultiSwapChoice(game *unoMultiGame, player *unoMultiPl
|
|||||||
game.phase = unoMultiPhasePlay
|
game.phase = unoMultiPhasePlay
|
||||||
|
|
||||||
name := p.DisplayName(player.userID)
|
name := p.DisplayName(player.userID)
|
||||||
targetName := p.DisplayName(target.userID)
|
targetName := p.multiName(target)
|
||||||
if target.isBot {
|
|
||||||
targetName = unoBotName()
|
|
||||||
}
|
|
||||||
|
|
||||||
p.SendMessage(player.dmRoomID, fmt.Sprintf("Swapped hands with %s! You now have %d cards.", targetName, len(player.hand)))
|
p.SendMessage(player.dmRoomID, fmt.Sprintf("Swapped hands with %s! You now have %d cards.", targetName, len(player.hand)))
|
||||||
if !target.isBot {
|
if !target.isBot {
|
||||||
|
|||||||
@@ -201,10 +201,7 @@ func (p *UnoPlugin) checkMultiMercyElimination(game *unoMultiGame, player *unoMu
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.active = false
|
player.active = false
|
||||||
name := p.DisplayName(player.userID)
|
name := p.multiName(player)
|
||||||
if player.isBot {
|
|
||||||
name = unoBotName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffle cards back into draw pile
|
// Shuffle cards back into draw pile
|
||||||
game.drawPile = append(game.drawPile, player.hand...)
|
game.drawPile = append(game.drawPile, player.hand...)
|
||||||
@@ -226,7 +223,7 @@ func (p *UnoPlugin) checkMultiMercyElimination(game *unoMultiGame, player *unoMu
|
|||||||
if len(active) == 1 {
|
if len(active) == 1 {
|
||||||
winner := active[0]
|
winner := active[0]
|
||||||
if winner.isBot {
|
if winner.isBot {
|
||||||
p.multiBotWins(game)
|
p.multiBotWins(game, winner)
|
||||||
} else {
|
} else {
|
||||||
p.multiPlayerWins(game, winner)
|
p.multiPlayerWins(game, winner)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user