Files
metro-puzzle-jp/internal/lang/dictionary.go
claude 9fc7e94074
All checks were successful
release / build-release (push) Successful in 1m41s
M6: plånbok, biljetter, kortet i telefonen och seedade minispel
- 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>
2026-10-24 09:50:12 +02:00

147 lines
3.8 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"`
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
}