M3: japanska skyltar – pixelfont, ordbok, kunskap, delvis översättning, hjälp
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:
2026-10-23 20:14:36 +02:00
parent 0e10cbf123
commit b67493fddb
40 changed files with 1846 additions and 174 deletions

96
internal/lang/tokenize.go Normal file
View File

@@ -0,0 +1,96 @@
package lang
import "strings"
// Kind är vad ett textsegment är.
type Kind int
// Segmenttyper.
const (
Plain Kind = iota // allt som inte är japanska: siffror, pilar, latin, mellanslag
Word // ett ord ur ordboken
Unknown // japanska tecken som inte täcks av ordboken
)
// Segment är en bit av en skylttext.
type Segment struct {
Text string
Kind Kind
Entry *Entry // satt när Kind == Word
}
// Tokenize delar upp en text i ordboksord (längsta matchning först), okänd
// japanska och övrig text. Sammanhängande okänd japanska slås ihop till ett
// segment så att tester kan peka ut vad som saknas i ordboken.
func (d *Dictionary) Tokenize(s string) []Segment {
runes := []rune(s)
var out []Segment
flush := func(kind Kind, text string) {
if text == "" {
return
}
if n := len(out); n > 0 && out[n-1].Kind == kind && kind != Word {
out[n-1].Text += text
return
}
out = append(out, Segment{Text: text, Kind: kind})
}
for i := 0; i < len(runes); {
r := runes[i]
if !IsKana(r) && !IsKanji(r) {
flush(Plain, string(r))
i++
continue
}
if w := d.longestMatch(runes[i:]); w != "" {
out = append(out, Segment{Text: w, Kind: Word, Entry: d.entries[w]})
i += len([]rune(w))
continue
}
flush(Unknown, string(r))
i++
}
return out
}
func (d *Dictionary) longestMatch(rest []rune) string {
for _, w := range d.words { // längsta först
wr := []rune(w)
if len(wr) > len(rest) {
continue
}
match := true
for k, c := range wr {
if rest[k] != c {
match = false
break
}
}
if match {
return w
}
}
return ""
}
// Unknown returnerar den japanska i texten som ordboken inte täcker.
func (d *Dictionary) Unknown(s string) []string {
var out []string
for _, seg := range d.Tokenize(s) {
if seg.Kind == Unknown {
out = append(out, strings.TrimSpace(seg.Text))
}
}
return out
}
// WordsIn returnerar ordboksorden i texten i ordning (med dubbletter).
func (d *Dictionary) WordsIn(s string) []string {
var out []string
for _, seg := range d.Tokenize(s) {
if seg.Kind == Word {
out = append(out, seg.Text)
}
}
return out
}