Files
metro-puzzle-jp/internal/lang/dictionary.go
claude b67493fddb
All checks were successful
release / build-release (push) Successful in 1m42s
M3: japanska skyltar – pixelfont, ordbok, kunskap, delvis översättning, hjälp
- 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>
2026-10-23 20:14:36 +02:00

134 lines
3.5 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 lang är språksystemet: ordboken över japanska skyltord, spelarens
// kunskap, tokenisering av skylttext och den delvisa översättning spelaren
// ser. Ren Go – inga Ebitengine-beroenden.
package lang
import (
"encoding/json"
"fmt"
"sort"
"strings"
"unicode"
)
// Entry är ett ord i ordboken.
type Entry struct {
Word string `json:"-"`
Kana string `json:"kana"`
Romaji string `json:"romaji"`
En string `json:"en"`
Sv string `json:"sv"`
Tier int `json:"tier"`
}
// Translation returnerar översättningen på UI-språket (sv/en) med fallback.
func (e *Entry) Translation(lang string) string {
if lang == "en" || e.Sv == "" {
return e.En
}
return e.Sv
}
// Dictionary är ordboken. Orden hålls sorterade längst först för
// längsta-matchning vid tokenisering.
type Dictionary struct {
entries map[string]*Entry
words []string
}
// NewDictionary skapar en tom ordbok (praktiskt i tester).
func NewDictionary() *Dictionary { return &Dictionary{entries: map[string]*Entry{}} }
// ParseDictionary läser jp-dictionary.json och validerar att varje post har
// kana, romaji, en, sv och tier. Nycklar som börjar med "_" ignoreras.
func ParseDictionary(data []byte) (*Dictionary, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("lang: ogiltig ordbok: %w", err)
}
d := NewDictionary()
for word, msg := range raw {
if strings.HasPrefix(word, "_") {
continue
}
var e Entry
if err := json.Unmarshal(msg, &e); err != nil {
return nil, fmt.Errorf("lang: ordet %q: %w", word, err)
}
if e.Kana == "" || e.Romaji == "" || e.En == "" || e.Sv == "" || e.Tier < 1 {
return nil, fmt.Errorf("lang: ordet %q saknar kana/romaji/en/sv/tier", word)
}
if !IsJapanese(word) {
return nil, fmt.Errorf("lang: nyckeln %q innehåller inte japanska tecken", word)
}
d.Add(word, e)
}
return d, nil
}
// Add lägger till eller ersätter ett ord.
func (d *Dictionary) Add(word string, e Entry) {
e.Word = word
if _, exists := d.entries[word]; !exists {
d.words = append(d.words, word)
sort.Slice(d.words, func(i, j int) bool {
ri, rj := len([]rune(d.words[i])), len([]rune(d.words[j]))
if ri != rj {
return ri > rj
}
return d.words[i] < d.words[j]
})
}
d.entries[word] = &e
}
// Lookup returnerar posten för ett ord, eller nil.
func (d *Dictionary) Lookup(word string) *Entry { return d.entries[word] }
// Len är antalet ord.
func (d *Dictionary) Len() int { return len(d.entries) }
// Words returnerar alla ord, längsta först.
func (d *Dictionary) Words() []string { return append([]string(nil), d.words...) }
// IsKana rapporterar om tecknet är hiragana eller katakana (inkl. ー).
func IsKana(r rune) bool {
return unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r) || r == 'ー'
}
// IsKanji rapporterar om tecknet är ett han-tecken.
func IsKanji(r rune) bool { return unicode.Is(unicode.Han, r) || r == '々' }
// IsJapanese rapporterar om strängen innehåller minst ett kana- eller kanji-tecken.
func IsJapanese(s string) bool {
for _, r := range s {
if IsKana(r) || IsKanji(r) {
return true
}
}
return false
}
// HasKana rapporterar om ordet innehåller kana.
func HasKana(s string) bool {
for _, r := range s {
if IsKana(r) {
return true
}
}
return false
}
// IsAllKana rapporterar om ordet bara består av kana.
func IsAllKana(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if !IsKana(r) {
return false
}
}
return true
}