Files
gogobee/internal/plugin/uno_test.go
prosolis 0d3485c7c2 Add market plugin, arena, holidays, UNO stacking fix, and multiple UX improvements
- Market plugin (#49): daily index snapshots (Yahoo Finance + Finnhub fallback),
  Ollama-generated summaries, !howsthemarket, !marketstatus, !marketreport commands,
  exchange hours with DST support, 30-min room cooldown
- Adventure arena: 5-tier combat gauntlet with 20 monsters, risk-reward cashout,
  death lockout changed from 24h to midnight UTC across all code and flavor text
- Adventure holidays: double daily actions on holidays
- Adventure DM fix: 15-minute response window prevents bare numbers from triggering
  adventure during UNO games
- Adventure scheduler: jitter between morning DMs to avoid Matrix rate limits
- Adventure revive: wire up adv_revived achievement on admin revive
- Column migration system for existing databases (holiday_action_taken)
- Fix italic markdown rendering after newlines
- Fix holdem DMs broken by space groups including DM rooms
- UNO No Mercy: remove stacking escalation rule (any draw card stacks on any other)
- UNO multiplayer: add turn announcements after stack absorption and turn passes,
  jitter between rapid-fire messages with safe mutex handling
- Birthday: add €1,000 gift and 10 XP + €100 community celebration bonus
- Market: guard against division by zero, nil pointer, and timezone errors
- README updates: plugin count 48→49, all new commands, feature flags, architecture

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

291 lines
8.4 KiB
Go
Raw 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())
}
}