M3: japanska skyltar – pixelfont, ordbok, kunskap, delvis översättning, hjälp
All checks were successful
release / build-release (push) Successful in 1m42s
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:
133
internal/lang/dictionary.go
Normal file
133
internal/lang/dictionary.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user