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>
134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
// 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
|
||
}
|