mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Add UNO Show 'Em No Mercy game mode
168-card deck with new card types (Skip Everyone, Discard All, Wild Reverse Draw Four, Wild Draw Six/Ten, Wild Color Roulette), draw stacking, draw-until-playable, mercy elimination at 25 cards, and optional 7-0 hand swap/rotation rule. Works in both solo and multiplayer. Includes blackjack max player increase to 4. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -279,8 +279,8 @@ func (p *BlackjackPlugin) handleBlackjack(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "You're already at the table!")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "You're already at the table!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(table.players) >= 2 {
|
if len(table.players) >= 4 {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Table is full (max 2 players).")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Table is full (max 4 players).")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.euro.Debit(ctx.Sender, bet, "blackjack_bet") {
|
if !p.euro.Debit(ctx.Sender, bet, "blackjack_bet") {
|
||||||
@@ -289,13 +289,20 @@ func (p *BlackjackPlugin) handleBlackjack(ctx MessageContext) error {
|
|||||||
|
|
||||||
table.players = append(table.players, &bjPlayer{UserID: ctx.Sender, Bet: bet})
|
table.players = append(table.players, &bjPlayer{UserID: ctx.Sender, Bet: bet})
|
||||||
name := p.bjDisplayName(ctx.Sender)
|
name := p.bjDisplayName(ctx.Sender)
|
||||||
|
|
||||||
|
if len(table.players) >= 4 {
|
||||||
_ = p.SendMessage(ctx.RoomID,
|
_ = p.SendMessage(ctx.RoomID,
|
||||||
fmt.Sprintf("🃏 **%s** joins the table! Bet: €%d\nTable is full — dealing!", name, int(bet)))
|
fmt.Sprintf("🃏 **%s** joins the table! Bet: €%d\nTable is full — dealing!", name, int(bet)))
|
||||||
|
|
||||||
if table.joinTimer != nil {
|
if table.joinTimer != nil {
|
||||||
table.joinTimer.Stop()
|
table.joinTimer.Stop()
|
||||||
}
|
}
|
||||||
p.startRound(ctx.RoomID, table)
|
p.startRound(ctx.RoomID, table)
|
||||||
|
} else {
|
||||||
|
remaining := len(table.players)
|
||||||
|
_ = p.SendMessage(ctx.RoomID,
|
||||||
|
fmt.Sprintf("🃏 **%s** joins the table! Bet: €%d (%d/4 players — `!blackjack deal` to start now)",
|
||||||
|
name, int(bet), remaining))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,15 +465,16 @@ func (p *BlackjackPlugin) findPlayer(table *bjTable, userID id.UserID) *bjPlayer
|
|||||||
|
|
||||||
func (p *BlackjackPlugin) handleHit(ctx MessageContext) error {
|
func (p *BlackjackPlugin) handleHit(ctx MessageContext) error {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
table, exists := p.tables[ctx.RoomID]
|
table, exists := p.tables[ctx.RoomID]
|
||||||
if !exists || table.phase != "playing" {
|
if !exists || table.phase != "playing" {
|
||||||
|
p.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
player := p.findPlayer(table, ctx.Sender)
|
player := p.findPlayer(table, ctx.Sender)
|
||||||
if player == nil || player.Done {
|
if player == nil || player.Done {
|
||||||
|
p.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,48 +485,70 @@ func (p *BlackjackPlugin) handleHit(ctx MessageContext) error {
|
|||||||
player.Bust = true
|
player.Bust = true
|
||||||
player.Done = true
|
player.Done = true
|
||||||
name := p.bjDisplayName(player.UserID)
|
name := p.bjDisplayName(player.UserID)
|
||||||
_ = p.SendMessage(ctx.RoomID,
|
msgs := []string{fmt.Sprintf("💥 **%s** busts with %s (%d)!", name, handStr(player.Hand), v)}
|
||||||
fmt.Sprintf("💥 **%s** busts with %s (%d)!", name, handStr(player.Hand), v))
|
allDoneMsgs := p.collectAllDone(ctx.RoomID, table)
|
||||||
p.checkAllDone(ctx.RoomID, table)
|
p.mu.Unlock()
|
||||||
|
for _, m := range append(msgs, allDoneMsgs...) {
|
||||||
|
_ = p.SendMessage(ctx.RoomID, m)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if v == 21 {
|
if v == 21 {
|
||||||
player.Done = true
|
player.Done = true
|
||||||
name := p.bjDisplayName(player.UserID)
|
name := p.bjDisplayName(player.UserID)
|
||||||
_ = p.SendMessage(ctx.RoomID,
|
msgs := []string{fmt.Sprintf("**%s** has 21! %s", name, handStr(player.Hand))}
|
||||||
fmt.Sprintf("**%s** has 21! %s", name, handStr(player.Hand)))
|
allDoneMsgs := p.collectAllDone(ctx.RoomID, table)
|
||||||
p.checkAllDone(ctx.RoomID, table)
|
p.mu.Unlock()
|
||||||
|
for _, m := range append(msgs, allDoneMsgs...) {
|
||||||
|
_ = p.SendMessage(ctx.RoomID, m)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = p.SendMessage(ctx.RoomID, p.renderTable(table, false))
|
msg := p.renderTable(table, false)
|
||||||
|
p.mu.Unlock()
|
||||||
|
_ = p.SendMessage(ctx.RoomID, msg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BlackjackPlugin) handleStand(ctx MessageContext) error {
|
func (p *BlackjackPlugin) handleStand(ctx MessageContext) error {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
table, exists := p.tables[ctx.RoomID]
|
table, exists := p.tables[ctx.RoomID]
|
||||||
if !exists || table.phase != "playing" {
|
if !exists || table.phase != "playing" {
|
||||||
|
p.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
player := p.findPlayer(table, ctx.Sender)
|
player := p.findPlayer(table, ctx.Sender)
|
||||||
if player == nil || player.Done {
|
if player == nil || player.Done {
|
||||||
|
p.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
player.Done = true
|
player.Done = true
|
||||||
name := p.bjDisplayName(player.UserID)
|
name := p.bjDisplayName(player.UserID)
|
||||||
_ = p.SendMessage(ctx.RoomID, fmt.Sprintf("**%s** stands at %d.", name, player.value()))
|
msgs := []string{fmt.Sprintf("**%s** stands at %d.", name, player.value())}
|
||||||
p.checkAllDone(ctx.RoomID, table)
|
allDoneMsgs := p.collectAllDone(ctx.RoomID, table)
|
||||||
|
p.mu.Unlock()
|
||||||
|
for _, m := range append(msgs, allDoneMsgs...) {
|
||||||
|
_ = p.SendMessage(ctx.RoomID, m)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAllDone checks if all players are done and triggers the dealer. Must be called with p.mu held.
|
// checkAllDone checks if all players are done and triggers the dealer. Must be called with p.mu held.
|
||||||
func (p *BlackjackPlugin) checkAllDone(roomID id.RoomID, table *bjTable) {
|
func (p *BlackjackPlugin) checkAllDone(roomID id.RoomID, table *bjTable) {
|
||||||
|
msgs := p.collectAllDone(roomID, table)
|
||||||
|
for _, m := range msgs {
|
||||||
|
_ = p.SendMessage(roomID, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectAllDone is like checkAllDone but returns messages instead of sending them.
|
||||||
|
// This allows callers to release the mutex before sending. Must be called with p.mu held.
|
||||||
|
func (p *BlackjackPlugin) collectAllDone(roomID id.RoomID, table *bjTable) []string {
|
||||||
var waiting []string
|
var waiting []string
|
||||||
for _, pl := range table.players {
|
for _, pl := range table.players {
|
||||||
if !pl.Done {
|
if !pl.Done {
|
||||||
@@ -527,14 +557,12 @@ func (p *BlackjackPlugin) checkAllDone(roomID id.RoomID, table *bjTable) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(waiting) > 0 {
|
if len(waiting) > 0 {
|
||||||
_ = p.SendMessage(roomID,
|
return []string{fmt.Sprintf("⏳ Waiting on: **%s**", strings.Join(waiting, "**, **"))}
|
||||||
fmt.Sprintf("⏳ Waiting on: **%s**", strings.Join(waiting, "**, **")))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All players done — stop timers and go to dealer
|
// All players done — stop timers and go to dealer
|
||||||
p.stopRoundTimers(table)
|
p.stopRoundTimers(table)
|
||||||
p.playDealer(roomID, table)
|
return p.collectDealer(roomID, table)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRoundTimer starts a shared timeout for the round plus reminder nudges. Must be called with p.mu held.
|
// startRoundTimer starts a shared timeout for the round plus reminder nudges. Must be called with p.mu held.
|
||||||
@@ -631,6 +659,14 @@ func (p *BlackjackPlugin) stopRoundTimers(table *bjTable) {
|
|||||||
|
|
||||||
// playDealer plays the dealer hand. Must be called with p.mu held.
|
// playDealer plays the dealer hand. Must be called with p.mu held.
|
||||||
func (p *BlackjackPlugin) playDealer(roomID id.RoomID, table *bjTable) {
|
func (p *BlackjackPlugin) playDealer(roomID id.RoomID, table *bjTable) {
|
||||||
|
msgs := p.collectDealer(roomID, table)
|
||||||
|
for _, m := range msgs {
|
||||||
|
_ = p.SendMessage(roomID, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectDealer is like playDealer but returns messages. Must be called with p.mu held.
|
||||||
|
func (p *BlackjackPlugin) collectDealer(roomID id.RoomID, table *bjTable) []string {
|
||||||
// Check if all players busted
|
// Check if all players busted
|
||||||
allBust := true
|
allBust := true
|
||||||
for _, pl := range table.players {
|
for _, pl := range table.players {
|
||||||
@@ -652,10 +688,18 @@ func (p *BlackjackPlugin) playDealer(roomID id.RoomID, table *bjTable) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.resolveRound(roomID, table)
|
return p.collectResolveRound(roomID, table)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BlackjackPlugin) resolveRound(roomID id.RoomID, table *bjTable) {
|
func (p *BlackjackPlugin) resolveRound(roomID id.RoomID, table *bjTable) {
|
||||||
|
msgs := p.collectResolveRound(roomID, table)
|
||||||
|
for _, m := range msgs {
|
||||||
|
_ = p.SendMessage(roomID, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectResolveRound resolves the round and returns messages. Must be called with p.mu held.
|
||||||
|
func (p *BlackjackPlugin) collectResolveRound(roomID id.RoomID, table *bjTable) []string {
|
||||||
table.phase = "done"
|
table.phase = "done"
|
||||||
dealerValue, _ := handValue(table.dealer)
|
dealerValue, _ := handValue(table.dealer)
|
||||||
dealerBust := dealerValue > 21
|
dealerBust := dealerValue > 21
|
||||||
@@ -738,7 +782,7 @@ func (p *BlackjackPlugin) resolveRound(roomID id.RoomID, table *bjTable) {
|
|||||||
table.joinTimer.Stop()
|
table.joinTimer.Stop()
|
||||||
}
|
}
|
||||||
delete(p.tables, roomID)
|
delete(p.tables, roomID)
|
||||||
_ = p.SendMessage(roomID, sb.String())
|
return []string{sb.String()}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ const (
|
|||||||
unoDrawTwo
|
unoDrawTwo
|
||||||
unoWildCard
|
unoWildCard
|
||||||
unoWildDrawFour
|
unoWildDrawFour
|
||||||
|
// No Mercy card types
|
||||||
|
unoSkipEveryone // colored — skips all other players
|
||||||
|
unoDrawFour // colored Draw Four (NOT wild)
|
||||||
|
unoDiscardAll // colored — discard all of that color from hand
|
||||||
|
unoWildReverseDraw4 // wild — reverse + draw 4 + color change
|
||||||
|
unoWildDrawSix // wild — draw 6
|
||||||
|
unoWildDrawTen // wild — draw 10
|
||||||
|
unoWildColorRoulette // wild — next player flips until chosen color
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v unoValue) String() string {
|
func (v unoValue) String() string {
|
||||||
@@ -94,6 +102,20 @@ func (v unoValue) String() string {
|
|||||||
return "Wild"
|
return "Wild"
|
||||||
case v == unoWildDrawFour:
|
case v == unoWildDrawFour:
|
||||||
return "Wild Draw Four"
|
return "Wild Draw Four"
|
||||||
|
case v == unoSkipEveryone:
|
||||||
|
return "Skip Everyone"
|
||||||
|
case v == unoDrawFour:
|
||||||
|
return "Draw Four"
|
||||||
|
case v == unoDiscardAll:
|
||||||
|
return "Discard All"
|
||||||
|
case v == unoWildReverseDraw4:
|
||||||
|
return "Wild Reverse Draw Four"
|
||||||
|
case v == unoWildDrawSix:
|
||||||
|
return "Wild Draw Six"
|
||||||
|
case v == unoWildDrawTen:
|
||||||
|
return "Wild Draw Ten"
|
||||||
|
case v == unoWildColorRoulette:
|
||||||
|
return "Wild Color Roulette"
|
||||||
default:
|
default:
|
||||||
return "?"
|
return "?"
|
||||||
}
|
}
|
||||||
@@ -101,7 +123,10 @@ func (v unoValue) String() string {
|
|||||||
|
|
||||||
func (v unoValue) isAction() bool {
|
func (v unoValue) isAction() bool {
|
||||||
return v == unoSkip || v == unoReverse || v == unoDrawTwo ||
|
return v == unoSkip || v == unoReverse || v == unoDrawTwo ||
|
||||||
v == unoWildCard || v == unoWildDrawFour
|
v == unoWildCard || v == unoWildDrawFour ||
|
||||||
|
v == unoSkipEveryone || v == unoDrawFour || v == unoDiscardAll ||
|
||||||
|
v == unoWildReverseDraw4 || v == unoWildDrawSix ||
|
||||||
|
v == unoWildDrawTen || v == unoWildColorRoulette
|
||||||
}
|
}
|
||||||
|
|
||||||
type unoCard struct {
|
type unoCard struct {
|
||||||
@@ -109,8 +134,14 @@ type unoCard struct {
|
|||||||
Value unoValue
|
Value unoValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c unoCard) isWild() bool {
|
||||||
|
return c.Value == unoWildCard || c.Value == unoWildDrawFour ||
|
||||||
|
c.Value == unoWildReverseDraw4 || c.Value == unoWildDrawSix ||
|
||||||
|
c.Value == unoWildDrawTen || c.Value == unoWildColorRoulette
|
||||||
|
}
|
||||||
|
|
||||||
func (c unoCard) Display() string {
|
func (c unoCard) Display() string {
|
||||||
if c.Value == unoWildCard || c.Value == unoWildDrawFour {
|
if c.isWild() {
|
||||||
return fmt.Sprintf("%s %s", unoWild.Emoji(), c.Value)
|
return fmt.Sprintf("%s %s", unoWild.Emoji(), c.Value)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s %s", c.Color.Emoji(), c.Value)
|
return fmt.Sprintf("%s %s", c.Color.Emoji(), c.Value)
|
||||||
@@ -118,14 +149,14 @@ func (c unoCard) Display() string {
|
|||||||
|
|
||||||
// DisplayWithColor shows a wild card with its chosen color.
|
// DisplayWithColor shows a wild card with its chosen color.
|
||||||
func (c unoCard) DisplayWithColor(chosenColor unoColor) string {
|
func (c unoCard) DisplayWithColor(chosenColor unoColor) string {
|
||||||
if c.Value == unoWildCard || c.Value == unoWildDrawFour {
|
if c.isWild() {
|
||||||
return fmt.Sprintf("%s %s", chosenColor.Emoji(), c.Value)
|
return fmt.Sprintf("%s %s", chosenColor.Emoji(), c.Value)
|
||||||
}
|
}
|
||||||
return c.Display()
|
return c.Display()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c unoCard) canPlayOn(top unoCard, topColor unoColor) bool {
|
func (c unoCard) canPlayOn(top unoCard, topColor unoColor) bool {
|
||||||
if c.Value == unoWildCard || c.Value == unoWildDrawFour {
|
if c.isWild() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return c.Color == topColor || c.Value == top.Value
|
return c.Color == topColor || c.Value == top.Value
|
||||||
@@ -190,6 +221,12 @@ type unoGame struct {
|
|||||||
startedAt time.Time
|
startedAt time.Time
|
||||||
done bool // set true when game ends — prevents double-completion
|
done bool // set true when game ends — prevents double-completion
|
||||||
|
|
||||||
|
// No Mercy mode
|
||||||
|
noMercy bool
|
||||||
|
sevenZeroRule bool
|
||||||
|
stackTotal int // cumulative draw penalty during stacking
|
||||||
|
stackMinValue int // minimum draw value to stack (0 = not stacking)
|
||||||
|
|
||||||
idleTimer *time.Timer
|
idleTimer *time.Timer
|
||||||
warningTimer *time.Timer
|
warningTimer *time.Timer
|
||||||
}
|
}
|
||||||
@@ -211,7 +248,12 @@ func (g *unoGame) draw(n int) []unoCard {
|
|||||||
|
|
||||||
func (g *unoGame) reshuffleDiscard() {
|
func (g *unoGame) reshuffleDiscard() {
|
||||||
// Rebuild deck from cards not in play
|
// Rebuild deck from cards not in play
|
||||||
fresh := newUnoDeck()
|
var fresh []unoCard
|
||||||
|
if g.noMercy {
|
||||||
|
fresh = newNoMercyDeck()
|
||||||
|
} else {
|
||||||
|
fresh = newUnoDeck()
|
||||||
|
}
|
||||||
inPlay := make(map[unoCard]int)
|
inPlay := make(map[unoCard]int)
|
||||||
|
|
||||||
for _, c := range g.playerHand {
|
for _, c := range g.playerHand {
|
||||||
@@ -361,7 +403,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 €amount | !uno start €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 go", 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"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +424,9 @@ func (p *UnoPlugin) OnMessage(ctx MessageContext) error {
|
|||||||
// Multiplayer subcommands
|
// Multiplayer subcommands
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(lower, "start "):
|
case strings.HasPrefix(lower, "start "):
|
||||||
return p.handleMultiStart(ctx, strings.TrimSpace(args[6:]))
|
rest := strings.TrimSpace(args[6:])
|
||||||
|
noMercy, sevenZero, amountStr := parseNoMercyFlags(rest)
|
||||||
|
return p.handleMultiStart(ctx, amountStr, noMercy, sevenZero)
|
||||||
case lower == "join":
|
case lower == "join":
|
||||||
return p.handleMultiJoin(ctx)
|
return p.handleMultiJoin(ctx)
|
||||||
case lower == "go":
|
case lower == "go":
|
||||||
@@ -393,16 +437,17 @@ func (p *UnoPlugin) OnMessage(ctx MessageContext) error {
|
|||||||
return p.handleMultiCancel(ctx)
|
return p.handleMultiCancel(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Solo challenge: !uno €amount
|
// Solo challenge: !uno [nomercy [7-0]] €amount
|
||||||
|
noMercy, sevenZero, amountStr := parseNoMercyFlags(args)
|
||||||
if isGamesRoom(ctx.RoomID) {
|
if isGamesRoom(ctx.RoomID) {
|
||||||
return p.handleChallenge(ctx, ctx.RoomID)
|
return p.handleChallenge(ctx, ctx.RoomID, noMercy, sevenZero, amountStr)
|
||||||
}
|
}
|
||||||
// Allow starting from DM — announce to games room
|
// Allow starting from DM — announce to games room
|
||||||
gr := gamesRoom()
|
gr := gamesRoom()
|
||||||
if gr == "" {
|
if gr == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return p.handleChallenge(ctx, id.RoomID(gr))
|
return p.handleChallenge(ctx, id.RoomID(gr), noMercy, sevenZero, amountStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DM gameplay — check solo games first, then multiplayer
|
// DM gameplay — check solo games first, then multiplayer
|
||||||
@@ -487,13 +532,12 @@ func (p *UnoPlugin) handlePotCheck(ctx MessageContext) error {
|
|||||||
// Challenge
|
// Challenge
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func (p *UnoPlugin) handleChallenge(ctx MessageContext, announceRoom id.RoomID) error {
|
func (p *UnoPlugin) handleChallenge(ctx MessageContext, announceRoom id.RoomID, noMercy, sevenZeroRule bool, rawAmount string) error {
|
||||||
reply := func(text string) error {
|
reply := func(text string) error {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, text)
|
return p.SendReply(ctx.RoomID, ctx.EventID, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
args := p.GetArgs(ctx.Body, "uno")
|
amountStr := strings.TrimPrefix(strings.TrimSpace(rawAmount), "€")
|
||||||
amountStr := strings.TrimPrefix(strings.TrimSpace(args), "€")
|
|
||||||
var amount float64
|
var amount float64
|
||||||
fmt.Sscanf(amountStr, "%f", &amount)
|
fmt.Sscanf(amountStr, "%f", &amount)
|
||||||
|
|
||||||
@@ -531,7 +575,7 @@ func (p *UnoPlugin) handleChallenge(ctx MessageContext, announceRoom id.RoomID)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize game
|
// Initialize game
|
||||||
game := p.initGame(ctx.Sender, announceRoom, dmRoom, amount)
|
game := p.initGame(ctx.Sender, announceRoom, dmRoom, amount, noMercy, sevenZeroRule)
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.games[ctx.Sender] = game
|
p.games[ctx.Sender] = game
|
||||||
@@ -541,9 +585,18 @@ func (p *UnoPlugin) handleChallenge(ctx MessageContext, announceRoom id.RoomID)
|
|||||||
// Room announcement
|
// Room announcement
|
||||||
playerName := p.unoDisplayName(ctx.Sender)
|
playerName := p.unoDisplayName(ctx.Sender)
|
||||||
botName := unoBotName()
|
botName := unoBotName()
|
||||||
|
modeTag := ""
|
||||||
|
startComment := pickCommentary("start")
|
||||||
|
if noMercy {
|
||||||
|
modeTag = " 🔥 **NO MERCY**"
|
||||||
|
startComment = pickNoMercyCommentary("nomercy_start")
|
||||||
|
if sevenZeroRule {
|
||||||
|
modeTag += " (7-0)"
|
||||||
|
}
|
||||||
|
}
|
||||||
p.SendMessage(announceRoom, fmt.Sprintf(
|
p.SendMessage(announceRoom, fmt.Sprintf(
|
||||||
"🃏 **%s** has challenged %s to Uno! Stakes: €%d\n\n%s\n\n[Check your DMs to play.]",
|
"🃏 **%s** has challenged %s to Uno!%s Stakes: €%d\n\n%s\n\n[Check your DMs to play.]",
|
||||||
playerName, botName, int(amount), pickCommentary("start"),
|
playerName, botName, modeTag, int(amount), startComment,
|
||||||
))
|
))
|
||||||
|
|
||||||
// Send initial hand to player (auto-draws if no playable cards)
|
// Send initial hand to player (auto-draws if no playable cards)
|
||||||
@@ -555,8 +608,13 @@ func (p *UnoPlugin) handleChallenge(ctx MessageContext, announceRoom id.RoomID)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UnoPlugin) initGame(playerID id.UserID, roomID, dmRoom id.RoomID, wager float64) *unoGame {
|
func (p *UnoPlugin) initGame(playerID id.UserID, roomID, dmRoom id.RoomID, wager float64, noMercy, sevenZeroRule bool) *unoGame {
|
||||||
deck := newUnoDeck()
|
var deck []unoCard
|
||||||
|
if noMercy {
|
||||||
|
deck = newNoMercyDeck()
|
||||||
|
} else {
|
||||||
|
deck = newUnoDeck()
|
||||||
|
}
|
||||||
|
|
||||||
// Deal 7 cards each — copy into own slices to avoid shared backing array
|
// Deal 7 cards each — copy into own slices to avoid shared backing array
|
||||||
playerHand := make([]unoCard, 7)
|
playerHand := make([]unoCard, 7)
|
||||||
@@ -566,11 +624,11 @@ func (p *UnoPlugin) initGame(playerID id.UserID, roomID, dmRoom id.RoomID, wager
|
|||||||
remaining := make([]unoCard, len(deck)-14)
|
remaining := make([]unoCard, len(deck)-14)
|
||||||
copy(remaining, deck[14:])
|
copy(remaining, deck[14:])
|
||||||
|
|
||||||
// Flip starting card — must not be Wild or Wild Draw Four
|
// Flip starting card — must be a number card (not action/wild)
|
||||||
var startCard unoCard
|
var startCard unoCard
|
||||||
startIdx := -1
|
startIdx := -1
|
||||||
for i, c := range remaining {
|
for i, c := range remaining {
|
||||||
if c.Value != unoWildCard && c.Value != unoWildDrawFour {
|
if !c.Value.isAction() && !c.isWild() {
|
||||||
startCard = c
|
startCard = c
|
||||||
startIdx = i
|
startIdx = i
|
||||||
break
|
break
|
||||||
@@ -584,7 +642,11 @@ func (p *UnoPlugin) initGame(playerID id.UserID, roomID, dmRoom id.RoomID, wager
|
|||||||
drawPile = append(drawPile, remaining[startIdx+1:]...)
|
drawPile = append(drawPile, remaining[startIdx+1:]...)
|
||||||
} else {
|
} else {
|
||||||
// All remaining are wilds (essentially impossible) — reshuffle whole deck
|
// All remaining are wilds (essentially impossible) — reshuffle whole deck
|
||||||
|
if noMercy {
|
||||||
|
deck = newNoMercyDeck()
|
||||||
|
} else {
|
||||||
deck = newUnoDeck()
|
deck = newUnoDeck()
|
||||||
|
}
|
||||||
copy(playerHand, deck[:7])
|
copy(playerHand, deck[:7])
|
||||||
copy(botHand, deck[7:14])
|
copy(botHand, deck[7:14])
|
||||||
startCard = deck[14]
|
startCard = deck[14]
|
||||||
@@ -604,6 +666,8 @@ func (p *UnoPlugin) initGame(playerID id.UserID, roomID, dmRoom id.RoomID, wager
|
|||||||
topColor: startCard.Color,
|
topColor: startCard.Color,
|
||||||
phase: unoPhasePlay,
|
phase: unoPhasePlay,
|
||||||
startedAt: time.Now(),
|
startedAt: time.Now(),
|
||||||
|
noMercy: noMercy,
|
||||||
|
sevenZeroRule: sevenZeroRule,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,12 +703,33 @@ func (p *UnoPlugin) handleDMInput(ctx MessageContext, game *unoGame) error {
|
|||||||
game.calledUno = true
|
game.calledUno = true
|
||||||
return p.SendMessage(game.dmRoomID, "✅ UNO called!")
|
return p.SendMessage(game.dmRoomID, "✅ UNO called!")
|
||||||
}
|
}
|
||||||
|
// No Mercy stacking: accept the stack
|
||||||
|
if game.noMercy && game.stackMinValue > 0 && (input == "accept" || input == "a") {
|
||||||
|
drawn := game.draw(game.stackTotal)
|
||||||
|
game.playerHand = append(game.playerHand, drawn...)
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("💥 You accept the stack and draw %d cards.\n%s",
|
||||||
|
game.stackTotal, pickNoMercyCommentary("stack_absorbed")))
|
||||||
|
game.stackTotal = 0
|
||||||
|
game.stackMinValue = 0
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.botTurn(game)
|
||||||
|
}
|
||||||
if input == "draw" {
|
if input == "draw" {
|
||||||
|
// No Mercy: during stacking, draw is not allowed — must stack or accept
|
||||||
|
if game.noMercy && game.stackMinValue > 0 {
|
||||||
|
return p.SendMessage(game.dmRoomID, "You must play a draw card to stack, or type **accept** to draw the stack.")
|
||||||
|
}
|
||||||
return p.handlePlayerDraw(game)
|
return p.handlePlayerDraw(game)
|
||||||
}
|
}
|
||||||
// Parse card number
|
// Parse card number
|
||||||
var cardIdx int
|
var cardIdx int
|
||||||
if _, err := fmt.Sscanf(input, "%d", &cardIdx); err != nil || cardIdx < 1 || cardIdx > len(game.playerHand) {
|
if _, err := fmt.Sscanf(input, "%d", &cardIdx); err != nil || cardIdx < 1 || cardIdx > len(game.playerHand) {
|
||||||
|
if game.noMercy && game.stackMinValue > 0 {
|
||||||
|
return p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("Reply with a card number (1-%d) to stack, or **accept** to draw %d cards.", len(game.playerHand), game.stackTotal))
|
||||||
|
}
|
||||||
return p.SendMessage(game.dmRoomID,
|
return p.SendMessage(game.dmRoomID,
|
||||||
fmt.Sprintf("Reply with a number (1-%d) to play a card, or **draw** to draw.", len(game.playerHand)))
|
fmt.Sprintf("Reply with a number (1-%d) to play a card, or **draw** to draw.", len(game.playerHand)))
|
||||||
}
|
}
|
||||||
@@ -657,7 +742,14 @@ func (p *UnoPlugin) handleDMInput(ctx MessageContext, game *unoGame) error {
|
|||||||
func (p *UnoPlugin) handlePlayerPlay(game *unoGame, idx int) error {
|
func (p *UnoPlugin) handlePlayerPlay(game *unoGame, idx int) error {
|
||||||
card := game.playerHand[idx]
|
card := game.playerHand[idx]
|
||||||
|
|
||||||
if !card.canPlayOn(game.discardTop, game.topColor) {
|
// No Mercy stacking validation
|
||||||
|
if game.noMercy && game.stackMinValue > 0 {
|
||||||
|
if !card.canPlayOnStacking(game.topColor, game.stackMinValue) {
|
||||||
|
return p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("You can't stack %s — need a draw card worth +%d or more. Type **accept** to draw %d.",
|
||||||
|
card.Display(), game.stackMinValue, game.stackTotal))
|
||||||
|
}
|
||||||
|
} else if !card.canPlayOn(game.discardTop, game.topColor) {
|
||||||
return p.SendMessage(game.dmRoomID, fmt.Sprintf("You can't play %s on %s. Choose another card or **draw**.",
|
return p.SendMessage(game.dmRoomID, fmt.Sprintf("You can't play %s on %s. Choose another card or **draw**.",
|
||||||
card.Display(), game.discardTop.DisplayWithColor(game.topColor)))
|
card.Display(), game.discardTop.DisplayWithColor(game.topColor)))
|
||||||
}
|
}
|
||||||
@@ -678,7 +770,7 @@ func (p *UnoPlugin) handlePlayerPlay(game *unoGame, idx int) error {
|
|||||||
game.discardTop = card
|
game.discardTop = card
|
||||||
game.turns++
|
game.turns++
|
||||||
|
|
||||||
if card.Value == unoWildCard || card.Value == unoWildDrawFour {
|
if card.isWild() {
|
||||||
game.pendingCard = &card
|
game.pendingCard = &card
|
||||||
game.phase = unoPhaseChooseColor
|
game.phase = unoPhaseChooseColor
|
||||||
return p.SendMessage(game.dmRoomID,
|
return p.SendMessage(game.dmRoomID,
|
||||||
@@ -709,7 +801,35 @@ func (p *UnoPlugin) handlePlayerPlay(game *unoGame, idx int) error {
|
|||||||
|
|
||||||
p.SendMessage(game.dmRoomID, fmt.Sprintf("You played %s.%s", card.Display(), bookMsg))
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("You played %s.%s", card.Display(), bookMsg))
|
||||||
|
|
||||||
|
// No Mercy: Discard All — remove remaining cards of same color
|
||||||
|
if game.noMercy && card.Value == unoDiscardAll {
|
||||||
|
discarded := discardAllOfColor(&game.playerHand, card.Color)
|
||||||
|
if discarded > 0 {
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("Discarded %d additional %s cards!", discarded, card.Color))
|
||||||
|
}
|
||||||
|
if len(game.playerHand) == 0 {
|
||||||
|
return p.playerWins(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No Mercy: 7-0 rule
|
||||||
|
if game.noMercy && game.sevenZeroRule {
|
||||||
|
if card.Value == unoSeven || card.Value == unoZero {
|
||||||
|
swapHandsSolo(game)
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("Hands swapped! You now have %d cards, %s has %d.",
|
||||||
|
len(game.playerHand), unoBotName(), len(game.botHand)))
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply action card effects
|
// Apply action card effects
|
||||||
|
if game.noMercy && isDrawCard(card.Value) {
|
||||||
|
// No Mercy stacking: bot gets a chance to stack
|
||||||
|
return p.soloPlayerPlaysDrawCard(game, card)
|
||||||
|
}
|
||||||
|
|
||||||
if card.Value == unoDrawTwo {
|
if card.Value == unoDrawTwo {
|
||||||
drawn := game.draw(2)
|
drawn := game.draw(2)
|
||||||
game.botHand = append(game.botHand, drawn...)
|
game.botHand = append(game.botHand, drawn...)
|
||||||
@@ -722,6 +842,12 @@ func (p *UnoPlugin) handlePlayerPlay(game *unoGame, idx int) error {
|
|||||||
return p.afterBotTurn(game)
|
return p.afterBotTurn(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No Mercy: Skip Everyone (same as skip in 2-player)
|
||||||
|
if game.noMercy && card.Value == unoSkipEveryone {
|
||||||
|
p.SendMessage(game.dmRoomID, unoBotName()+"'s turn is skipped!")
|
||||||
|
return p.afterBotTurn(game)
|
||||||
|
}
|
||||||
|
|
||||||
// Bot's turn
|
// Bot's turn
|
||||||
return p.botTurn(game)
|
return p.botTurn(game)
|
||||||
}
|
}
|
||||||
@@ -765,7 +891,30 @@ func (p *UnoPlugin) handleColorChoice(game *unoGame, input string) error {
|
|||||||
|
|
||||||
p.SendMessage(game.dmRoomID, fmt.Sprintf("Color set to %s %s.%s", color.Emoji(), color, bookMsg))
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("Color set to %s %s.%s", color.Emoji(), color, bookMsg))
|
||||||
|
|
||||||
// Wild Draw Four — bot draws 4 and loses turn
|
// No Mercy wild effects
|
||||||
|
if game.noMercy && pendingCard != nil {
|
||||||
|
switch pendingCard.Value {
|
||||||
|
case unoWildReverseDraw4:
|
||||||
|
// Reverse (acts as skip in 2-player) + stacking with +4
|
||||||
|
return p.soloPlayerPlaysDrawCard(game, *pendingCard)
|
||||||
|
case unoWildDrawSix:
|
||||||
|
return p.soloPlayerPlaysDrawCard(game, *pendingCard)
|
||||||
|
case unoWildDrawTen:
|
||||||
|
return p.soloPlayerPlaysDrawCard(game, *pendingCard)
|
||||||
|
case unoWildColorRoulette:
|
||||||
|
// Next player (bot) flips until chosen color
|
||||||
|
bn := unoBotName()
|
||||||
|
flipped := p.executeColorRouletteSolo(game, false, color)
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("🎰 Color Roulette! %s flips %d cards until finding %s %s.\n%s",
|
||||||
|
bn, len(flipped), color.Emoji(), color, pickNoMercyCommentary("color_roulette")))
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.afterBotTurn(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wild Draw Four — bot draws 4 and loses turn (classic mode)
|
||||||
if pendingCard != nil && pendingCard.Value == unoWildDrawFour {
|
if pendingCard != nil && pendingCard.Value == unoWildDrawFour {
|
||||||
drawn := game.draw(4)
|
drawn := game.draw(4)
|
||||||
game.botHand = append(game.botHand, drawn...)
|
game.botHand = append(game.botHand, drawn...)
|
||||||
@@ -784,6 +933,11 @@ func (p *UnoPlugin) handleColorChoice(game *unoGame, input string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *UnoPlugin) handlePlayerDraw(game *unoGame) error {
|
func (p *UnoPlugin) handlePlayerDraw(game *unoGame) error {
|
||||||
|
// No Mercy: draw until playable
|
||||||
|
if game.noMercy {
|
||||||
|
return p.handlePlayerDrawNoMercy(game)
|
||||||
|
}
|
||||||
|
|
||||||
drawn := game.draw(1)
|
drawn := game.draw(1)
|
||||||
if len(drawn) == 0 {
|
if len(drawn) == 0 {
|
||||||
p.SendMessage(game.dmRoomID, "No cards left to draw! Turn passes to "+unoBotName()+".")
|
p.SendMessage(game.dmRoomID, "No cards left to draw! Turn passes to "+unoBotName()+".")
|
||||||
@@ -804,6 +958,48 @@ func (p *UnoPlugin) handlePlayerDraw(game *unoGame) error {
|
|||||||
return p.botTurn(game)
|
return p.botTurn(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *UnoPlugin) handlePlayerDrawNoMercy(game *unoGame) error {
|
||||||
|
var allDrawn []unoCard
|
||||||
|
var playableCard *unoCard
|
||||||
|
|
||||||
|
for {
|
||||||
|
drawn := game.draw(1)
|
||||||
|
if len(drawn) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
card := drawn[0]
|
||||||
|
game.playerHand = append(game.playerHand, card)
|
||||||
|
allDrawn = append(allDrawn, card)
|
||||||
|
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if card.canPlayOn(game.discardTop, game.topColor) {
|
||||||
|
playableCard = &card
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allDrawn) == 0 {
|
||||||
|
p.SendMessage(game.dmRoomID, "No cards left to draw! Turn passes to "+unoBotName()+".")
|
||||||
|
return p.botTurn(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
if playableCard != nil {
|
||||||
|
game.drawnCard = playableCard
|
||||||
|
game.phase = unoPhaseDrawnPlayable
|
||||||
|
return p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("Drew %d card(s): %s\nLast card is playable! Play it? (**yes** / **no**)",
|
||||||
|
len(allDrawn), formatDrawnCards(allDrawn)))
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("Drew %d card(s): %s\nNo playable card found. Turn passes to %s.",
|
||||||
|
len(allDrawn), formatDrawnCards(allDrawn), unoBotName()))
|
||||||
|
return p.botTurn(game)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *UnoPlugin) handleDrawnPlayable(game *unoGame, input string) error {
|
func (p *UnoPlugin) handleDrawnPlayable(game *unoGame, input string) error {
|
||||||
if input != "yes" && input != "y" && input != "no" && input != "n" {
|
if input != "yes" && input != "y" && input != "no" && input != "n" {
|
||||||
return p.SendMessage(game.dmRoomID, "Play the drawn card? (**yes** / **no**)")
|
return p.SendMessage(game.dmRoomID, "Play the drawn card? (**yes** / **no**)")
|
||||||
@@ -828,7 +1024,7 @@ func (p *UnoPlugin) handleDrawnPlayable(game *unoGame, input string) error {
|
|||||||
game.discardTop = drawnCard
|
game.discardTop = drawnCard
|
||||||
game.turns++
|
game.turns++
|
||||||
|
|
||||||
if drawnCard.Value == unoWildCard || drawnCard.Value == unoWildDrawFour {
|
if drawnCard.isWild() {
|
||||||
game.pendingCard = &drawnCard
|
game.pendingCard = &drawnCard
|
||||||
game.phase = unoPhaseChooseColor
|
game.phase = unoPhaseChooseColor
|
||||||
return p.SendMessage(game.dmRoomID,
|
return p.SendMessage(game.dmRoomID,
|
||||||
@@ -851,13 +1047,41 @@ func (p *UnoPlugin) handleDrawnPlayable(game *unoGame, input string) error {
|
|||||||
}
|
}
|
||||||
p.SendMessage(game.dmRoomID, fmt.Sprintf("You played %s.%s", drawnCard.Display(), bookMsg))
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("You played %s.%s", drawnCard.Display(), bookMsg))
|
||||||
|
|
||||||
|
// No Mercy: Discard All
|
||||||
|
if game.noMercy && drawnCard.Value == unoDiscardAll {
|
||||||
|
discarded := discardAllOfColor(&game.playerHand, drawnCard.Color)
|
||||||
|
if discarded > 0 {
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("Discarded %d additional %s cards!", discarded, drawnCard.Color))
|
||||||
|
}
|
||||||
|
if len(game.playerHand) == 0 {
|
||||||
|
return p.playerWins(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No Mercy: 7-0 rule
|
||||||
|
if game.noMercy && game.sevenZeroRule {
|
||||||
|
if drawnCard.Value == unoSeven || drawnCard.Value == unoZero {
|
||||||
|
swapHandsSolo(game)
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("Hands swapped! You now have %d cards, %s has %d.",
|
||||||
|
len(game.playerHand), unoBotName(), len(game.botHand)))
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No Mercy stacking for draw cards
|
||||||
|
if game.noMercy && isDrawCard(drawnCard.Value) {
|
||||||
|
return p.soloPlayerPlaysDrawCard(game, drawnCard)
|
||||||
|
}
|
||||||
|
|
||||||
if drawnCard.Value == unoDrawTwo {
|
if drawnCard.Value == unoDrawTwo {
|
||||||
drawn := game.draw(2)
|
drawn := game.draw(2)
|
||||||
game.botHand = append(game.botHand, drawn...)
|
game.botHand = append(game.botHand, drawn...)
|
||||||
p.SendMessage(game.dmRoomID, unoBotName()+" draws 2 cards and loses their turn.")
|
p.SendMessage(game.dmRoomID, unoBotName()+" draws 2 cards and loses their turn.")
|
||||||
return p.afterBotTurn(game)
|
return p.afterBotTurn(game)
|
||||||
}
|
}
|
||||||
if drawnCard.Value == unoSkip || drawnCard.Value == unoReverse {
|
if drawnCard.Value == unoSkip || drawnCard.Value == unoReverse || drawnCard.Value == unoSkipEveryone {
|
||||||
p.SendMessage(game.dmRoomID, unoBotName()+"'s turn is skipped!")
|
p.SendMessage(game.dmRoomID, unoBotName()+"'s turn is skipped!")
|
||||||
return p.afterBotTurn(game)
|
return p.afterBotTurn(game)
|
||||||
}
|
}
|
||||||
@@ -865,11 +1089,78 @@ func (p *UnoPlugin) handleDrawnPlayable(game *unoGame, input string) error {
|
|||||||
return p.botTurn(game)
|
return p.botTurn(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Solo stacking (No Mercy)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// soloPlayerPlaysDrawCard handles when the player plays a draw card in No Mercy.
|
||||||
|
// The bot gets a chance to stack back.
|
||||||
|
func (p *UnoPlugin) soloPlayerPlaysDrawCard(game *unoGame, card unoCard) error {
|
||||||
|
dv := cardDrawValue(card.Value)
|
||||||
|
game.stackTotal += dv
|
||||||
|
game.stackMinValue = dv
|
||||||
|
|
||||||
|
// Reverse effect for Wild Reverse Draw Four
|
||||||
|
if card.Value == unoWildReverseDraw4 {
|
||||||
|
// In 2-player, reverse = skip, so effectively bot is the target
|
||||||
|
// Direction doesn't matter in solo but we note the reverse
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bot tries to stack
|
||||||
|
return p.soloBotHandleStack(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
// soloBotHandleStack has the bot try to stack or absorb.
|
||||||
|
func (p *UnoPlugin) soloBotHandleStack(game *unoGame) error {
|
||||||
|
bn := unoBotName()
|
||||||
|
|
||||||
|
// Bot tries to find a stackable card
|
||||||
|
stackCard, idx := botPickStackCard(game.botHand, game.topColor, game.stackMinValue)
|
||||||
|
if idx < 0 {
|
||||||
|
// Bot absorbs
|
||||||
|
drawn := game.draw(game.stackTotal)
|
||||||
|
game.botHand = append(game.botHand, drawn...)
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("💥 %s can't stack! Draws %d cards. (%d cards now)\n%s",
|
||||||
|
bn, game.stackTotal, len(game.botHand), pickNoMercyCommentary("stack_absorbed")))
|
||||||
|
game.stackTotal = 0
|
||||||
|
game.stackMinValue = 0
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.afterBotTurn(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bot stacks
|
||||||
|
game.botHand = append(game.botHand[:idx], game.botHand[idx+1:]...)
|
||||||
|
game.discardTop = stackCard
|
||||||
|
dv := cardDrawValue(stackCard.Value)
|
||||||
|
game.stackTotal += dv
|
||||||
|
game.stackMinValue = dv
|
||||||
|
game.turns++
|
||||||
|
|
||||||
|
if stackCard.isWild() {
|
||||||
|
game.topColor = botPickColor(game.botHand)
|
||||||
|
} else {
|
||||||
|
game.topColor = stackCard.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("🔥 %s stacks: %s! Total penalty: +%d",
|
||||||
|
bn, stackCard.DisplayWithColor(game.topColor), game.stackTotal))
|
||||||
|
|
||||||
|
// Player must now respond to the stack
|
||||||
|
return p.playerTurnOrAutoDraw(game)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Bot turn
|
// Bot turn
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func (p *UnoPlugin) botTurn(game *unoGame) error {
|
func (p *UnoPlugin) botTurn(game *unoGame) error {
|
||||||
|
// No Mercy stacking: if a stack is pending, bot must handle it
|
||||||
|
if game.noMercy && game.stackMinValue > 0 {
|
||||||
|
return p.soloBotHandleStack(game)
|
||||||
|
}
|
||||||
|
|
||||||
// Long game commentary (DM only)
|
// Long game commentary (DM only)
|
||||||
if game.turns > 0 && game.turns%30 == 0 {
|
if game.turns > 0 && game.turns%30 == 0 {
|
||||||
p.SendMessage(game.dmRoomID, pickCommentary("long_game"))
|
p.SendMessage(game.dmRoomID, pickCommentary("long_game"))
|
||||||
@@ -878,9 +1169,12 @@ func (p *UnoPlugin) botTurn(game *unoGame) error {
|
|||||||
card, idx := p.botChooseCard(game)
|
card, idx := p.botChooseCard(game)
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
// Bot draws
|
// Bot draws
|
||||||
|
if game.noMercy {
|
||||||
|
return p.botDrawNoMercy(game)
|
||||||
|
}
|
||||||
|
|
||||||
drawn := game.draw(1)
|
drawn := game.draw(1)
|
||||||
if len(drawn) == 0 {
|
if len(drawn) == 0 {
|
||||||
// Can't draw — turn passes to player (show hand, don't auto-draw to avoid infinite loop)
|
|
||||||
p.SendMessage(game.dmRoomID, unoBotName()+" can't draw — no cards left. Turn passes.")
|
p.SendMessage(game.dmRoomID, unoBotName()+" can't draw — no cards left. Turn passes.")
|
||||||
game.phase = unoPhasePlay
|
game.phase = unoPhasePlay
|
||||||
p.sendHandDisplay(game)
|
p.sendHandDisplay(game)
|
||||||
@@ -888,9 +1182,8 @@ func (p *UnoPlugin) botTurn(game *unoGame) error {
|
|||||||
}
|
}
|
||||||
game.botHand = append(game.botHand, drawn[0])
|
game.botHand = append(game.botHand, drawn[0])
|
||||||
|
|
||||||
// Check if drawn card is playable
|
|
||||||
if drawn[0].canPlayOn(game.discardTop, game.topColor) {
|
if drawn[0].canPlayOn(game.discardTop, game.topColor) {
|
||||||
game.botHand = game.botHand[:len(game.botHand)-1] // remove last (drawn card)
|
game.botHand = game.botHand[:len(game.botHand)-1]
|
||||||
return p.botPlaysCard(game, drawn[0])
|
return p.botPlaysCard(game, drawn[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -907,12 +1200,68 @@ func (p *UnoPlugin) botTurn(game *unoGame) error {
|
|||||||
return p.botPlaysCard(game, card)
|
return p.botPlaysCard(game, card)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// botDrawNoMercy draws until playable for the bot in No Mercy mode.
|
||||||
|
func (p *UnoPlugin) botDrawNoMercy(game *unoGame) error {
|
||||||
|
bn := unoBotName()
|
||||||
|
var allDrawn []unoCard
|
||||||
|
var playableCard *unoCard
|
||||||
|
|
||||||
|
for {
|
||||||
|
drawn := game.draw(1)
|
||||||
|
if len(drawn) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
card := drawn[0]
|
||||||
|
game.botHand = append(game.botHand, card)
|
||||||
|
allDrawn = append(allDrawn, card)
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if card.canPlayOn(game.discardTop, game.topColor) {
|
||||||
|
playableCard = &card
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allDrawn) == 0 {
|
||||||
|
p.SendMessage(game.dmRoomID, bn+" can't draw — no cards left. Turn passes.")
|
||||||
|
game.phase = unoPhasePlay
|
||||||
|
p.sendHandDisplay(game)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if playableCard != nil {
|
||||||
|
// Remove the playable card from bot's hand
|
||||||
|
for i := len(game.botHand) - 1; i >= 0; i-- {
|
||||||
|
if game.botHand[i] == *playableCard {
|
||||||
|
game.botHand = append(game.botHand[:i], game.botHand[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commentKey := "bot_draw_normal"
|
||||||
|
if game.bookDown {
|
||||||
|
commentKey = "bot_draw_bookdown"
|
||||||
|
}
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("%s drew %d card(s). %s",
|
||||||
|
bn, len(allDrawn), pickCommentary(commentKey)))
|
||||||
|
return p.botPlaysCard(game, *playableCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
commentKey := "bot_draw_normal"
|
||||||
|
if game.bookDown {
|
||||||
|
commentKey = "bot_draw_bookdown"
|
||||||
|
}
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("%s drew %d card(s). No playable card. %s",
|
||||||
|
bn, len(allDrawn), pickCommentary(commentKey)))
|
||||||
|
return p.playerTurnOrAutoDraw(game)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
||||||
game.discardTop = card
|
game.discardTop = card
|
||||||
game.turns++
|
game.turns++
|
||||||
|
|
||||||
// Choose color for wilds
|
// Choose color for wilds
|
||||||
if card.Value == unoWildCard || card.Value == unoWildDrawFour {
|
if card.isWild() {
|
||||||
game.topColor = p.botChooseColor(game)
|
game.topColor = p.botChooseColor(game)
|
||||||
} else {
|
} else {
|
||||||
game.topColor = card.Color
|
game.topColor = card.Color
|
||||||
@@ -926,6 +1275,15 @@ func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
|||||||
|
|
||||||
switch card.Value {
|
switch card.Value {
|
||||||
case unoDrawTwo:
|
case unoDrawTwo:
|
||||||
|
if game.noMercy {
|
||||||
|
// No Mercy: stacking
|
||||||
|
dm.WriteString(fmt.Sprintf("%s plays: %s — stack incoming! (+%d total)",
|
||||||
|
bn, displayCard, cardDrawValue(card.Value)))
|
||||||
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
game.stackTotal = cardDrawValue(card.Value)
|
||||||
|
game.stackMinValue = cardDrawValue(card.Value)
|
||||||
|
return p.playerTurnOrAutoDraw(game)
|
||||||
|
}
|
||||||
commentKey := "bot_draw_two"
|
commentKey := "bot_draw_two"
|
||||||
if game.bookDown {
|
if game.bookDown {
|
||||||
commentKey = "bot_draw_two_bookdown"
|
commentKey = "bot_draw_two_bookdown"
|
||||||
@@ -935,6 +1293,15 @@ func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
|||||||
drawn := game.draw(2)
|
drawn := game.draw(2)
|
||||||
game.playerHand = append(game.playerHand, drawn...)
|
game.playerHand = append(game.playerHand, drawn...)
|
||||||
|
|
||||||
|
case unoDrawFour:
|
||||||
|
// Only exists in No Mercy — colored draw four
|
||||||
|
dm.WriteString(fmt.Sprintf("%s plays: %s — stack incoming! (+%d total)",
|
||||||
|
bn, displayCard, cardDrawValue(card.Value)))
|
||||||
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
game.stackTotal = cardDrawValue(card.Value)
|
||||||
|
game.stackMinValue = cardDrawValue(card.Value)
|
||||||
|
return p.playerTurnOrAutoDraw(game)
|
||||||
|
|
||||||
case unoWildDrawFour:
|
case unoWildDrawFour:
|
||||||
commentKey := "bot_wild_draw_four"
|
commentKey := "bot_wild_draw_four"
|
||||||
if game.bookDown {
|
if game.bookDown {
|
||||||
@@ -945,6 +1312,42 @@ func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
|||||||
drawn := game.draw(4)
|
drawn := game.draw(4)
|
||||||
game.playerHand = append(game.playerHand, drawn...)
|
game.playerHand = append(game.playerHand, drawn...)
|
||||||
|
|
||||||
|
case unoWildReverseDraw4:
|
||||||
|
dm.WriteString(fmt.Sprintf("%s plays: %s (chose %s %s) — stack incoming! (+%d total)",
|
||||||
|
bn, card.Display(), game.topColor.Emoji(), game.topColor, cardDrawValue(card.Value)))
|
||||||
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
game.stackTotal = cardDrawValue(card.Value)
|
||||||
|
game.stackMinValue = cardDrawValue(card.Value)
|
||||||
|
return p.playerTurnOrAutoDraw(game)
|
||||||
|
|
||||||
|
case unoWildDrawSix:
|
||||||
|
dm.WriteString(fmt.Sprintf("%s plays: %s (chose %s %s) — stack incoming! (+%d total)",
|
||||||
|
bn, card.Display(), game.topColor.Emoji(), game.topColor, cardDrawValue(card.Value)))
|
||||||
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
game.stackTotal = cardDrawValue(card.Value)
|
||||||
|
game.stackMinValue = cardDrawValue(card.Value)
|
||||||
|
return p.playerTurnOrAutoDraw(game)
|
||||||
|
|
||||||
|
case unoWildDrawTen:
|
||||||
|
dm.WriteString(fmt.Sprintf("%s plays: %s (chose %s %s) — stack incoming! (+%d total)",
|
||||||
|
bn, card.Display(), game.topColor.Emoji(), game.topColor, cardDrawValue(card.Value)))
|
||||||
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
game.stackTotal = cardDrawValue(card.Value)
|
||||||
|
game.stackMinValue = cardDrawValue(card.Value)
|
||||||
|
return p.playerTurnOrAutoDraw(game)
|
||||||
|
|
||||||
|
case unoWildColorRoulette:
|
||||||
|
flipped := p.executeColorRouletteSolo(game, true, game.topColor)
|
||||||
|
dm.WriteString(fmt.Sprintf("%s plays: %s (chose %s %s)\n🎰 Color Roulette! You flip %d cards.\n%s",
|
||||||
|
bn, card.Display(), game.topColor.Emoji(), game.topColor,
|
||||||
|
len(flipped), pickNoMercyCommentary("color_roulette")))
|
||||||
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Player was the victim — bot goes again (skip effect)
|
||||||
|
return p.botTurn(game)
|
||||||
|
|
||||||
case unoSkip, unoReverse:
|
case unoSkip, unoReverse:
|
||||||
commentKey := "bot_play_normal"
|
commentKey := "bot_play_normal"
|
||||||
if game.bookDown {
|
if game.bookDown {
|
||||||
@@ -953,6 +1356,25 @@ func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
|||||||
dm.WriteString(fmt.Sprintf(pickCommentary(commentKey), displayCard))
|
dm.WriteString(fmt.Sprintf(pickCommentary(commentKey), displayCard))
|
||||||
dm.WriteString("\nYour turn is skipped!")
|
dm.WriteString("\nYour turn is skipped!")
|
||||||
|
|
||||||
|
case unoSkipEveryone:
|
||||||
|
commentKey := "bot_play_normal"
|
||||||
|
if game.bookDown {
|
||||||
|
commentKey = "bot_play_bookdown"
|
||||||
|
}
|
||||||
|
dm.WriteString(fmt.Sprintf(pickCommentary(commentKey), displayCard))
|
||||||
|
dm.WriteString("\nYour turn is skipped!")
|
||||||
|
|
||||||
|
case unoDiscardAll:
|
||||||
|
discarded := discardAllOfColor(&game.botHand, card.Color)
|
||||||
|
commentKey := "bot_play_normal"
|
||||||
|
if game.bookDown {
|
||||||
|
commentKey = "bot_play_bookdown"
|
||||||
|
}
|
||||||
|
dm.WriteString(fmt.Sprintf(pickCommentary(commentKey), displayCard))
|
||||||
|
if discarded > 0 {
|
||||||
|
dm.WriteString(fmt.Sprintf("\n%s discards %d additional %s cards!", bn, discarded, card.Color))
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
commentKey := "bot_play_normal"
|
commentKey := "bot_play_normal"
|
||||||
if game.bookDown {
|
if game.bookDown {
|
||||||
@@ -961,7 +1383,20 @@ func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
|||||||
dm.WriteString(fmt.Sprintf(pickCommentary(commentKey), displayCard))
|
dm.WriteString(fmt.Sprintf(pickCommentary(commentKey), displayCard))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check bot win
|
// No Mercy: 7-0 rule effects from bot
|
||||||
|
if game.noMercy && game.sevenZeroRule {
|
||||||
|
if card.Value == unoSeven || card.Value == unoZero {
|
||||||
|
swapHandsSolo(game)
|
||||||
|
dm.WriteString(fmt.Sprintf("\n%s\nHands swapped! You now have %d cards.",
|
||||||
|
pickNoMercyCommentary("hand_swap"), len(game.playerHand)))
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bot win (after discard all, etc.)
|
||||||
if len(game.botHand) == 0 {
|
if len(game.botHand) == 0 {
|
||||||
p.SendMessage(game.dmRoomID, dm.String())
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
return p.botWins(game)
|
return p.botWins(game)
|
||||||
@@ -982,12 +1417,21 @@ func (p *UnoPlugin) botPlaysCard(game *unoGame, card unoCard) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip/Reverse — bot goes again in 2-player
|
// Skip/Reverse — bot goes again in 2-player
|
||||||
if card.Value == unoSkip || card.Value == unoReverse {
|
if card.Value == unoSkip || card.Value == unoReverse ||
|
||||||
|
card.Value == unoSkipEveryone {
|
||||||
p.SendMessage(game.dmRoomID, dm.String())
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
return p.botTurn(game)
|
return p.botTurn(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.SendMessage(game.dmRoomID, dm.String())
|
p.SendMessage(game.dmRoomID, dm.String())
|
||||||
|
|
||||||
|
// Mercy check after player received cards (color roulette victim, etc.)
|
||||||
|
if game.noMercy {
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return p.playerTurnOrAutoDraw(game)
|
return p.playerTurnOrAutoDraw(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1120,6 +1564,9 @@ func botPickColor(hand []unoCard) unoColor {
|
|||||||
|
|
||||||
// Solo wrappers for backward compatibility
|
// Solo wrappers for backward compatibility
|
||||||
func (p *UnoPlugin) botChooseCard(game *unoGame) (unoCard, int) {
|
func (p *UnoPlugin) botChooseCard(game *unoGame) (unoCard, int) {
|
||||||
|
if game.noMercy {
|
||||||
|
return botPickCardNoMercy(game.botHand, game.discardTop, game.topColor, game.bookDown, len(game.playerHand), game.stackMinValue)
|
||||||
|
}
|
||||||
return botPickCard(game.botHand, game.discardTop, game.topColor, game.bookDown, len(game.playerHand))
|
return botPickCard(game.botHand, game.discardTop, game.topColor, game.bookDown, len(game.playerHand))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1150,6 +1597,9 @@ func (p *UnoPlugin) appendHandDisplay(game *unoGame, sb *strings.Builder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString(fmt.Sprintf("\n%s has %d cards.", unoBotName(), len(game.botHand)))
|
sb.WriteString(fmt.Sprintf("\n%s has %d cards.", unoBotName(), len(game.botHand)))
|
||||||
|
if game.noMercy && len(game.playerHand) >= 20 {
|
||||||
|
sb.WriteString(fmt.Sprintf("\n⚠️ **You have %d cards! (25 = eliminated)**", len(game.playerHand)))
|
||||||
|
}
|
||||||
sb.WriteString("\n\nReply with a card number to play, or **draw** to draw.")
|
sb.WriteString("\n\nReply with a card number to play, or **draw** to draw.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1159,19 +1609,98 @@ func (p *UnoPlugin) sendHandDisplay(game *unoGame) {
|
|||||||
p.SendMessage(game.dmRoomID, sb.String())
|
p.SendMessage(game.dmRoomID, sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendHandDisplayStacking shows hand during stacking — only stackable cards are playable.
|
||||||
|
func (p *UnoPlugin) sendHandDisplayStacking(game *unoGame) {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("⚠️ **Stack incoming: +%d!**\nDiscard pile: %s\n\n**Your hand:**\n",
|
||||||
|
game.stackTotal, game.discardTop.DisplayWithColor(game.topColor)))
|
||||||
|
|
||||||
|
for i, c := range game.playerHand {
|
||||||
|
marker := ""
|
||||||
|
if c.canPlayOnStacking(game.topColor, game.stackMinValue) {
|
||||||
|
marker = " ✅"
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%d. %s%s\n", i+1, c.Display(), marker))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("\n%s has %d cards.", unoBotName(), len(game.botHand)))
|
||||||
|
sb.WriteString("\n\nPlay a draw card to stack, or type **accept** to draw the stack.")
|
||||||
|
|
||||||
|
p.SendMessage(game.dmRoomID, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
// playerTurnOrAutoDraw shows the hand if the player has playable cards,
|
// playerTurnOrAutoDraw shows the hand if the player has playable cards,
|
||||||
// otherwise auto-draws and passes to the bot.
|
// otherwise auto-draws and passes to the bot.
|
||||||
func (p *UnoPlugin) playerTurnOrAutoDraw(game *unoGame) error {
|
func (p *UnoPlugin) playerTurnOrAutoDraw(game *unoGame) error {
|
||||||
|
// No Mercy stacking: if a stack is pending, show stack prompt
|
||||||
|
if game.noMercy && game.stackMinValue > 0 {
|
||||||
|
if hasStackableCard(game.playerHand, game.topColor, game.stackMinValue) {
|
||||||
|
game.phase = unoPhasePlay
|
||||||
|
p.sendHandDisplayStacking(game)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Player must absorb the stack
|
||||||
|
drawn := game.draw(game.stackTotal)
|
||||||
|
game.playerHand = append(game.playerHand, drawn...)
|
||||||
|
p.SendMessage(game.dmRoomID, fmt.Sprintf("💥 No stackable card! You draw %d cards.\n%s",
|
||||||
|
game.stackTotal, pickNoMercyCommentary("stack_absorbed")))
|
||||||
|
game.stackTotal = 0
|
||||||
|
game.stackMinValue = 0
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// After absorbing, bot's turn (player was skipped)
|
||||||
|
return p.botTurn(game)
|
||||||
|
}
|
||||||
|
|
||||||
if game.hasPlayable() {
|
if game.hasPlayable() {
|
||||||
game.phase = unoPhasePlay
|
game.phase = unoPhasePlay
|
||||||
p.sendHandDisplay(game)
|
p.sendHandDisplay(game)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// No playable cards — try to draw
|
// No Mercy: draw until playable
|
||||||
|
if game.noMercy {
|
||||||
|
var allDrawn []unoCard
|
||||||
|
var playableCard *unoCard
|
||||||
|
for {
|
||||||
|
cards := game.draw(1)
|
||||||
|
if len(cards) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
card := cards[0]
|
||||||
|
game.playerHand = append(game.playerHand, card)
|
||||||
|
allDrawn = append(allDrawn, card)
|
||||||
|
if p.checkSoloMercyElimination(game) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if card.canPlayOn(game.discardTop, game.topColor) {
|
||||||
|
playableCard = &card
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(allDrawn) == 0 {
|
||||||
|
p.SendMessage(game.dmRoomID, "No playable cards and deck is empty.")
|
||||||
|
game.phase = unoPhasePlay
|
||||||
|
p.sendHandDisplay(game)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if playableCard != nil {
|
||||||
|
game.drawnCard = playableCard
|
||||||
|
game.phase = unoPhaseDrawnPlayable
|
||||||
|
return p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("No playable cards — drew %d card(s): %s\nLast card is playable! Play it? (**yes** / **no**)",
|
||||||
|
len(allDrawn), formatDrawnCards(allDrawn)))
|
||||||
|
}
|
||||||
|
p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("No playable cards — drew %d card(s). None playable. Turn passes to %s.",
|
||||||
|
len(allDrawn), unoBotName()))
|
||||||
|
return p.botTurn(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classic: draw one
|
||||||
drawn := game.draw(1)
|
drawn := game.draw(1)
|
||||||
if len(drawn) == 0 {
|
if len(drawn) == 0 {
|
||||||
// Deck empty + no playable cards — show hand, let player try manually
|
|
||||||
p.SendMessage(game.dmRoomID, "No playable cards and deck is empty.")
|
p.SendMessage(game.dmRoomID, "No playable cards and deck is empty.")
|
||||||
game.phase = unoPhasePlay
|
game.phase = unoPhasePlay
|
||||||
p.sendHandDisplay(game)
|
p.sendHandDisplay(game)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
614
internal/plugin/uno_nomercy.go
Normal file
614
internal/plugin/uno_nomercy.go
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// No Mercy helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// cardDrawValue returns the draw penalty for a card (0 if not a draw card).
|
||||||
|
func cardDrawValue(v unoValue) int {
|
||||||
|
switch v {
|
||||||
|
case unoDrawTwo:
|
||||||
|
return 2
|
||||||
|
case unoDrawFour, unoWildReverseDraw4:
|
||||||
|
return 4
|
||||||
|
case unoWildDrawSix:
|
||||||
|
return 6
|
||||||
|
case unoWildDrawTen:
|
||||||
|
return 10
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDrawCard(v unoValue) bool {
|
||||||
|
return cardDrawValue(v) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// canPlayOnStacking checks if a card can be played during a stacking phase.
|
||||||
|
// Only draw cards with value >= stackMinValue are allowed.
|
||||||
|
// Wild draws always match; colored draws must match topColor.
|
||||||
|
func (c unoCard) canPlayOnStacking(topColor unoColor, stackMinValue int) bool {
|
||||||
|
dv := cardDrawValue(c.Value)
|
||||||
|
if dv < stackMinValue {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.isWild() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return c.Color == topColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasStackableCard returns true if the hand contains a card that can be stacked.
|
||||||
|
func hasStackableCard(hand []unoCard, topColor unoColor, stackMinValue int) bool {
|
||||||
|
for _, c := range hand {
|
||||||
|
if c.canPlayOnStacking(topColor, stackMinValue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// No Mercy deck (168 cards)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func newNoMercyDeck() []unoCard {
|
||||||
|
var cards []unoCard
|
||||||
|
colors := []unoColor{unoRed, unoBlue, unoYellow, unoGreen}
|
||||||
|
|
||||||
|
for _, color := range colors {
|
||||||
|
// Numbers 0-9: ×2 each
|
||||||
|
for v := unoZero; v <= unoNine; v++ {
|
||||||
|
cards = append(cards, unoCard{color, v})
|
||||||
|
cards = append(cards, unoCard{color, v})
|
||||||
|
}
|
||||||
|
// Skip ×3
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
cards = append(cards, unoCard{color, unoSkip})
|
||||||
|
}
|
||||||
|
// Skip Everyone ×2
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
cards = append(cards, unoCard{color, unoSkipEveryone})
|
||||||
|
}
|
||||||
|
// Reverse ×4
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
cards = append(cards, unoCard{color, unoReverse})
|
||||||
|
}
|
||||||
|
// Draw Two ×2
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
cards = append(cards, unoCard{color, unoDrawTwo})
|
||||||
|
}
|
||||||
|
// Draw Four (colored) ×2
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
cards = append(cards, unoCard{color, unoDrawFour})
|
||||||
|
}
|
||||||
|
// Discard All ×3
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
cards = append(cards, unoCard{color, unoDiscardAll})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wild Reverse Draw Four ×8
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
cards = append(cards, unoCard{unoWild, unoWildReverseDraw4})
|
||||||
|
}
|
||||||
|
// Wild Draw Six ×4
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
cards = append(cards, unoCard{unoWild, unoWildDrawSix})
|
||||||
|
}
|
||||||
|
// Wild Draw Ten ×4
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
cards = append(cards, unoCard{unoWild, unoWildDrawTen})
|
||||||
|
}
|
||||||
|
// Wild Color Roulette ×8
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
cards = append(cards, unoCard{unoWild, unoWildColorRoulette})
|
||||||
|
}
|
||||||
|
|
||||||
|
rand.Shuffle(len(cards), func(i, j int) { cards[i], cards[j] = cards[j], cards[i] })
|
||||||
|
return cards
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mercy Rule
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const mercyLimit = 25
|
||||||
|
|
||||||
|
// checkSoloMercyElimination checks if the player or bot has 25+ cards.
|
||||||
|
// Returns true if someone was eliminated (game ends).
|
||||||
|
func (p *UnoPlugin) checkSoloMercyElimination(game *unoGame) bool {
|
||||||
|
if !game.noMercy {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(game.playerHand) >= mercyLimit {
|
||||||
|
p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("💀 **MERCY KILL!** You have %d cards — eliminated!", len(game.playerHand)))
|
||||||
|
p.botWins(game)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(game.botHand) >= mercyLimit {
|
||||||
|
bn := unoBotName()
|
||||||
|
p.SendMessage(game.dmRoomID,
|
||||||
|
fmt.Sprintf("💀 **MERCY KILL!** %s has %d cards — eliminated!", bn, len(game.botHand)))
|
||||||
|
p.playerWins(game)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkMultiMercyElimination checks if a multiplayer player has 25+ cards.
|
||||||
|
// Returns true if the player was eliminated.
|
||||||
|
func (p *UnoPlugin) checkMultiMercyElimination(game *unoMultiGame, player *unoMultiPlayer) bool {
|
||||||
|
if !game.noMercy || len(player.hand) < mercyLimit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
player.active = false
|
||||||
|
name := p.unoDisplayName(player.userID)
|
||||||
|
if player.isBot {
|
||||||
|
name = unoBotName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle cards back into draw pile
|
||||||
|
game.drawPile = append(game.drawPile, player.hand...)
|
||||||
|
rand.Shuffle(len(game.drawPile), func(i, j int) { game.drawPile[i], game.drawPile[j] = game.drawPile[j], game.drawPile[i] })
|
||||||
|
player.hand = nil
|
||||||
|
|
||||||
|
p.SendMessage(game.roomID,
|
||||||
|
fmt.Sprintf("💀 **MERCY KILL!** %s had %d+ cards — eliminated!\n\n%s",
|
||||||
|
name, mercyLimit, pickNoMercyCommentary("mercy_kill")))
|
||||||
|
|
||||||
|
if !player.isBot {
|
||||||
|
p.SendMessage(player.dmRoomID,
|
||||||
|
fmt.Sprintf("💀 You've been mercy-killed! (%d+ cards)", mercyLimit))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if game should end
|
||||||
|
active := game.activePlayers()
|
||||||
|
if len(active) <= 1 {
|
||||||
|
if len(active) == 1 {
|
||||||
|
winner := active[0]
|
||||||
|
if winner.isBot {
|
||||||
|
p.multiBotWins(game)
|
||||||
|
} else {
|
||||||
|
p.multiPlayerWins(game, winner)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
game.done = true
|
||||||
|
p.SendMessage(game.roomID, "🃏 Game ended — no players remaining.")
|
||||||
|
p.cleanupMultiGame(game)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Draw-until-playable helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// formatDrawnCards formats a list of drawn cards for display.
|
||||||
|
func formatDrawnCards(cards []unoCard) string {
|
||||||
|
if len(cards) == 1 {
|
||||||
|
return cards[0].Display()
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
for _, c := range cards {
|
||||||
|
parts = append(parts, c.Display())
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Color Roulette
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// executeColorRoulette flips cards from draw pile until the chosen color appears.
|
||||||
|
// All flipped cards are added to the target's hand.
|
||||||
|
// Returns the flipped cards for display.
|
||||||
|
func (p *UnoPlugin) executeColorRouletteSolo(game *unoGame, targetIsPlayer bool, chosenColor unoColor) []unoCard {
|
||||||
|
var flipped []unoCard
|
||||||
|
for {
|
||||||
|
if len(game.drawPile) == 0 {
|
||||||
|
game.reshuffleDiscard()
|
||||||
|
}
|
||||||
|
if len(game.drawPile) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
card := game.drawPile[0]
|
||||||
|
game.drawPile = game.drawPile[1:]
|
||||||
|
flipped = append(flipped, card)
|
||||||
|
if targetIsPlayer {
|
||||||
|
game.playerHand = append(game.playerHand, card)
|
||||||
|
} else {
|
||||||
|
game.botHand = append(game.botHand, card)
|
||||||
|
}
|
||||||
|
if card.Color == chosenColor {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flipped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnoPlugin) executeColorRouletteMulti(game *unoMultiGame, target *unoMultiPlayer, chosenColor unoColor) []unoCard {
|
||||||
|
var flipped []unoCard
|
||||||
|
for {
|
||||||
|
if len(game.drawPile) == 0 {
|
||||||
|
game.reshuffleDiscard()
|
||||||
|
}
|
||||||
|
if len(game.drawPile) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
card := game.drawPile[0]
|
||||||
|
game.drawPile = game.drawPile[1:]
|
||||||
|
flipped = append(flipped, card)
|
||||||
|
target.hand = append(target.hand, card)
|
||||||
|
if card.Color == chosenColor {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flipped
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 7-0 Rule helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// rotateHandsMulti rotates all active players' hands in the play direction.
|
||||||
|
func rotateHandsMulti(game *unoMultiGame) {
|
||||||
|
active := game.activePlayers()
|
||||||
|
if len(active) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ordered list of active players by turn order in current direction
|
||||||
|
n := len(game.players)
|
||||||
|
var ordered []*unoMultiPlayer
|
||||||
|
idx := game.currentIdx
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
p := game.players[idx]
|
||||||
|
if p.active {
|
||||||
|
ordered = append(ordered, p)
|
||||||
|
}
|
||||||
|
idx = (idx + game.direction + n) % n
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ordered) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all hands
|
||||||
|
hands := make([][]unoCard, len(ordered))
|
||||||
|
for i, p := range ordered {
|
||||||
|
hands[i] = p.hand
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate: each player gets the hand of the player behind them (opposite of direction)
|
||||||
|
for i := range ordered {
|
||||||
|
prev := (i - 1 + len(ordered)) % len(ordered)
|
||||||
|
ordered[i].hand = hands[prev]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swapHandsSolo swaps player and bot hands.
|
||||||
|
func swapHandsSolo(game *unoGame) {
|
||||||
|
game.playerHand, game.botHand = game.botHand, game.playerHand
|
||||||
|
}
|
||||||
|
|
||||||
|
// swapHandsMulti swaps two players' hands.
|
||||||
|
func swapHandsMulti(a, b *unoMultiPlayer) {
|
||||||
|
a.hand, b.hand = b.hand, a.hand
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Discard All helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// discardAllOfColor removes all cards of the given color from the hand.
|
||||||
|
// Returns the count of additional cards removed (not counting the played card).
|
||||||
|
func discardAllOfColor(hand *[]unoCard, color unoColor) int {
|
||||||
|
var kept []unoCard
|
||||||
|
removed := 0
|
||||||
|
for _, c := range *hand {
|
||||||
|
if c.Color == color {
|
||||||
|
removed++
|
||||||
|
} else {
|
||||||
|
kept = append(kept, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*hand = kept
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// No Mercy Bot AI
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// botPickCardNoMercy selects the best card for bot in No Mercy mode.
|
||||||
|
func botPickCardNoMercy(hand []unoCard, discardTop unoCard, topColor unoColor, bookDown bool, opponentMinCards int, stackMinValue int) (unoCard, int) {
|
||||||
|
// During stacking, only stackable cards are valid
|
||||||
|
if stackMinValue > 0 {
|
||||||
|
return botPickStackCard(hand, topColor, stackMinValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var playable []int
|
||||||
|
for i, c := range hand {
|
||||||
|
if c.canPlayOn(discardTop, topColor) {
|
||||||
|
playable = append(playable, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(playable) == 0 {
|
||||||
|
return unoCard{}, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if bookDown {
|
||||||
|
return botPickAggressiveNoMercy(hand, topColor, playable)
|
||||||
|
}
|
||||||
|
return botPickNormalNoMercy(hand, topColor, playable, opponentMinCards)
|
||||||
|
}
|
||||||
|
|
||||||
|
func botPickStackCard(hand []unoCard, topColor unoColor, stackMinValue int) (unoCard, int) {
|
||||||
|
var stackable []int
|
||||||
|
for i, c := range hand {
|
||||||
|
if c.canPlayOnStacking(topColor, stackMinValue) {
|
||||||
|
stackable = append(stackable, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(stackable) == 0 {
|
||||||
|
return unoCard{}, -1 // bot must absorb
|
||||||
|
}
|
||||||
|
// Play the lowest-value stackable card to preserve bigger weapons
|
||||||
|
bestIdx := stackable[0]
|
||||||
|
bestVal := cardDrawValue(hand[bestIdx].Value)
|
||||||
|
for _, i := range stackable[1:] {
|
||||||
|
dv := cardDrawValue(hand[i].Value)
|
||||||
|
if dv < bestVal {
|
||||||
|
bestIdx = i
|
||||||
|
bestVal = dv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hand[bestIdx], bestIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
func botPickNormalNoMercy(hand []unoCard, topColor unoColor, playable []int, opponentMinCards int) (unoCard, int) {
|
||||||
|
var draws, actions, numbers, discardAlls, skipEveryones, roulettes []int
|
||||||
|
|
||||||
|
for _, i := range playable {
|
||||||
|
c := hand[i]
|
||||||
|
switch {
|
||||||
|
case isDrawCard(c.Value):
|
||||||
|
draws = append(draws, i)
|
||||||
|
case c.Value == unoDiscardAll:
|
||||||
|
discardAlls = append(discardAlls, i)
|
||||||
|
case c.Value == unoSkipEveryone:
|
||||||
|
skipEveryones = append(skipEveryones, i)
|
||||||
|
case c.Value == unoWildColorRoulette:
|
||||||
|
roulettes = append(roulettes, i)
|
||||||
|
case c.Value.isAction():
|
||||||
|
actions = append(actions, i)
|
||||||
|
default:
|
||||||
|
numbers = append(numbers, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize Discard All if we have 3+ cards of that color
|
||||||
|
for _, i := range discardAlls {
|
||||||
|
color := hand[i].Color
|
||||||
|
count := 0
|
||||||
|
for _, c := range hand {
|
||||||
|
if c.Color == color {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count >= 3 {
|
||||||
|
return hand[i], i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If opponent close to winning, go aggressive with draw cards
|
||||||
|
if opponentMinCards <= 3 {
|
||||||
|
if len(draws) > 0 {
|
||||||
|
// Play highest draw value
|
||||||
|
bestIdx := draws[0]
|
||||||
|
bestVal := cardDrawValue(hand[bestIdx].Value)
|
||||||
|
for _, i := range draws[1:] {
|
||||||
|
dv := cardDrawValue(hand[i].Value)
|
||||||
|
if dv > bestVal {
|
||||||
|
bestIdx = i
|
||||||
|
bestVal = dv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hand[bestIdx], bestIdx
|
||||||
|
}
|
||||||
|
if len(skipEveryones) > 0 {
|
||||||
|
return hand[skipEveryones[0]], skipEveryones[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal priority: numbers > actions > discard all > skip everyone > draws > roulettes
|
||||||
|
if len(numbers) > 0 {
|
||||||
|
// Prefer color match
|
||||||
|
for _, i := range numbers {
|
||||||
|
if hand[i].Color == topColor {
|
||||||
|
return hand[i], i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hand[numbers[0]], numbers[0]
|
||||||
|
}
|
||||||
|
if len(actions) > 0 {
|
||||||
|
return hand[actions[0]], actions[0]
|
||||||
|
}
|
||||||
|
if len(discardAlls) > 0 {
|
||||||
|
return hand[discardAlls[0]], discardAlls[0]
|
||||||
|
}
|
||||||
|
if len(skipEveryones) > 0 {
|
||||||
|
return hand[skipEveryones[0]], skipEveryones[0]
|
||||||
|
}
|
||||||
|
if len(draws) > 0 {
|
||||||
|
// Play lowest draw to save bigger ones
|
||||||
|
bestIdx := draws[0]
|
||||||
|
bestVal := cardDrawValue(hand[bestIdx].Value)
|
||||||
|
for _, i := range draws[1:] {
|
||||||
|
dv := cardDrawValue(hand[i].Value)
|
||||||
|
if dv < bestVal {
|
||||||
|
bestIdx = i
|
||||||
|
bestVal = dv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hand[bestIdx], bestIdx
|
||||||
|
}
|
||||||
|
if len(roulettes) > 0 {
|
||||||
|
return hand[roulettes[0]], roulettes[0]
|
||||||
|
}
|
||||||
|
return hand[playable[0]], playable[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func botPickAggressiveNoMercy(hand []unoCard, topColor unoColor, playable []int) (unoCard, int) {
|
||||||
|
var draws, actions, others []int
|
||||||
|
|
||||||
|
for _, i := range playable {
|
||||||
|
c := hand[i]
|
||||||
|
switch {
|
||||||
|
case isDrawCard(c.Value):
|
||||||
|
draws = append(draws, i)
|
||||||
|
case c.Value.isAction():
|
||||||
|
actions = append(actions, i)
|
||||||
|
default:
|
||||||
|
others = append(others, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggressive: play highest draw card first
|
||||||
|
if len(draws) > 0 {
|
||||||
|
bestIdx := draws[0]
|
||||||
|
bestVal := cardDrawValue(hand[bestIdx].Value)
|
||||||
|
for _, i := range draws[1:] {
|
||||||
|
dv := cardDrawValue(hand[i].Value)
|
||||||
|
if dv > bestVal {
|
||||||
|
bestIdx = i
|
||||||
|
bestVal = dv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hand[bestIdx], bestIdx
|
||||||
|
}
|
||||||
|
if len(actions) > 0 {
|
||||||
|
return hand[actions[0]], actions[0]
|
||||||
|
}
|
||||||
|
if len(others) > 0 {
|
||||||
|
return hand[others[0]], others[0]
|
||||||
|
}
|
||||||
|
return hand[playable[0]], playable[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// botChooseSwapTarget picks the player with the fewest cards (to steal a small hand).
|
||||||
|
func botChooseSwapTarget(game *unoMultiGame, bot *unoMultiPlayer) *unoMultiPlayer {
|
||||||
|
var best *unoMultiPlayer
|
||||||
|
bestCards := 999
|
||||||
|
for _, p := range game.players {
|
||||||
|
if p == bot || !p.active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(p.hand) < bestCards {
|
||||||
|
bestCards = len(p.hand)
|
||||||
|
best = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
// botRouletteColor picks a color the bot has least of (to maximize damage).
|
||||||
|
func botRouletteColor(hand []unoCard) unoColor {
|
||||||
|
counts := map[unoColor]int{}
|
||||||
|
for _, c := range hand {
|
||||||
|
if c.Color != unoWild {
|
||||||
|
counts[c.Color]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pick color with fewest cards (opponent likely has fewer too)
|
||||||
|
best := unoRed
|
||||||
|
bestCount := 999
|
||||||
|
for _, color := range []unoColor{unoRed, unoBlue, unoYellow, unoGreen} {
|
||||||
|
if counts[color] < bestCount {
|
||||||
|
bestCount = counts[color]
|
||||||
|
best = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// No Mercy commentary
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var noMercyCommentary = map[string][]string{
|
||||||
|
"nomercy_start": {
|
||||||
|
"*GogoBee sets the book down. Not gently.*\n\n\"No mercy? Fine. 💛\"\n\n*deals cards with unsettling precision*",
|
||||||
|
"*GogoBee marks the page, closes the book with a snap.*\n\n\"Oh, you want to play *that* version. Okay. 💛\"\n\n*shuffles 168 cards without breaking eye contact*",
|
||||||
|
},
|
||||||
|
"mercy_kill": {
|
||||||
|
"\"Sometimes the kindest thing is the quickest. 💛\" *doesn't look up*",
|
||||||
|
"\"That's what mercy looks like. 💛\"",
|
||||||
|
"\"...and that's why they call it No Mercy. 💛\"",
|
||||||
|
},
|
||||||
|
"stack_absorbed": {
|
||||||
|
"\"That's a lot of cards. 💛\" *turns a page*",
|
||||||
|
"\"Ouch. 💛\"",
|
||||||
|
},
|
||||||
|
"color_roulette": {
|
||||||
|
"\"Let's see what fate has in store. 💛\"",
|
||||||
|
"\"Flip, flip, flip... 💛\" *watches with mild interest*",
|
||||||
|
},
|
||||||
|
"discard_all": {
|
||||||
|
"\"Oh, that's efficient. 💛\"",
|
||||||
|
},
|
||||||
|
"skip_everyone": {
|
||||||
|
"\"Nobody gets a turn. How fun. 💛\"",
|
||||||
|
},
|
||||||
|
"hand_swap": {
|
||||||
|
"\"Musical chairs, card edition. 💛\"",
|
||||||
|
"\"Surprise. 💛\"",
|
||||||
|
},
|
||||||
|
"hand_rotate": {
|
||||||
|
"\"Everyone pass your cards. Yes, all of them. 💛\"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickNoMercyCommentary(key string) string {
|
||||||
|
lines := noMercyCommentary[key]
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
line := lines[rand.IntN(len(lines))]
|
||||||
|
return strings.ReplaceAll(line, "GogoBee", unoBotName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// No Mercy mode flags parser
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// parseNoMercyFlags parses "nomercy [7-0] €amount" and returns noMercy, sevenZeroRule, and remaining amount string.
|
||||||
|
func parseNoMercyFlags(args string) (noMercy bool, sevenZeroRule bool, amountStr string) {
|
||||||
|
lower := strings.ToLower(strings.TrimSpace(args))
|
||||||
|
|
||||||
|
if !strings.HasPrefix(lower, "nomercy") {
|
||||||
|
return false, false, args
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := strings.TrimSpace(args[7:]) // len("nomercy") == 7
|
||||||
|
lowerRest := strings.ToLower(rest)
|
||||||
|
|
||||||
|
if strings.HasPrefix(lowerRest, "7-0") {
|
||||||
|
return true, true, strings.TrimSpace(rest[3:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, false, rest
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user