Files
gogobee/internal/plugin/uno_test.go
prosolis b188d7036b Add UNO sudden death and multiplayer bot turn announcements
2-player UNO (especially No Mercy) regularly stalls past 100 turns.
Sudden death announces at turn 80 with a 20-turn countdown, then
scores hands by card point values — lowest total wins. Applies to
solo (vs bot) and multiplayer when 2 active players remain.

Also adds "It's X's turn" labels to bot plays in multiplayer,
matching the format human plays already had, and adds sudden death
checks to all turn advancement paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 09:27:00 -07:00

346 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
import "testing"
// ---------------------------------------------------------------------------
// Card properties
// ---------------------------------------------------------------------------
func TestUnoCard_IsWild(t *testing.T) {
tests := []struct {
name string
card unoCard
want bool
}{
{"wild", unoCard{unoWild, unoWildCard}, true},
{"wild draw four", unoCard{unoWild, unoWildDrawFour}, true},
{"wild draw six", unoCard{unoWild, unoWildDrawSix}, true},
{"wild draw ten", unoCard{unoWild, unoWildDrawTen}, true},
{"wild reverse draw 4", unoCard{unoWild, unoWildReverseDraw4}, true},
{"wild color roulette", unoCard{unoWild, unoWildColorRoulette}, true},
{"red skip", unoCard{unoRed, unoSkip}, false},
{"blue 5", unoCard{unoBlue, unoFive}, false},
{"green draw two", unoCard{unoGreen, unoDrawTwo}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.card.isWild(); got != tt.want {
t.Errorf("isWild() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnoValue_IsAction(t *testing.T) {
actions := []unoValue{unoSkip, unoReverse, unoDrawTwo, unoWildCard, unoWildDrawFour,
unoSkipEveryone, unoDrawFour, unoDiscardAll, unoWildReverseDraw4,
unoWildDrawSix, unoWildDrawTen, unoWildColorRoulette}
for _, v := range actions {
if !v.isAction() {
t.Errorf("%s should be an action card", v)
}
}
numbers := []unoValue{unoZero, unoOne, unoTwo, unoThree, unoFour, unoFive, unoSix, unoSeven, unoEight, unoNine}
for _, v := range numbers {
if v.isAction() {
t.Errorf("%s should NOT be an action card", v)
}
}
}
// ---------------------------------------------------------------------------
// canPlayOn
// ---------------------------------------------------------------------------
func TestCanPlayOn(t *testing.T) {
tests := []struct {
name string
card unoCard
top unoCard
topColor unoColor
want bool
}{
{"wild always playable", unoCard{unoWild, unoWildCard}, unoCard{unoRed, unoFive}, unoRed, true},
{"wd4 always playable", unoCard{unoWild, unoWildDrawFour}, unoCard{unoBlue, unoThree}, unoBlue, true},
{"same color", unoCard{unoRed, unoThree}, unoCard{unoRed, unoSeven}, unoRed, true},
{"same value diff color", unoCard{unoBlue, unoFive}, unoCard{unoRed, unoFive}, unoRed, true},
{"diff color diff value", unoCard{unoBlue, unoThree}, unoCard{unoRed, unoSeven}, unoRed, false},
{"matches chosen color not card color", unoCard{unoGreen, unoTwo}, unoCard{unoWild, unoWildCard}, unoGreen, true},
{"doesnt match chosen color", unoCard{unoRed, unoTwo}, unoCard{unoWild, unoWildCard}, unoGreen, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.card.canPlayOn(tt.top, tt.topColor); got != tt.want {
t.Errorf("canPlayOn() = %v, want %v", got, tt.want)
}
})
}
}
// ---------------------------------------------------------------------------
// Deck sizes
// ---------------------------------------------------------------------------
func TestNewUnoDeck_Size(t *testing.T) {
deck := newUnoDeck()
// Standard UNO: 4 colors × (1 zero + 2×12 other) + 8 wilds = 4×25 + 8 = 108
if len(deck) != 108 {
t.Errorf("standard deck size = %d, want 108", len(deck))
}
}
func TestNewNoMercyDeck_Size(t *testing.T) {
deck := newNoMercyDeck()
if len(deck) < 108 {
t.Errorf("no mercy deck should be larger than standard, got %d", len(deck))
}
}
// ---------------------------------------------------------------------------
// Bot AI
// ---------------------------------------------------------------------------
func TestBotPickCard_NoPlayable(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoRed, unoTwo},
}
top := unoCard{unoBlue, unoSeven}
_, idx := botPickCard(hand, top, unoBlue, false, 5)
if idx != -1 {
t.Errorf("should return -1 when nothing playable, got %d", idx)
}
}
func TestBotPickCard_PlaysPlayable(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoBlue, unoSeven}, // matches top
}
top := unoCard{unoBlue, unoFive}
card, idx := botPickCard(hand, top, unoBlue, false, 5)
if idx == -1 {
t.Fatal("should find a playable card")
}
if !card.canPlayOn(top, unoBlue) {
t.Error("picked card should be playable on top")
}
}
func TestBotPickCard_AggressiveUsesWD4(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoWild, unoWildDrawFour},
{unoBlue, unoSeven},
}
top := unoCard{unoBlue, unoFive}
// bookDown=true triggers aggressive mode
card, idx := botPickCard(hand, top, unoBlue, true, 5)
if idx == -1 {
t.Fatal("should find a playable card")
}
if card.Value != unoWildDrawFour {
t.Errorf("aggressive mode should prefer WD4, got %s", card.Value)
}
}
func TestBotPickColor_MostCommon(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoRed, unoTwo},
{unoRed, unoThree},
{unoBlue, unoFive},
{unoWild, unoWildCard}, // wilds don't count
}
color := botPickColor(hand)
if color != unoRed {
t.Errorf("should pick red (3 cards), got %s", color)
}
}
func TestBotPickColor_EmptyHand(t *testing.T) {
// Should default to red with no cards
color := botPickColor([]unoCard{})
if color != unoRed {
t.Errorf("empty hand should default to red, got %s", color)
}
}
// ---------------------------------------------------------------------------
// No Mercy helpers
// ---------------------------------------------------------------------------
func TestCardDrawValue(t *testing.T) {
tests := []struct {
value unoValue
want int
}{
{unoDrawTwo, 2},
{unoDrawFour, 4},
{unoWildReverseDraw4, 4},
{unoWildDrawSix, 6},
{unoWildDrawTen, 10},
{unoOne, 0},
{unoSkip, 0},
{unoWildCard, 0},
}
for _, tt := range tests {
if got := cardDrawValue(tt.value); got != tt.want {
t.Errorf("cardDrawValue(%s) = %d, want %d", tt.value, got, tt.want)
}
}
}
func TestIsDrawCard(t *testing.T) {
if !isDrawCard(unoDrawTwo) {
t.Error("Draw Two should be a draw card")
}
if !isDrawCard(unoWildDrawTen) {
t.Error("Wild Draw Ten should be a draw card")
}
if isDrawCard(unoSkip) {
t.Error("Skip should not be a draw card")
}
}
func TestCanPlayOnStacking(t *testing.T) {
tests := []struct {
name string
card unoCard
topColor unoColor
stackMinValue int
want bool
}{
{"wild draw six stacks on 2", unoCard{unoWild, unoWildDrawSix}, unoRed, 2, true},
{"draw two on draw two same color", unoCard{unoRed, unoDrawTwo}, unoRed, 2, true},
{"draw two wrong color", unoCard{unoBlue, unoDrawTwo}, unoRed, 2, false},
{"number cant stack", unoCard{unoRed, unoFive}, unoRed, 2, false},
{"draw two stacks on 4 same color", unoCard{unoRed, unoDrawTwo}, unoRed, 4, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.card.canPlayOnStacking(tt.topColor, tt.stackMinValue); got != tt.want {
t.Errorf("canPlayOnStacking() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasStackableCard(t *testing.T) {
hand := []unoCard{
{unoRed, unoFive},
{unoBlue, unoDrawTwo},
{unoGreen, unoSkip},
}
if hasStackableCard(hand, unoBlue, 2) != true {
t.Error("should find blue draw two as stackable")
}
if hasStackableCard(hand, unoRed, 2) != false {
t.Error("no red draw cards in hand")
}
}
func TestParseNoMercyFlags(t *testing.T) {
tests := []struct {
input string
noMercy bool
sevenZero bool
amount string
}{
{"nomercy", true, false, ""},
{"nomercy 50", true, false, "50"},
{"nomercy7-0", true, true, ""},
{"nomercy7-0 100", true, true, "100"},
{"50", false, false, "50"},
{"", false, false, ""},
{"NOMERCY", true, false, ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
nm, sz, amt := parseNoMercyFlags(tt.input)
if nm != tt.noMercy || sz != tt.sevenZero || amt != tt.amount {
t.Errorf("parseNoMercyFlags(%q) = (%v, %v, %q), want (%v, %v, %q)",
tt.input, nm, sz, amt, tt.noMercy, tt.sevenZero, tt.amount)
}
})
}
}
// ---------------------------------------------------------------------------
// Color/Value string representations
// ---------------------------------------------------------------------------
func TestUnoColor_String(t *testing.T) {
if unoRed.String() != "Red" {
t.Errorf("red: got %q", unoRed.String())
}
if unoWild.String() != "Wild" {
t.Errorf("wild: got %q", unoWild.String())
}
}
func TestUnoColor_Emoji(t *testing.T) {
if unoRed.Emoji() != "🟥" {
t.Errorf("red emoji: got %q", unoRed.Emoji())
}
if unoBlue.Emoji() != "🟦" {
t.Errorf("blue emoji: got %q", unoBlue.Emoji())
}
}
// ---------------------------------------------------------------------------
// Card point values (sudden death scoring)
// ---------------------------------------------------------------------------
func TestCardPointValue(t *testing.T) {
tests := []struct {
name string
card unoCard
want int
}{
{"zero", unoCard{unoRed, unoZero}, 0},
{"five", unoCard{unoBlue, unoFive}, 5},
{"nine", unoCard{unoGreen, unoNine}, 9},
{"skip", unoCard{unoRed, unoSkip}, 20},
{"reverse", unoCard{unoBlue, unoReverse}, 20},
{"draw two", unoCard{unoYellow, unoDrawTwo}, 20},
{"skip everyone", unoCard{unoRed, unoSkipEveryone}, 30},
{"draw four colored", unoCard{unoGreen, unoDrawFour}, 30},
{"discard all", unoCard{unoBlue, unoDiscardAll}, 30},
{"wild", unoCard{unoWild, unoWildCard}, 50},
{"wild draw four", unoCard{unoWild, unoWildDrawFour}, 50},
{"wild reverse draw 4", unoCard{unoWild, unoWildReverseDraw4}, 60},
{"wild draw six", unoCard{unoWild, unoWildDrawSix}, 60},
{"wild color roulette", unoCard{unoWild, unoWildColorRoulette}, 60},
{"wild draw ten", unoCard{unoWild, unoWildDrawTen}, 75},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cardPointValue(tt.card); got != tt.want {
t.Errorf("cardPointValue(%v) = %d, want %d", tt.card, got, tt.want)
}
})
}
}
func TestScoreHand(t *testing.T) {
hand := []unoCard{
{unoRed, unoFive}, // 5
{unoBlue, unoNine}, // 9
{unoGreen, unoSkip}, // 20
{unoWild, unoWildCard}, // 50
}
got := scoreHand(hand)
want := 84
if got != want {
t.Errorf("scoreHand() = %d, want %d", got, want)
}
}
func TestScoreHandEmpty(t *testing.T) {
if got := scoreHand(nil); got != 0 {
t.Errorf("scoreHand(nil) = %d, want 0", got)
}
}