package main import ( "regexp" "strings" "testing" ) var ( earlyEnds = []string{"revolution", "coup", "default", "impeachment"} metricIDs = []string{"order", "stability", "civilRights", "opinion", "economy", "hardPower", "softPower", "unrest", "armyLoyalty", "growth", "unemployment", "inflation", "debt", "score", "years"} tokenPattern = regexp.MustCompile(`\{(\w+)\}`) // Pronoun tokens each language knows how to fill (see i18n.js, tp()). allowedTokens = map[string]map[string]bool{ "en": set([]string{"sub", "Sub", "obj", "pos", "is", "has", "was", "country"}), "fr": set([]string{"sub", "Sub", "tonic", "e", "country"}), } ) func checkTokens(t *testing.T, where string, v any) { t.Helper() m, _ := v.(obj) for lang, raw := range m { s, ok := raw.(string) if !ok { continue } for _, match := range tokenPattern.FindAllStringSubmatch(s, -1) { if !allowedTokens[lang][match[1]] { t.Errorf("%s.%s: unknown token {%s}", where, lang, match[1]) } } } } func TestEndgame(t *testing.T) { eg := load(t, "data/endgame.json") bal := load(t, "data/balance.json") // Banners: the term plus one per way of losing. over := eg["over"].(obj) sameKeys(t, "endgame.over", over, append([]string{"term"}, earlyEnds...)) for k, v := range over { e := v.(obj) if !isLocalized(e["h"]) || !isLocalized(e["d"]) { t.Errorf("over.%s: h and d need non-empty en and fr", k) } } // Urgent telegrams: one per danger zone. alerts := eg["alerts"].(obj) sameKeys(t, "endgame.alerts", alerts, earlyEnds) for k, v := range alerts { a := v.(obj) if !isLocalized(a["title"]) || !isLocalized(a["body"]) { t.Errorf("alerts.%s: title and body need non-empty en and fr", k) } } // Historian titles. metrics := set(metricIDs) ids := map[string]bool{} perEnd := map[string]int{} titles := eg["titles"].([]any) var lastTerm obj for i, raw := range titles { ti := raw.(obj) id, _ := ti["id"].(string) if id == "" || ids[id] { t.Errorf("titles[%d]: missing or duplicate id %q", i, id) } ids[id] = true title, _ := ti["title"].(obj) if s, _ := title["en"].(string); strings.TrimSpace(s) == "" { t.Errorf("%s: title.en is empty", id) } fr, _ := title["fr"].(obj) for _, form := range []string{"he", "she", "they"} { if s, _ := fr[form].(string); strings.TrimSpace(s) == "" { t.Errorf("%s: title.fr.%s is empty", id, form) } } if !isLocalized(ti["verdict"]) { t.Errorf("%s: verdict needs non-empty en and fr", id) } checkTokens(t, id+".verdict", ti["verdict"]) cond, hasCond := ti["conditions"].(obj) for _, bound := range []string{"min", "max"} { m, _ := cond[bound].(obj) for k := range m { if !metrics[k] { t.Errorf("%s.conditions.%s: unknown metric %q", id, bound, k) } } } if end, ok := ti["over"].(string); ok { if !set(earlyEnds)[end] { t.Errorf("%s: unknown over %q", id, end) } perEnd[end]++ if hasCond { t.Errorf("%s: titles for an early end take no conditions", id) } } else { lastTerm = ti } } for _, end := range earlyEnds { if perEnd[end] != 1 { t.Errorf("early end %q needs exactly one title, has %d", end, perEnd[end]) } } if _, has := lastTerm["conditions"]; has || lastTerm == nil { t.Errorf("the last term title must have no conditions so every term gets one") } // Balance sections the review and the game-over rules rely on. g := bal["gameOver"].(obj) sameKeys(t, "balance.gameOver", g, []string{"default", "impeachment", "coup", "revolution"}) sc := bal["score"].(obj) valid := set(append(append([]string{}, indIDs...), "calm")) for k := range sc["weights"].(obj) { if !valid[k] { t.Errorf("score.weights: unknown key %q", k) } } grades := sc["grades"].([]any) prev := 101.0 for _, raw := range grades { pair := raw.([]any) min := pair[0].(float64) if min >= prev { t.Errorf("score.grades must be in strictly descending order of threshold") } prev = min } if prev != 0 { t.Errorf("the last grade must have threshold 0 so every score gets a grade") } }