First commit, done on M6 from the PLAN.MD

This commit is contained in:
gauvainboiche
2026-10-25 17:34:44 +01:00
parent d5dc2a3901
commit 04193e2013
86 changed files with 8396 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
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")
}
}