All checks were successful
release / build-release (push) Successful in 1m41s
- sim.Wallet: IC-kort, kontanter, pappersbiljett, telefon med batteri, spenderat; följer med mellan banor (Game.Wallet) - internal/minigame: gemensamt Game-gränssnitt (Update/Done/Success), all slump från del-seeds: TopUp (hitta チャージ, belopp, mata sedlar), Ticket (taxetavla med resmål på japanska), Crowd (tidsfönster, två försök), Ride (stationerna passerar, kliv av vid resmålet) - sim.Run: spärren kräver biljett på biljettbanor annars användbart kort; telefonen ger ×1,1 fart men 15 % seedad omskanning (frysning 1,5 s), dött batteri gör kortet oanvändbart; automaten startar påfyllning eller biljettköp; tåget kontrollerar biljetten; fulla tåg kräver trängsel; resan avgör vinst/förlust (fel station, åkte förbi, fel tåg); StepMini låter klockan och sällskapet ticka medan rörelsen pausar - level: ticketRequired, rideStops, Train.Crowded; 002 är biljettbana med fullt tåg och fyra stopp; ordboken har stationer (kind) + sju nya namn - game: vyer för minispelen, plånbok i HUD, P i menyn = kort i telefonen, förlustorsaker och spenderat på resultatet, flaggor -phone/-card, skriptkommandon hold/waitstop; inforuta/minikarta döljs under minispel - plan: M7b (Björn) – tågvind, ankomstindikator nära rälsen, in-/utkörningsanimation för tåg och bussar – i spelbeskrivning, roadmap, testplan - docs/README/wiki med tre nya bilder (automat, biljettavla, tågresa) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
147 lines
3.8 KiB
Go
147 lines
3.8 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"`
|
||
Kind string `json:"kind"` // valfri kategori, t.ex. "station"
|
||
}
|
||
|
||
// 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...) }
|
||
|
||
// OfKind returnerar orden i en kategori (t.ex. "station"), sorterade.
|
||
func (d *Dictionary) OfKind(kind string) []string {
|
||
var out []string
|
||
for _, w := range d.words {
|
||
if d.entries[w].Kind == kind {
|
||
out = append(out, w)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
// 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
|
||
}
|