M3: japanska skyltar – pixelfont, ordbok, kunskap, delvis översättning, hjälp
All checks were successful
release / build-release (push) Successful in 1m42s

- resources/text/fonts: PixelMplus 10/12 px (M+ FONT LICENSE) + licens;
  all text ritas via text/v2 på heltalspositioner, debugfonten är borta
- internal/lang: ordbok (60 ord, validerad), tokenisering med längsta
  matchning, Knowledge (ord + kana, nivå 0–5, hintar), Render (kända ord
  markerade/översatta, okända kvar, kana som romaji), EnglishShare/
  ShowEnglish per svårighet (deterministiskt per bana+position),
  HelpSeconds; datatest kräver att all japanska i banorna finns i ordboken
- sim.Run: AskHelp (H) med väntetid efter nivå, avbryts om man går,
  avslöjar skylten och lär första okända ordet; Knowledge delas mellan banor
- game: skylt- och avgångstavlor renderade genom kunskapen med färgkodning,
  engelsk rad på lätta banor, hjälp-förlopp, "Nytt ord"-notis, HUD visar
  resmålet på japanska från nivå 1 och japanska-nivå; meny med svårighet;
  pixelbaserad radbrytning av färgade textsegment; -know-flagga
- tools/shots: tre skyltbilder (skylt, hjälp, nytt ord) med förkunskap
- docs/README/wiki uppdaterade

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 20:14:36 +02:00
parent 0e10cbf123
commit b67493fddb
40 changed files with 1846 additions and 174 deletions

View File

@@ -0,0 +1,81 @@
package lang
import (
"os"
"path/filepath"
"strings"
"testing"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
)
func loadDict(t *testing.T) *Dictionary {
t.Helper()
b, err := res.ReadFile("text", "jp-dictionary.json")
if err != nil {
t.Fatal(err)
}
d, err := ParseDictionary(b)
if err != nil {
t.Fatal(err)
}
return d
}
// TestShippedDictionary: ordboken parsar och har rimligt innehåll.
func TestShippedDictionary(t *testing.T) {
d := loadDict(t)
if d.Len() < 20 {
t.Fatalf("bara %d ord", d.Len())
}
for _, w := range d.Words() {
e := d.Lookup(w)
if e.Tier < 1 || e.Tier > 5 {
t.Errorf("%s: tier %d", w, e.Tier)
}
}
}
// TestAllJapaneseInLevelsIsInDictionary: all japanska i banorna (skyltar,
// namn, resmål, linjer) måste gå att slå upp, annars kan skyltsystemet
// aldrig översätta den.
func TestAllJapaneseInLevelsIsInDictionary(t *testing.T) {
d := loadDict(t)
dir, err := res.Path("data", "levels")
if err != nil {
t.Fatal(err)
}
files, _ := filepath.Glob(filepath.Join(dir, "*.json"))
if len(files) == 0 {
t.Fatal("inga banor")
}
for _, f := range files {
b, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
l, err := level.Parse(b)
if err != nil {
t.Fatal(err)
}
check := func(what, s string) {
if s == "" {
t.Errorf("%s: %s saknar japansk text", l.ID, what)
return
}
if u := d.Unknown(s); len(u) > 0 {
t.Errorf("%s: %s %q har japanska som saknas i ordboken: %s", l.ID, what, s, strings.Join(u, ", "))
}
}
check("namn", l.Name["ja"])
check("resmål", l.Destination["ja"])
for i, tr := range l.Trains {
check("tåg "+string(rune('0'+i))+" linje", tr.Line["ja"])
check("tåg "+string(rune('0'+i))+" resmål", tr.Destination["ja"])
}
for _, sg := range l.Signs {
check("skylt", sg.Text["ja"])
}
}
}