mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-11 03:13:45 +00:00
refactor: remove unused knobs, strings and stale text
Drop what nothing uses any more: the --max-syllables flag with its sourceSpec field, meta row and reject reason; the store's clearRejection; the --gap CSS token; three orphaned i18n strings; two exports with no importer; the preview npm script; the proto.yml "baseline exists" step that has been unconditionally true since the schema landed on main; and two root-level .gitignore entries for paths Playwright never writes. Correct text that outlived its subject: NOTICE named a tools/ directory that never existed, the README still pointed at the first plan and carried a migration note for retired sources, and a few comments still said "release" for an upstream that is a weekly export. The pin test header now says three copies and also holds README and ATTRIBUTION to the same URL; the Make-targets table lists clean and help. Wire fixtures regenerated so client_hello uses the ProtocolVersion constant and server_game_started carries the 30s turn limit. builder_version becomes 4 for the dropped meta row.
This commit is contained in:
1 parent
805cbca39b
commit
557de1af94
22 files changed
+66
-111
No files matched your search
@@ -43,23 +43,10 @@ jobs:
|
||||
- name: Lint the schema
|
||||
run: buf lint
|
||||
|
||||
# Skipped only until this contract first lands on main; after that the
|
||||
# baseline always exists and the check is unconditional.
|
||||
- name: Look for a breaking-change baseline on main
|
||||
id: baseline
|
||||
run: |
|
||||
if git cat-file -e origin/main:proto/noitu/v1/game.proto 2>/dev/null; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::no schema on main yet, skipping the breaking-change check"
|
||||
fi
|
||||
|
||||
# ref=origin/main, not branch=main: a pull_request checkout is a detached
|
||||
# HEAD with no local main, and branch=main fails there with a git clone
|
||||
# error rather than a breaking-change report.
|
||||
- name: Reject breaking changes
|
||||
if: steps.baseline.outputs.exists == 'true'
|
||||
run: buf breaking --against '.git#ref=origin/main'
|
||||
|
||||
# A committed generated tree that no longer matches the schema is worse
|
||||
|
||||
@@ -24,8 +24,6 @@ web/.svelte-kit/
|
||||
node_modules/
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
web/test-results/
|
||||
web/playwright-report/
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ fixture-dict: $(FIXTURE_DB)
|
||||
proto: web/node_modules
|
||||
buf generate
|
||||
|
||||
# What CI runs: the schema is well-formed, and the committed generated trees
|
||||
# match what the schema currently produces.
|
||||
# The local equivalent of proto.yml: the schema is well-formed, and the
|
||||
# committed generated trees match what the schema currently produces.
|
||||
# --intent-to-add makes a newly emitted file visible: git diff alone ignores
|
||||
# untracked files and would call an incomplete committed tree clean.
|
||||
proto-check: proto
|
||||
|
||||
@@ -10,7 +10,7 @@ This product is distributed under two licenses, applying to different artifacts.
|
||||
All source code in this repository is licensed under the Apache License,
|
||||
Version 2.0. See the LICENSE file at the repository root.
|
||||
|
||||
This covers everything under server/, tools/, web/, and proto/.
|
||||
This covers everything under server/, web/, and proto/.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
2. DICTIONARY DATA — Creative Commons Attribution-ShareAlike 4.0 International
|
||||
|
||||
@@ -21,9 +21,8 @@ there is one implementation of it and not two.
|
||||
|
||||
## Status
|
||||
|
||||
Playable: vs bot at three difficulties, and online rooms of two to four by room code. See
|
||||
[`plans/260904-1125-noi-tu-web-game/plan.md`](./plans/260904-1125-noi-tu-web-game/plan.md)
|
||||
for the implementation plan and phase breakdown.
|
||||
Playable: vs bot at three difficulties, and online rooms of two to four by room code.
|
||||
Design history lives under `plans/`.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -169,9 +168,7 @@ The export is fetched fresh, not pinned: kaikki.org re-exports Wiktionary about
|
||||
keeps no dated snapshots, so two builds a week apart can differ slightly. The database
|
||||
records the SHA-256 of the file it was built from in its `meta` table. Neither the export
|
||||
nor the derived database is committed; both are build artifacts. See
|
||||
[`data/ATTRIBUTION.md`](./data/ATTRIBUTION.md). Leftover `data/dictionary.db` or
|
||||
`data/undertheseanlp-words.jsonl` files from earlier sources are no longer read and can be
|
||||
deleted.
|
||||
[`data/ATTRIBUTION.md`](./data/ATTRIBUTION.md).
|
||||
|
||||
## Running the server
|
||||
|
||||
@@ -231,9 +228,11 @@ dev-only URL to get wrong.
|
||||
| `proto` | Regenerate the Go and JS wire types from `proto/` (needs `buf`) |
|
||||
| `proto-check` | Lint the schema and verify the committed generated code is in sync |
|
||||
| `fixture-dict` | Build the small test dictionary, no download needed |
|
||||
| `test` | Run Go and JavaScript tests |
|
||||
| `test` | Run Go and JavaScript tests (`test-go`, `test-web`) |
|
||||
| `test-e2e` | Run the Playwright suite against the fixture dictionary |
|
||||
| `run` | Build and run the server locally |
|
||||
| `clean` | Remove build artifacts, keeping the downloaded export |
|
||||
| `help` | List these targets |
|
||||
|
||||
### Without `make`
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
|
||||
$r-8f2cNgười chơi ẩn danh
|
||||
$r-8f2cNgười chơi ẩn danh
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"n
|
||||
|
||||
hòa bìnhbình ×ø©‘3(0 œ:
|
||||
hòa bìnhbình ×ø©‘3(0°ê:
|
||||
p1Người chơi 0:
|
||||
p2Khách mời *(0:
|
||||
p3Khách 2 Bp1
|
||||
@@ -17,7 +17,6 @@ type rejectReason string
|
||||
const (
|
||||
rejectEmpty rejectReason = "empty"
|
||||
rejectTooShort rejectReason = "fewer than 2 syllables"
|
||||
rejectTooLong rejectReason = "over max-syllables"
|
||||
rejectDigit rejectReason = "contains a digit"
|
||||
rejectPunct rejectReason = "contains punctuation"
|
||||
rejectNonVietnam rejectReason = "no Vietnamese letters"
|
||||
@@ -27,8 +26,8 @@ const (
|
||||
)
|
||||
|
||||
// accept normalizes a raw source entry and decides whether it belongs in the
|
||||
// game dictionary. maxSyllables of 0 means no upper bound.
|
||||
func accept(raw string, maxSyllables int) (word string, syllables []string, reason rejectReason, ok bool) {
|
||||
// game dictionary.
|
||||
func accept(raw string) (word string, syllables []string, reason rejectReason, ok bool) {
|
||||
word, syllables, err := vietnamese.Normalize(raw)
|
||||
if err != nil {
|
||||
return "", nil, rejectEmpty, false
|
||||
@@ -37,9 +36,6 @@ func accept(raw string, maxSyllables int) (word string, syllables []string, reas
|
||||
if !vietnamese.HasEnoughSyllables(syllables) {
|
||||
return "", nil, rejectTooShort, false
|
||||
}
|
||||
if maxSyllables > 0 && len(syllables) > maxSyllables {
|
||||
return "", nil, rejectTooLong, false
|
||||
}
|
||||
|
||||
for _, r := range word {
|
||||
switch {
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestAcceptKeepsValidWords(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
word, syllables, reason, ok := accept(tc.raw, 0)
|
||||
word, syllables, reason, ok := accept(tc.raw)
|
||||
if !ok {
|
||||
t.Errorf("accept(%q) rejected: %s", tc.raw, reason)
|
||||
continue
|
||||
@@ -55,7 +55,7 @@ func TestAcceptRejects(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, _, reason, ok := accept(tc.raw, 0)
|
||||
_, _, reason, ok := accept(tc.raw)
|
||||
if ok {
|
||||
t.Fatalf("accept(%q) succeeded, want rejection %s", tc.raw, tc.want)
|
||||
}
|
||||
@@ -65,21 +65,3 @@ func TestAcceptRejects(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptMaxSyllables(t *testing.T) {
|
||||
const raw = "công nghiệp hóa"
|
||||
|
||||
if _, _, _, ok := accept(raw, 0); !ok {
|
||||
t.Errorf("accept(%q, no limit) rejected, want accepted", raw)
|
||||
}
|
||||
if _, _, _, ok := accept(raw, 3); !ok {
|
||||
t.Errorf("accept(%q, max 3) rejected, want accepted", raw)
|
||||
}
|
||||
_, _, reason, ok := accept(raw, 2)
|
||||
if ok {
|
||||
t.Fatalf("accept(%q, max 2) accepted, want rejection", raw)
|
||||
}
|
||||
if reason != rejectTooLong {
|
||||
t.Errorf("reason = %s, want %s", reason, rejectTooLong)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ type kaikkiProvenance struct {
|
||||
// Lines are read with bufio.Reader rather than bufio.Scanner because a row
|
||||
// carries every sense and translation of its entry and can run to hundreds of
|
||||
// kilobytes; a scanner's fixed cap would be a guess that eventually fails.
|
||||
func readKaikkiList(path string, maxSyllables int) (map[string]entry, map[rejectReason]int, map[string]int, kaikkiProvenance, error) {
|
||||
func readKaikkiList(path string) (map[string]entry, map[rejectReason]int, map[string]int, kaikkiProvenance, error) {
|
||||
var prov kaikkiProvenance
|
||||
|
||||
f, err := os.Open(path)
|
||||
@@ -92,7 +92,7 @@ func readKaikkiList(path string, maxSyllables int) (map[string]entry, map[reject
|
||||
rejects[rejectNotVietnamese]++
|
||||
} else {
|
||||
pos[row.Pos]++
|
||||
if word, syllables, reason, ok := accept(row.Word, maxSyllables); !ok {
|
||||
if word, syllables, reason, ok := accept(row.Word); !ok {
|
||||
rejects[reason]++
|
||||
} else {
|
||||
words[word] = entry{
|
||||
@@ -153,13 +153,12 @@ func formatPosTally(pos map[string]int) string {
|
||||
// kaikkiSourceSpec describes a kaikki build for the meta table. With no commit
|
||||
// or checksum pinned upstream, the hash and row count of the bytes read are the
|
||||
// provenance.
|
||||
func kaikkiSourceSpec(path string, prov kaikkiProvenance, maxSyllables int) sourceSpec {
|
||||
func kaikkiSourceSpec(path string, prov kaikkiProvenance) sourceSpec {
|
||||
return sourceSpec{
|
||||
table: "kaikki:" + filepath.Base(path),
|
||||
url: kaikkiSourceURL,
|
||||
license: "CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)",
|
||||
attribution: "See data/ATTRIBUTION.md for required attribution and the list of modifications.",
|
||||
maxSyllables: maxSyllables,
|
||||
table: "kaikki:" + filepath.Base(path),
|
||||
url: kaikkiSourceURL,
|
||||
license: "CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)",
|
||||
attribution: "See data/ATTRIBUTION.md for required attribution and the list of modifications.",
|
||||
extra: [][2]string{
|
||||
{"source_sha256", prov.sha256},
|
||||
{"source_rows", fmt.Sprint(prov.rows)},
|
||||
|
||||
@@ -32,7 +32,7 @@ func defaultKaikkiLines() []string {
|
||||
}
|
||||
|
||||
func TestKaikkiListKeepsVietnameseEntries(t *testing.T) {
|
||||
words, rejects, pos, prov, err := readKaikkiList(fixtureKaikki(t, defaultKaikkiLines()...), 0)
|
||||
words, rejects, pos, prov, err := readKaikkiList(fixtureKaikki(t, defaultKaikkiLines()...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func TestKaikkiListKeepsVietnameseEntries(t *testing.T) {
|
||||
|
||||
func TestKaikkiListHashesTheBytesItRead(t *testing.T) {
|
||||
path := fixtureKaikki(t, defaultKaikkiLines()...)
|
||||
_, _, _, prov, err := readKaikkiList(path, 0)
|
||||
_, _, _, prov, err := readKaikkiList(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func TestKaikkiListHandlesDownloadShapes(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
words, _, _, prov, err := readKaikkiList(fixtureKaikkiRaw(t, tc.raw), 0)
|
||||
words, _, _, prov, err := readKaikkiList(fixtureKaikkiRaw(t, tc.raw))
|
||||
if tc.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("err = %v, want one containing %q", err, tc.wantErr)
|
||||
@@ -126,7 +126,7 @@ func TestKaikkiListNamesMalformedLine(t *testing.T) {
|
||||
`{"word": "học sinh", "pos": "noun", "lang_code": "vi"}`,
|
||||
`{"word": "broken"`,
|
||||
)
|
||||
_, _, _, _, err := readKaikkiList(path, 0)
|
||||
_, _, _, _, err := readKaikkiList(path)
|
||||
if err == nil {
|
||||
t.Fatal("malformed line was skipped, want error")
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func TestKaikkiListReadsLongLines(t *testing.T) {
|
||||
// scanner buffer; the reader must not have a line cap.
|
||||
padding := strings.Repeat("x", 2<<20)
|
||||
path := fixtureKaikki(t, `{"word": "học sinh", "pos": "noun", "lang_code": "vi", "note": "`+padding+`"}`)
|
||||
words, _, _, _, err := readKaikkiList(path, 0)
|
||||
words, _, _, _, err := readKaikkiList(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -33,17 +33,16 @@ import (
|
||||
|
||||
// builderVer changes whenever the meta table's contract does, so two databases
|
||||
// with different provenance rows never claim the same builder.
|
||||
const builderVer = "3"
|
||||
const builderVer = "4"
|
||||
|
||||
type config struct {
|
||||
// kaikki is the corpus: the upstream wiktextract JSONL export.
|
||||
kaikki string
|
||||
// words is an alternative source: a plain list, one word per line, used to
|
||||
// build a small fixture database without the upstream download.
|
||||
words string
|
||||
out string
|
||||
maxSyllables int
|
||||
minWords int
|
||||
words string
|
||||
out string
|
||||
minWords int
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -53,7 +52,6 @@ func main() {
|
||||
flag.StringVar(&cfg.kaikki, "kaikki", "", "upstream kaikki.org wiktextract JSONL export to read")
|
||||
flag.StringVar(&cfg.words, "words", "", "read a plain word list instead of the upstream export (one word per line, # comments)")
|
||||
flag.StringVar(&cfg.out, "out", "../data/noitu.db", "derived database to write")
|
||||
flag.IntVar(&cfg.maxSyllables, "max-syllables", 0, "reject words longer than this (0 = no limit)")
|
||||
flag.IntVar(&cfg.minWords, "min-words", 30000, "fail if fewer words survive filtering")
|
||||
flag.Parse()
|
||||
|
||||
@@ -84,7 +82,7 @@ func runFromKaikkiList(cfg config) error {
|
||||
return fmt.Errorf("kaikki export not found at %s — run 'make fetch-dict' first: %w", cfg.kaikki, err)
|
||||
}
|
||||
|
||||
words, rejects, pos, prov, err := readKaikkiList(cfg.kaikki, cfg.maxSyllables)
|
||||
words, rejects, pos, prov, err := readKaikkiList(cfg.kaikki)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,7 +90,7 @@ func runFromKaikkiList(cfg config) error {
|
||||
log.Printf("parts of speech: %s", formatPosTally(pos))
|
||||
log.Printf("accepted %d distinct words from %s (%d rows, sha256 %s)", len(words), cfg.kaikki, prov.rows, prov.sha256)
|
||||
|
||||
return finish(cfg, words, kaikkiSourceSpec(cfg.kaikki, prov, cfg.maxSyllables))
|
||||
return finish(cfg, words, kaikkiSourceSpec(cfg.kaikki, prov))
|
||||
}
|
||||
|
||||
// finish is the tail every input mode shares: the size floor, alias
|
||||
@@ -120,7 +118,7 @@ func finish(cfg config, words map[string]entry, src sourceSpec) error {
|
||||
}
|
||||
|
||||
// runFromWordList derives a database from a plain list of words instead of the
|
||||
// upstream release.
|
||||
// upstream export.
|
||||
//
|
||||
// It exists so tests and CI have a real dictionary to play against without the
|
||||
// upstream download. The filtering, alias generation, writing and verification
|
||||
@@ -141,7 +139,7 @@ func runFromWordList(cfg config) error {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
word, syllables, reason, ok := accept(line, cfg.maxSyllables)
|
||||
word, syllables, reason, ok := accept(line)
|
||||
if !ok {
|
||||
rejects[reason]++
|
||||
continue
|
||||
@@ -222,8 +220,7 @@ type entry struct {
|
||||
type sourceSpec struct {
|
||||
// table names the input: "kaikki:<file>" for the corpus, "wordlist:<file>"
|
||||
// for a fixture, so the output says which build produced it.
|
||||
table string
|
||||
maxSyllables int
|
||||
table string
|
||||
// url is the upstream artifact; empty for fixture builds.
|
||||
url string
|
||||
// license and attribution describe the data's licence obligations. The
|
||||
@@ -407,7 +404,6 @@ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
{"word_count", fmt.Sprint(len(words))},
|
||||
{"alias_count", fmt.Sprint(len(aliases))},
|
||||
{"source_table", src.table},
|
||||
{"max_syllables", fmt.Sprint(src.maxSyllables)},
|
||||
}
|
||||
meta = append(meta, src.extra...)
|
||||
for _, kv := range meta {
|
||||
|
||||
@@ -39,15 +39,14 @@ func defaultRows() [][2]string {
|
||||
}
|
||||
}
|
||||
|
||||
func buildFixture(t *testing.T, rows [][2]string, maxSyllables int) string {
|
||||
func buildFixture(t *testing.T, rows [][2]string) string {
|
||||
t.Helper()
|
||||
|
||||
out := filepath.Join(t.TempDir(), "noitu.db")
|
||||
cfg := config{
|
||||
kaikki: fixtureSource(t, rows),
|
||||
out: out,
|
||||
maxSyllables: maxSyllables,
|
||||
minWords: 1,
|
||||
kaikki: fixtureSource(t, rows),
|
||||
out: out,
|
||||
minWords: 1,
|
||||
}
|
||||
if err := run(cfg); err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
@@ -66,7 +65,7 @@ func openOut(t *testing.T, path string) *sql.DB {
|
||||
}
|
||||
|
||||
func TestBuildProducesExpectedWords(t *testing.T) {
|
||||
db := openOut(t, buildFixture(t, defaultRows(), 0))
|
||||
db := openOut(t, buildFixture(t, defaultRows()))
|
||||
|
||||
var count int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM words`).Scan(&count); err != nil {
|
||||
@@ -99,7 +98,7 @@ func TestBuildProducesExpectedWords(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildComputesOutDegree(t *testing.T) {
|
||||
db := openOut(t, buildFixture(t, defaultRows(), 0))
|
||||
db := openOut(t, buildFixture(t, defaultRows()))
|
||||
|
||||
// "pháp luật" and "pháp" (rejected) mean exactly one word starts with "pháp".
|
||||
assertOutDegree(t, db, "pháp", 1)
|
||||
@@ -121,7 +120,7 @@ func assertOutDegree(t *testing.T, db *sql.DB, syllable string, want int) {
|
||||
}
|
||||
|
||||
func TestBuildWritesAliases(t *testing.T) {
|
||||
db := openOut(t, buildFixture(t, defaultRows(), 0))
|
||||
db := openOut(t, buildFixture(t, defaultRows()))
|
||||
|
||||
var canonical string
|
||||
err := db.QueryRow(`SELECT canonical FROM aliases WHERE variant = ?`, "hoà bình").Scan(&canonical)
|
||||
@@ -146,7 +145,7 @@ func TestBuildWritesAliases(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildRecordsProvenance(t *testing.T) {
|
||||
db := openOut(t, buildFixture(t, defaultRows(), 0))
|
||||
db := openOut(t, buildFixture(t, defaultRows()))
|
||||
|
||||
for _, key := range []string{"source_url", "source_license", "attribution", "built_at", "word_count"} {
|
||||
var value string
|
||||
@@ -176,7 +175,7 @@ func TestBuildFailsBelowMinWords(t *testing.T) {
|
||||
// A failed build must leave the previous good database untouched. Building in
|
||||
// place would delete it and leave an empty file the server would happily open.
|
||||
func TestFailedBuildPreservesPreviousOutput(t *testing.T) {
|
||||
out := buildFixture(t, defaultRows(), 0)
|
||||
out := buildFixture(t, defaultRows())
|
||||
|
||||
before, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ const fixtureDir = "../../../proto/testdata"
|
||||
func clientVariants() map[string]*noituv1.ClientMessage {
|
||||
return map[string]*noituv1.ClientMessage{
|
||||
"client_hello": {Payload: &noituv1.ClientMessage_Hello{Hello: &noituv1.Hello{
|
||||
ProtocolVersion: 1,
|
||||
ProtocolVersion: ProtocolVersion,
|
||||
ResumeToken: "r-8f2c",
|
||||
Nickname: "Người chơi ẩn danh",
|
||||
}}},
|
||||
@@ -50,11 +50,11 @@ func clientVariants() map[string]*noituv1.ClientMessage {
|
||||
"client_set_ready": {Payload: &noituv1.ClientMessage_SetReady{SetReady: &noituv1.SetReady{
|
||||
Ready: true,
|
||||
}}},
|
||||
"client_start_game": {Payload: &noituv1.ClientMessage_StartGame{StartGame: &noituv1.StartGame{}}},
|
||||
"client_start_game": {Payload: &noituv1.ClientMessage_StartGame{StartGame: &noituv1.StartGame{}}},
|
||||
"client_kick_player": {Payload: &noituv1.ClientMessage_KickPlayer{KickPlayer: &noituv1.KickPlayer{
|
||||
PlayerId: "p3",
|
||||
}}},
|
||||
"client_leave_room": {Payload: &noituv1.ClientMessage_LeaveRoom{LeaveRoom: &noituv1.LeaveRoom{}}},
|
||||
"client_leave_room": {Payload: &noituv1.ClientMessage_LeaveRoom{LeaveRoom: &noituv1.LeaveRoom{}}},
|
||||
"client_send_chat": {Payload: &noituv1.ClientMessage_SendChat{SendChat: &noituv1.SendChat{
|
||||
Text: "Chào bạn, ván này khó thật!",
|
||||
}}},
|
||||
@@ -76,7 +76,7 @@ func serverVariants() map[string]*noituv1.ServerMessage {
|
||||
MyTurn: true,
|
||||
DeadlineUnixMs: 1756998020000,
|
||||
TurnSeq: 1,
|
||||
TurnLimitMs: 20000,
|
||||
TurnLimitMs: 30000,
|
||||
Players: fixtureScores(),
|
||||
TurnPlayerId: "p1",
|
||||
}}},
|
||||
|
||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# Hand-written common Vietnamese compounds, not an extract of the upstream
|
||||
# dictionary: CI must never download the upstream wordlist, and a fixture derived
|
||||
# from it would carry that release's licence into every test artifact.
|
||||
# from it would carry that export's licence into every test artifact.
|
||||
#
|
||||
# `make fixture-dict` turns this into a real derived database through the same
|
||||
# filter, alias and write path production uses, so a fixture cannot drift into
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ONE_SYLLABLE_WORD = 'sinh';
|
||||
* an unhelpful "game_start_failed". Asserting it here turns that into a
|
||||
* sentence a reader can act on.
|
||||
*/
|
||||
export const HUB_SYLLABLE = 'sinh';
|
||||
const HUB_SYLLABLE = 'sinh';
|
||||
const MIN_OPENING_OUT_DEGREE = 20;
|
||||
|
||||
const hubContinuations = (byFirstSyllable.get(HUB_SYLLABLE) ?? []).length;
|
||||
@@ -66,5 +66,3 @@ if (hubContinuations < MIN_OPENING_OUT_DEGREE) {
|
||||
`the server needs ${MIN_OPENING_OUT_DEGREE} before it will open a game on that syllable`
|
||||
);
|
||||
}
|
||||
|
||||
export { words };
|
||||
@@ -6,7 +6,6 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"test": "vite build && vitest run",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--gap: 12px;
|
||||
--font: system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
|
||||
color-scheme: light;
|
||||
|
||||
@@ -34,12 +34,10 @@ export const t = {
|
||||
you: 'Bạn',
|
||||
opponent: 'Đối thủ',
|
||||
someone: 'Người chơi',
|
||||
scoreLabel: 'Điểm',
|
||||
syllableUnit: 'tiếng',
|
||||
correctedFrom: 'Bạn gõ “{typed}”, từ đúng là “{word}”.',
|
||||
|
||||
secondsLeft: '{n} giây',
|
||||
waiting: 'Đang chờ…',
|
||||
connecting: 'Đang kết nối…',
|
||||
connected: 'Đã kết nối',
|
||||
reconnecting: 'Mất kết nối, đang thử lại…',
|
||||
@@ -77,7 +75,6 @@ export const t = {
|
||||
copyLink: 'Sao chép liên kết mời',
|
||||
shareLink: 'Chia sẻ',
|
||||
copied: 'Đã sao chép',
|
||||
waitingForOpponent: 'Đang chờ người chơi vào phòng…',
|
||||
leaveRoom: 'Rời phòng',
|
||||
shareInvite: 'Vào chơi nối từ với tôi',
|
||||
|
||||
|
||||
@@ -492,9 +492,6 @@ export function createGameStore() {
|
||||
);
|
||||
},
|
||||
|
||||
clearRejection() {
|
||||
state.rejection = null;
|
||||
},
|
||||
clearError() {
|
||||
state.error = null;
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@ export const LIVENESS_TIMEOUT_MS = 3 * PING_INTERVAL_MS;
|
||||
* client reconnects every half second forever after a deploy that bumps the
|
||||
* protocol.
|
||||
*/
|
||||
export const TERMINAL_ERROR_CODES = new Set(['protocol_version_mismatch']);
|
||||
const TERMINAL_ERROR_CODES = new Set(['protocol_version_mismatch']);
|
||||
|
||||
const RESUME_KEY = 'noitu.resumeToken';
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// The upstream dictionary URL lives in two places: the Makefile, which builds
|
||||
// it for a developer, and the Dockerfile, which builds it for the image. They
|
||||
// have to agree, or the container ships a wordlist nobody tested against.
|
||||
// The upstream dictionary URL lives in three places: the Makefile, which
|
||||
// builds it for a developer, the Dockerfile, which builds it for the image, and
|
||||
// the builder, which stamps it into the database. They have to agree, or the
|
||||
// container ships a wordlist nobody tested against. The docs that quote the
|
||||
// URL are held to the same copy.
|
||||
//
|
||||
// This lives in the JavaScript suite for no better reason than that it is the
|
||||
// suite that already reads other files in the repository. It is checking two
|
||||
// build files, not the frontend.
|
||||
// suite that already reads other files in the repository. It is checking build
|
||||
// files and docs, not the frontend.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -49,4 +51,11 @@ describe('the upstream dictionary export', () => {
|
||||
const builderUrl = pin(builder, /kaikkiSourceURL\s*=\s*"([^"]+)"/, 'kaikkiSourceURL in the builder');
|
||||
expect(builderUrl).toBe(makeUrl);
|
||||
});
|
||||
|
||||
it('is the URL the docs quote', () => {
|
||||
for (const rel of ['../../README.md', '../../data/ATTRIBUTION.md']) {
|
||||
const doc = readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
|
||||
expect(doc, `${rel} does not quote DICT_URL`).toContain(makeUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user