Files
gogobee/internal/plugin/adventure_holidays_test.go
prosolis dfa7beeb96 Adv 2.0 D&D polish: GM→DM rename, streamed zone combat, end-to-end scenario test
GM→DM rename across docs and code (GMNarrationType→DMNarrationType,
GMState→DMState, narration constants, comments) so the system reads as
"Dungeon Master" everywhere. Player-visible "GM mood" wording stays
where it appears in flavor.

Streamed zone/expedition combat: zone advance now stages patrol →
patrol play-by-play → patrol resolution → room intro → room play-by-play
→ final outcome through sendZoneCombatMessages with 2–3s pacing
(arena keeps its 5–8s window). Combat narrative lines pick up a compact
d20-vs-AC roll annotation for hit/crit/miss/block events.

Combat outcome polish: dndHPSnapshot lets narration show sheet HP
rather than legacy combat-engine HP, and markAdventureDead clears the
zombie state where hp_current was 0 but the legacy alive flag stayed
true after a D&D-layer KO.

Adv 2.0 announcement (ADVENTURE_2.0_ANNOUNCEMENT.md), README rewrite
covering the new layer, and adv2_scenario_test.go — a full
zone-run + expedition + harvest playthrough against a copy of the prod
DB asserting persisted state end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00

307 lines
8.2 KiB
Go

