mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Add Wild Draw Four challenge mechanic and bot bluffing
- When a WD4 is played, the victim can challenge or accept - Challenge succeeds if the WD4 player had a card matching the previous color — they draw 4 instead - Challenge fails: victim draws 6 (4 + 2 penalty) - Bot as victim: challenges probabilistically based on opponent hand size (10-70% chance, more cards = more likely to challenge) - Bot as player: 20% chance to illegally play WD4 when it has matching color cards, making challenges meaningful against it - Auto-play defaults to accepting (safe option) - Challenge phase integrates with existing turn timer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1030,6 +1030,13 @@ func botPickNormal(hand []unoCard, topColor unoColor, playable []int, opponentMi
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes play WD4 even when we have other options (20% chance)
|
||||
// This makes the challenge mechanic meaningful against the bot.
|
||||
if len(wd4s) > 0 && (len(actions) > 0 || len(numbers) > 0) && rand.IntN(5) == 0 {
|
||||
idx := wd4s[0]
|
||||
return hand[idx], idx
|
||||
}
|
||||
|
||||
// Save WD4 unless opponent is close to winning
|
||||
if opponentMinCards > 3 {
|
||||
if len(actions) > 0 {
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
unoMultiPhasePlay unoMultiPhase = iota
|
||||
unoMultiPhaseChooseColor // active player must pick a color
|
||||
unoMultiPhaseDrawnPlayable // active player drew a playable card, yes/no
|
||||
unoMultiPhaseChallenge // next player may challenge a Wild Draw Four
|
||||
)
|
||||
|
||||
type unoMultiPlayer struct {
|
||||
@@ -48,6 +49,11 @@ type unoMultiGame struct {
|
||||
phase unoMultiPhase
|
||||
drawnCard *unoCard // card drawn this turn
|
||||
pendingCard *unoCard // wild waiting for color
|
||||
|
||||
// Wild Draw Four challenge state
|
||||
wd4Player *unoMultiPlayer // who played the WD4
|
||||
wd4Victim *unoMultiPlayer // who can challenge
|
||||
wd4PrevColor unoColor // color before the wild was played
|
||||
turns int
|
||||
turnID int // monotonic, used to invalidate stale timers
|
||||
startedAt time.Time
|
||||
@@ -717,6 +723,9 @@ func (p *UnoPlugin) handleMultiDMInput(ctx MessageContext, game *unoMultiGame) e
|
||||
}
|
||||
|
||||
switch game.phase {
|
||||
case unoMultiPhaseChallenge:
|
||||
return p.handleMultiChallengeInput(game, player, input)
|
||||
|
||||
case unoMultiPhaseChooseColor:
|
||||
return p.handleMultiColorChoice(game, player, input)
|
||||
|
||||
@@ -748,6 +757,20 @@ func (p *UnoPlugin) handleMultiDMInput(ctx MessageContext, game *unoMultiGame) e
|
||||
// Play handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (p *UnoPlugin) handleMultiChallengeInput(game *unoMultiGame, player *unoMultiPlayer, input string) error {
|
||||
switch input {
|
||||
case "challenge", "c":
|
||||
name := p.unoDisplayName(player.userID)
|
||||
p.SendMessage(game.roomID, fmt.Sprintf("⚡ %s challenges the Wild Draw Four!", name))
|
||||
p.resolveWD4Challenge(game, true)
|
||||
case "accept", "a":
|
||||
p.resolveWD4Challenge(game, false)
|
||||
default:
|
||||
p.SendMessage(player.dmRoomID, "Type **challenge** or **accept**.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UnoPlugin) handleMultiPlayerPlay(game *unoMultiGame, player *unoMultiPlayer, idx int) error {
|
||||
card := player.hand[idx]
|
||||
|
||||
@@ -784,6 +807,9 @@ func (p *UnoPlugin) handleMultiPlayerPlay(game *unoMultiGame, player *unoMultiPl
|
||||
// Wild — need color choice
|
||||
if card.Value == unoWildCard || card.Value == unoWildDrawFour {
|
||||
game.pendingCard = &card
|
||||
if card.Value == unoWildDrawFour {
|
||||
game.wd4PrevColor = game.topColor
|
||||
}
|
||||
game.phase = unoMultiPhaseChooseColor
|
||||
p.SendMessage(player.dmRoomID,
|
||||
fmt.Sprintf("You played **%s**! Choose a color:\n1. 🟥 Red\n2. 🟦 Blue\n3. 🟨 Yellow\n4. 🟩 Green", card.Value))
|
||||
@@ -906,6 +932,9 @@ func (p *UnoPlugin) handleMultiDrawnPlayable(game *unoMultiGame, player *unoMult
|
||||
|
||||
if drawnCard.Value == unoWildCard || drawnCard.Value == unoWildDrawFour {
|
||||
game.pendingCard = &drawnCard
|
||||
if drawnCard.Value == unoWildDrawFour {
|
||||
game.wd4PrevColor = game.topColor
|
||||
}
|
||||
game.phase = unoMultiPhaseChooseColor
|
||||
p.SendMessage(player.dmRoomID,
|
||||
fmt.Sprintf("You played **%s**! Choose a color:\n1. 🟥 Red\n2. 🟦 Blue\n3. 🟨 Yellow\n4. 🟩 Green", drawnCard.Value))
|
||||
@@ -935,6 +964,7 @@ type cardEffectResult struct {
|
||||
skippedName string // non-empty if a player was skipped
|
||||
reversed bool // true if direction was reversed
|
||||
drawnCount int // cards drawn by the victim (2 or 4)
|
||||
needsChallenge bool // true if WD4 challenge phase should start
|
||||
}
|
||||
|
||||
// applyCardEffects applies skip/reverse/draw effects and advances the turn.
|
||||
@@ -979,13 +1009,8 @@ func (p *UnoPlugin) applyCardEffects(game *unoMultiGame, card unoCard) cardEffec
|
||||
game.turnID++
|
||||
|
||||
case unoWildDrawFour:
|
||||
drawn := game.draw(4)
|
||||
nextPlayer.hand = append(nextPlayer.hand, drawn...)
|
||||
result.needsChallenge = true
|
||||
result.skippedName = nextName
|
||||
result.drawnCount = 4
|
||||
game.currentIdx = nextIdx
|
||||
game.currentIdx = game.nextActiveIdx()
|
||||
game.turnID++
|
||||
|
||||
default:
|
||||
game.currentIdx = game.nextActiveIdx()
|
||||
@@ -997,7 +1022,9 @@ func (p *UnoPlugin) applyCardEffects(game *unoMultiGame, card unoCard) cardEffec
|
||||
|
||||
// writeEffectLines appends human-readable effect descriptions to a string builder.
|
||||
func writeEffectLines(sb *strings.Builder, eff cardEffectResult) {
|
||||
if eff.drawnCount > 0 {
|
||||
if eff.needsChallenge {
|
||||
sb.WriteString(fmt.Sprintf("\n %s may challenge! ⚡", eff.skippedName))
|
||||
} else if eff.drawnCount > 0 {
|
||||
sb.WriteString(fmt.Sprintf("\n %s draws %d and is skipped!", eff.skippedName, eff.drawnCount))
|
||||
} else if eff.skippedName != "" && eff.reversed {
|
||||
sb.WriteString(fmt.Sprintf("\n %s is skipped! (reverse)", eff.skippedName))
|
||||
@@ -1017,6 +1044,18 @@ func (p *UnoPlugin) applyAndAnnounce(game *unoMultiGame, player *unoMultiPlayer,
|
||||
eff := p.applyCardEffects(game, card)
|
||||
writeEffectLines(&roomMsg, eff)
|
||||
|
||||
// UNO announcement
|
||||
if len(player.hand) == 1 && player.calledUno {
|
||||
roomMsg.WriteString(fmt.Sprintf("\n %s calls UNO! 🔥", name))
|
||||
}
|
||||
|
||||
// Wild Draw Four — enter challenge phase
|
||||
if eff.needsChallenge {
|
||||
p.SendMessage(game.roomID, roomMsg.String())
|
||||
p.startWD4Challenge(game, player)
|
||||
return
|
||||
}
|
||||
|
||||
// Book state commentary (occasionally)
|
||||
if game.turns%4 == 0 {
|
||||
if changed := game.updateBookState(); changed {
|
||||
@@ -1028,11 +1067,6 @@ func (p *UnoPlugin) applyAndAnnounce(game *unoMultiGame, player *unoMultiPlayer,
|
||||
}
|
||||
}
|
||||
|
||||
// UNO announcement
|
||||
if len(player.hand) == 1 && player.calledUno {
|
||||
roomMsg.WriteString(fmt.Sprintf("\n %s calls UNO! 🔥", name))
|
||||
}
|
||||
|
||||
// Next player
|
||||
nextUp := game.currentPlayer()
|
||||
nextUpName := p.unoDisplayName(nextUp.userID)
|
||||
@@ -1045,6 +1079,127 @@ func (p *UnoPlugin) applyAndAnnounce(game *unoMultiGame, player *unoMultiPlayer,
|
||||
p.executeMultiTurn(game)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wild Draw Four challenge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// startWD4Challenge enters the challenge phase. The victim can challenge or accept.
|
||||
// Caller must hold game.mu.
|
||||
func (p *UnoPlugin) startWD4Challenge(game *unoMultiGame, wd4Player *unoMultiPlayer) {
|
||||
nextIdx := game.nextActiveIdx()
|
||||
victim := game.players[nextIdx]
|
||||
|
||||
game.wd4Player = wd4Player
|
||||
game.wd4Victim = victim
|
||||
game.phase = unoMultiPhaseChallenge
|
||||
game.currentIdx = nextIdx
|
||||
game.turnID++
|
||||
|
||||
playerName := p.unoDisplayName(wd4Player.userID)
|
||||
if wd4Player.isBot {
|
||||
playerName = unoBotName()
|
||||
}
|
||||
|
||||
if victim.isBot {
|
||||
// Bot decides whether to challenge
|
||||
p.botHandleWD4Challenge(game)
|
||||
return
|
||||
}
|
||||
|
||||
p.SendMessage(victim.dmRoomID,
|
||||
fmt.Sprintf("⚡ **%s** played Wild Draw Four!\nYou can **challenge** — if they had a %s %s card, they draw 4 instead.\nIf the challenge fails, you draw 6.\n\nType **challenge** or **accept**.",
|
||||
playerName, game.wd4PrevColor.Emoji(), game.wd4PrevColor))
|
||||
p.startMultiAutoPlayTimer(game)
|
||||
}
|
||||
|
||||
// resolveWD4Challenge resolves the challenge. Caller must hold game.mu.
|
||||
func (p *UnoPlugin) resolveWD4Challenge(game *unoMultiGame, challenged bool) {
|
||||
wd4Player := game.wd4Player
|
||||
victim := game.wd4Victim
|
||||
wd4Name := p.unoDisplayName(wd4Player.userID)
|
||||
if wd4Player.isBot {
|
||||
wd4Name = unoBotName()
|
||||
}
|
||||
victimName := p.unoDisplayName(victim.userID)
|
||||
if victim.isBot {
|
||||
victimName = unoBotName()
|
||||
}
|
||||
|
||||
// Clear challenge state
|
||||
game.wd4Player = nil
|
||||
game.wd4Victim = nil
|
||||
game.phase = unoMultiPhasePlay
|
||||
|
||||
if !challenged {
|
||||
// Victim accepts — draw 4, get skipped
|
||||
drawn := game.draw(4)
|
||||
victim.hand = append(victim.hand, drawn...)
|
||||
p.SendMessage(game.roomID,
|
||||
fmt.Sprintf("🃏 %s accepts the Wild Draw Four. Draws 4 and is skipped!", victimName))
|
||||
game.currentIdx = game.nextActiveIdx()
|
||||
game.turnID++
|
||||
p.executeMultiTurn(game)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the WD4 player had a card matching the previous color
|
||||
hadMatch := false
|
||||
for _, c := range wd4Player.hand {
|
||||
if c.Color == game.wd4PrevColor {
|
||||
hadMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hadMatch {
|
||||
// Challenge succeeds — WD4 player drew illegally, they draw 4
|
||||
drawn := game.draw(4)
|
||||
wd4Player.hand = append(wd4Player.hand, drawn...)
|
||||
p.SendMessage(game.roomID,
|
||||
fmt.Sprintf("⚡ **Challenge successful!** %s had a %s %s card. %s draws 4!",
|
||||
wd4Name, game.wd4PrevColor.Emoji(), game.wd4PrevColor, wd4Name))
|
||||
// Victim is NOT skipped — turn continues from victim
|
||||
game.turnID++
|
||||
p.executeMultiTurn(game)
|
||||
} else {
|
||||
// Challenge fails — victim draws 6 (4 + 2 penalty)
|
||||
drawn := game.draw(6)
|
||||
victim.hand = append(victim.hand, drawn...)
|
||||
p.SendMessage(game.roomID,
|
||||
fmt.Sprintf("⚡ **Challenge failed!** %s played legally. %s draws 6!",
|
||||
wd4Name, victimName))
|
||||
// Victim is skipped
|
||||
game.currentIdx = game.nextActiveIdx()
|
||||
game.turnID++
|
||||
p.executeMultiTurn(game)
|
||||
}
|
||||
}
|
||||
|
||||
// botHandleWD4Challenge decides whether the bot challenges a WD4. Caller must hold game.mu.
|
||||
func (p *UnoPlugin) botHandleWD4Challenge(game *unoMultiGame) {
|
||||
// More cards = more likely they had a matching color = more likely to challenge.
|
||||
// 1-3 cards: 10%, 4-5: 30%, 6-7: 50%, 8+: 70%
|
||||
cards := len(game.wd4Player.hand)
|
||||
threshold := 10
|
||||
if cards >= 8 {
|
||||
threshold = 70
|
||||
} else if cards >= 6 {
|
||||
threshold = 50
|
||||
} else if cards >= 4 {
|
||||
threshold = 30
|
||||
}
|
||||
challenged := rand.IntN(100) < threshold
|
||||
bn := unoBotName()
|
||||
|
||||
if challenged {
|
||||
p.SendMessage(game.roomID, fmt.Sprintf("⚡ %s challenges the Wild Draw Four!", bn))
|
||||
} else {
|
||||
p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s accepts the Wild Draw Four.", bn))
|
||||
}
|
||||
|
||||
p.resolveWD4Challenge(game, challenged)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bot turn (multiplayer)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1104,6 +1259,9 @@ func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
|
||||
|
||||
// Wild color choice
|
||||
if card.Value == unoWildCard || card.Value == unoWildDrawFour {
|
||||
if card.Value == unoWildDrawFour {
|
||||
game.wd4PrevColor = game.topColor
|
||||
}
|
||||
game.topColor = botPickColor(bot.hand)
|
||||
} else {
|
||||
game.topColor = card.Color
|
||||
@@ -1149,6 +1307,17 @@ func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
|
||||
eff := p.applyCardEffects(game, card)
|
||||
writeEffectLines(roomBuf, eff)
|
||||
|
||||
// WD4 challenge — flush buffer and enter challenge phase
|
||||
if eff.needsChallenge {
|
||||
if roomBuf.Len() > 0 {
|
||||
p.SendMessage(game.roomID, roomBuf.String())
|
||||
roomBuf.Reset()
|
||||
}
|
||||
game.turns++
|
||||
p.startWD4Challenge(game, bot)
|
||||
return
|
||||
}
|
||||
|
||||
game.turns++
|
||||
}
|
||||
|
||||
@@ -1249,6 +1418,13 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
||||
name := p.unoDisplayName(player.userID)
|
||||
|
||||
switch game.phase {
|
||||
case unoMultiPhaseChallenge:
|
||||
// Auto-play: accept the WD4 (safe default)
|
||||
p.SendMessage(player.dmRoomID, "*Auto-played:* Accepted Wild Draw Four.")
|
||||
p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Accepts the Wild Draw Four.", name))
|
||||
p.resolveWD4Challenge(game, false)
|
||||
return
|
||||
|
||||
case unoMultiPhaseChooseColor:
|
||||
// Auto-pick most common color
|
||||
color := botPickColor(player.hand)
|
||||
@@ -1287,6 +1463,9 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
||||
game.turns++
|
||||
|
||||
if drawnCard.Value == unoWildCard || drawnCard.Value == unoWildDrawFour {
|
||||
if drawnCard.Value == unoWildDrawFour {
|
||||
game.wd4PrevColor = game.topColor
|
||||
}
|
||||
color := botPickColor(player.hand)
|
||||
game.topColor = color
|
||||
p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* %s (chose %s %s).", drawnCard.Display(), color.Emoji(), color))
|
||||
@@ -1343,6 +1522,9 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
||||
game.turns++
|
||||
|
||||
if card.Value == unoWildCard || card.Value == unoWildDrawFour {
|
||||
if card.Value == unoWildDrawFour {
|
||||
game.wd4PrevColor = game.topColor
|
||||
}
|
||||
color := botPickColor(player.hand)
|
||||
game.topColor = color
|
||||
p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* %s (chose %s %s).", card.Display(), color.Emoji(), color))
|
||||
@@ -1398,6 +1580,9 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
||||
game.turns++
|
||||
|
||||
if card.Value == unoWildCard || card.Value == unoWildDrawFour {
|
||||
if card.Value == unoWildDrawFour {
|
||||
game.wd4PrevColor = game.topColor
|
||||
}
|
||||
color := botPickColor(player.hand)
|
||||
game.topColor = color
|
||||
p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Drew and played %s (chose %s %s).", card.Display(), color.Emoji(), color))
|
||||
@@ -1425,8 +1610,12 @@ func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer
|
||||
}
|
||||
|
||||
// applyAutoEffects applies card effects after an auto-play and advances the turn.
|
||||
func (p *UnoPlugin) applyAutoEffects(game *unoMultiGame, _ *unoMultiPlayer, card unoCard) {
|
||||
p.applyCardEffects(game, card)
|
||||
func (p *UnoPlugin) applyAutoEffects(game *unoMultiGame, player *unoMultiPlayer, card unoCard) {
|
||||
eff := p.applyCardEffects(game, card)
|
||||
if eff.needsChallenge {
|
||||
p.startWD4Challenge(game, player)
|
||||
return
|
||||
}
|
||||
p.executeMultiTurn(game)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user