package plugin
import (
"strings"
"testing"
"time"
)
func TestEasterDate(t *testing.T) {
tests := []struct {
year int
month time.Month
day int
}{
{2024, time.March, 31},
{2025, time.April, 20},
{2026, time.April, 5},
{2027, time.March, 28},
{2028, time.April, 16},
}
for _, tc := range tests {
got := easterDate(tc.year)
want := time.Date(tc.year, tc.month, tc.day, 0, 0, 0, 0, time.UTC)
if !got.Equal(want) {
t.Errorf("Easter %d: got %s, want %s", tc.year, got.Format("2006-01-02"), want.Format("2006-01-02"))
}
}
}
func TestNthMonday(t *testing.T) {
tests := []struct {
desc string
year int
month time.Month
n int
want string
}{
{"Family Day 2026", 2026, time.February, 3, "2026-02-16"},
{"Labour Day 2026", 2026, time.September, 1, "2026-09-07"},
{"Thanksgiving CA 2026", 2026, time.October, 2, "2026-10-12"},
{"Family Day 2027", 2027, time.February, 3, "2027-02-15"},
}
for _, tc := range tests {
got := nthMonday(tc.year, tc.month, tc.n)
if got.Format("2006-01-02") != tc.want {
t.Errorf("%s: got %s, want %s", tc.desc, got.Format("2006-01-02"), tc.want)
}
}
}
func TestMondayBeforeMay25(t *testing.T) {
tests := []struct {
year int
want string
}{
{2026, "2026-05-18"},
{2027, "2027-05-24"},
{2028, "2028-05-22"},
}
for _, tc := range tests {
got := mondayBeforeMay25(tc.year)
if got.Format("2006-01-02") != tc.want {
t.Errorf("Victoria Day %d: got %s, want %s", tc.year, got.Format("2006-01-02"), tc.want)
}
}
}
// ── Hebrew Calendar Tests ───────────────────────────────────────────────────
func TestHebrewRoshHashanah(t *testing.T) {
// Verified against hebcal.com
tests := []struct {
year int
want string
}{
{2023, "2023-09-16"}, // 5784
{2024, "2024-10-03"}, // 5785
{2025, "2025-09-23"}, // 5786
{2026, "2026-09-12"}, // 5787
{2027, "2027-10-02"}, // 5788
{2028, "2028-09-21"}, // 5789
{2030, "2030-09-28"}, // 5791
}
for _, tc := range tests {
got := hebrewRoshHashanah(tc.year)
if got.Format("2006-01-02") != tc.want {
t.Errorf("Rosh Hashanah %d: got %s, want %s", tc.year, got.Format("2006-01-02"), tc.want)
}
}
}
func TestHebrewYomKippur(t *testing.T) {
tests := []struct {
year int
want string
}{
{2026, "2026-09-21"},
{2027, "2027-10-11"},
}
for _, tc := range tests {
got := hebrewYomKippur(tc.year)
if got.Format("2006-01-02") != tc.want {
t.Errorf("Yom Kippur %d: got %s, want %s", tc.year, got.Format("2006-01-02"), tc.want)
}
}
}
func TestHebrewPassover(t *testing.T) {
// 15 Nisan (first full day of Pesach)
tests := []struct {
year int
want string
}{
{2024, "2024-04-23"},
{2025, "2025-04-13"},
{2026, "2026-04-02"},
{2027, "2027-04-22"},
}
for _, tc := range tests {
got := hebrewPassover(tc.year)
if got.Format("2006-01-02") != tc.want {
t.Errorf("Passover %d: got %s, want %s", tc.year, got.Format("2006-01-02"), tc.want)
}
}
}
func TestHebrewHanukkah(t *testing.T) {
// 25 Kislev (first full day)
tests := []struct {
year int
want string
}{
{2024, "2024-12-26"},
{2025, "2025-12-15"},
{2026, "2026-12-05"},
{2027, "2027-12-25"},
}
for _, tc := range tests {
got := hebrewHanukkah(tc.year)
if got.Format("2006-01-02") != tc.want {
t.Errorf("Hanukkah %d: got %s, want %s", tc.year, got.Format("2006-01-02"), tc.want)
}
}
}
// ── Tabular Islamic Calendar Tests ──────────────────────────────────────────
func TestIslamicEidAlFitr(t *testing.T) {
// Tabular Islamic calendar — ±1 day from observed
tests := []struct {
year int
want string
}{
{2026, "2026-03-20"},
{2027, "2027-03-10"},
}
for _, tc := range tests {
dates := tabularIslamicDates(tc.year, 10, 1)
if len(dates) == 0 {
t.Errorf("Eid al-Fitr %d: no dates returned", tc.year)
continue
}
got := dates[0].Format("2006-01-02")
if got != tc.want {
t.Errorf("Eid al-Fitr %d: got %s, want %s", tc.year, got, tc.want)
}
}
}
func TestIslamicEidAlAdha(t *testing.T) {
tests := []struct {
year int
want string
}{
{2026, "2026-05-27"},
{2027, "2027-05-17"}, // Tabular Islamic calendar ±1 day from observed
}
for _, tc := range tests {
dates := tabularIslamicDates(tc.year, 12, 10)
if len(dates) == 0 {
t.Errorf("Eid al-Adha %d: no dates returned", tc.year)
continue
}
got := dates[0].Format("2006-01-02")
if got != tc.want {
t.Errorf("Eid al-Adha %d: got %s, want %s", tc.year, got, tc.want)
}
}
}
// ── Integration Tests ───────────────────────────────────────────────────────
func TestFloatingHolidays_AllComputed(t *testing.T) {
for year := 2024; year <= 2035; year++ {
holidays := floatingHolidays(year)
if len(holidays) < 14 { // at least: 4 Easter + Nowruz + 4 Canadian + 4 Hebrew + 2 Islamic
t.Errorf("year %d: only %d floating holidays, expected at least 14", year, len(holidays))
}
// Verify no zero-time dates (would indicate a computation failure)
for _, h := range holidays {
if h.Date.IsZero() {
t.Errorf("year %d: holiday %q has zero date", year, h.Name)
}
if h.Date.Year() != year {
t.Errorf("year %d: holiday %q falls in %d", year, h.Name, h.Date.Year())
}
}
}
}
func TestFloatingHolidays_DateCollisions(t *testing.T) {
holidays := floatingHolidays(2026)
seen := make(map[string][]string)
for _, h := range holidays {
key := h.Date.Format("2006-01-02")
seen[key] = append(seen[key], h.Name)
}
for date, names := range seen {
if len(names) > 1 {
t.Logf("NOTE: %d holidays on %s: %s", len(names), date, strings.Join(names, ", "))
}
}
}
func TestFixedHolidays_Christmas(t *testing.T) {
found := false
for _, h := range majorHolidays {
if h.Month == 12 && h.Day == 25 {
found = true
break
}
}
if !found {
t.Error("Christmas not in majorHolidays")
}
}
func TestFixedHolidays_NoDuplicateMonthDay(t *testing.T) {
seen := make(map[[2]int]string)
for _, h := range majorHolidays {
key := [2]int{h.Month, h.Day}
if prev, ok := seen[key]; ok {
t.Errorf("duplicate fixed holiday on %d/%d: %s and %s", h.Month, h.Day, prev, h.Name)
}
seen[key] = h.Name
}
}
func TestDailySummary_HolidayBlock(t *testing.T) {
players := []AdvPlayerDaySummary{
{DisplayName: "Alice", Activity: "dungeon", Location: "Cellar", Outcome: "success", LootValue: 500},
{DisplayName: "Bob", Activity: "mine", Location: "Quarry", Outcome: "success", LootValue: 300},
{DisplayName: "Carol", IsDead: true, DeadUntil: "14:00 UTC", Activity: "dungeon", Location: "Cave", Outcome: "death"},
}
text := renderAdvDailySummary("2026-12-25", nil, TwinBeeRewardSummary{}, players, "Christmas")
if !strings.Contains(text, "Christmas") {
t.Error("summary should contain holiday name")
}
// Adv 2.0 — holiday recap now advertises the +5 mood / free pack /
// +1 yield perks instead of the retired double-action mechanic.
if !strings.Contains(text, "TwinBee's blessing") {
t.Error("summary should mention TwinBee's blessing on holidays")
}
if !strings.Contains(text, "free standard pack") {
t.Error("summary should mention the complimentary standard pack")
}
// Without holiday
textNoHol := renderAdvDailySummary("2026-12-26", nil, TwinBeeRewardSummary{}, players, "")
if strings.Contains(textNoHol, "TwinBee's blessing") {
t.Error("non-holiday summary should NOT mention TwinBee's blessing")
}
}
// ── JDN Roundtrip Test ──────────────────────────────────────────────────────
func TestGregorianFromJDN_Roundtrip(t *testing.T) {
// Verify known JDN ↔ Gregorian conversions
tests := []struct {
jdn int
date string
}{
{2451545, "2000-01-01"}, // J2000.0
{2440588, "1970-01-01"}, // Unix epoch
{2299161, "1582-10-15"}, // Gregorian calendar adoption
}
for _, tc := range tests {
got := gregorianFromJDN(tc.jdn)
if got.Format("2006-01-02") != tc.date {
t.Errorf("JDN %d: got %s, want %s", tc.jdn, got.Format("2006-01-02"), tc.date)
}
}
}