Files
scrabble-game/ui/src/lib/robot/testdata/strategy.json
T
Ilia Denisov 8c5995c076
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m37s
feat(offline): port robot move-choice strategy to TS (parity-pinned)
Phase A (2/2) of PWA offline mode: the offline robot picks its move exactly as the
server does, so a local vs_ai game plays the same. Builds on the move generator
from #188; not wired into a game loop yet (Phase B).

- ui/src/lib/robot/strategy.ts: port of backend/internal/robot/strategy.go's
  move-choice slice — mix (FNV-1a, via BigInt for bit-exact uint64), playToWin
  (~40% play-to-win), deviates (the fading off-strategy wobble) and selectMove
  (pick the candidate whose resulting margin lands closest to the +/-[1,30] band,
  conservative tie-break), composed by decide(). The generator's ranked moves feed
  straight in. Think-time/sleep/nudge scheduling is server-only and not ported.
- backend/internal/robot/strategyfixture_test.go: an in-package, env-gated emitter
  (EMIT_STRATEGY_FIXTURES=1) writing golden fixtures from the real Go strategy — it
  reaches the unexported mix/playToWin/deviates/selectMove.
- strategy.parity.test.ts: 21 mix + 56 decision cases match Go exactly (play/
  exchange/pass, the deviate flip, tie-break, band overshoot).

Pure additive library code; no runtime behavior change (unused at runtime, so the
bundle is unchanged).
2026-07-06 01:54:20 +02:00

1058 lines
18 KiB
JSON

{
"playToWinPercent": 40,
"mix": [
{
"seed": "1",
"salt": "win",
"nums": [],
"value": "3463786966066269450"
},
{
"seed": "1",
"salt": "deviate",
"nums": [
0
],
"value": "16739214574173863474"
},
{
"seed": "1",
"salt": "deviate",
"nums": [
7
],
"value": "4989416721367080085"
},
{
"seed": "42",
"salt": "win",
"nums": [],
"value": "11996028682749602919"
},
{
"seed": "42",
"salt": "deviate",
"nums": [
0
],
"value": "3199075231987854375"
},
{
"seed": "42",
"salt": "deviate",
"nums": [
7
],
"value": "6019611456924280128"
},
{
"seed": "-7",
"salt": "win",
"nums": [],
"value": "5394446344798824475"
},
{
"seed": "-7",
"salt": "deviate",
"nums": [
0
],
"value": "13744638055244324387"
},
{
"seed": "-7",
"salt": "deviate",
"nums": [
7
],
"value": "15976953462211913796"
},
{
"seed": "1000003",
"salt": "win",
"nums": [],
"value": "18263589017178353807"
},
{
"seed": "1000003",
"salt": "deviate",
"nums": [
0
],
"value": "7838718956381548031"
},
{
"seed": "1000003",
"salt": "deviate",
"nums": [
7
],
"value": "10659255181317973784"
},
{
"seed": "9999999999",
"salt": "win",
"nums": [],
"value": "6610647830354154516"
},
{
"seed": "9999999999",
"salt": "deviate",
"nums": [
0
],
"value": "12675031630059091344"
},
{
"seed": "9999999999",
"salt": "deviate",
"nums": [
7
],
"value": "9854495405122665591"
},
{
"seed": "123456789",
"salt": "win",
"nums": [],
"value": "12237171742041588149"
},
{
"seed": "123456789",
"salt": "deviate",
"nums": [
0
],
"value": "16735187508536178449"
},
{
"seed": "123456789",
"salt": "deviate",
"nums": [
7
],
"value": "9450020469664573878"
},
{
"seed": "-123456789",
"salt": "win",
"nums": [],
"value": "11055856957027161150"
},
{
"seed": "-123456789",
"salt": "deviate",
"nums": [
0
],
"value": "14082038702359824862"
},
{
"seed": "-123456789",
"salt": "deviate",
"nums": [
7
],
"value": "2920461667521877817"
}
],
"decisions": [
{
"seed": "1",
"moveCount": 3,
"myScore": 40,
"oppScore": 55,
"candScores": [
30,
12,
8
],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "1",
"moveCount": 10,
"myScore": 120,
"oppScore": 90,
"candScores": [
25,
18,
5
],
"rackLen": 7,
"bagLen": 8,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
},
{
"seed": "1",
"moveCount": 1,
"myScore": 0,
"oppScore": 0,
"candScores": [
60,
30,
15,
7
],
"rackLen": 7,
"bagLen": 30,
"playToWin": false,
"win": true,
"kind": "play",
"index": 3
},
{
"seed": "1",
"moveCount": 20,
"myScore": 200,
"oppScore": 40,
"candScores": [
50,
20
],
"rackLen": 5,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "1",
"moveCount": 5,
"myScore": 30,
"oppScore": 30,
"candScores": [
20,
20,
20
],
"rackLen": 7,
"bagLen": 14,
"playToWin": false,
"win": false,
"kind": "play",
"index": 0
},
{
"seed": "1",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": false,
"kind": "exchange",
"index": -1
},
{
"seed": "1",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 3,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "pass",
"index": -1
},
{
"seed": "1",
"moveCount": 15,
"myScore": 300,
"oppScore": 10,
"candScores": [
80,
40,
10
],
"rackLen": 7,
"bagLen": 6,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
},
{
"seed": "42",
"moveCount": 3,
"myScore": 40,
"oppScore": 55,
"candScores": [
30,
12,
8
],
"rackLen": 7,
"bagLen": 20,
"playToWin": true,
"win": true,
"kind": "play",
"index": 0
},
{
"seed": "42",
"moveCount": 10,
"myScore": 120,
"oppScore": 90,
"candScores": [
25,
18,
5
],
"rackLen": 7,
"bagLen": 8,
"playToWin": true,
"win": true,
"kind": "play",
"index": 2
},
{
"seed": "42",
"moveCount": 1,
"myScore": 0,
"oppScore": 0,
"candScores": [
60,
30,
15,
7
],
"rackLen": 7,
"bagLen": 30,
"playToWin": true,
"win": false,
"kind": "play",
"index": 3
},
{
"seed": "42",
"moveCount": 20,
"myScore": 200,
"oppScore": 40,
"candScores": [
50,
20
],
"rackLen": 5,
"bagLen": 2,
"playToWin": true,
"win": true,
"kind": "play",
"index": 1
},
{
"seed": "42",
"moveCount": 5,
"myScore": 30,
"oppScore": 30,
"candScores": [
20,
20,
20
],
"rackLen": 7,
"bagLen": 14,
"playToWin": true,
"win": true,
"kind": "play",
"index": 0
},
{
"seed": "42",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 7,
"bagLen": 20,
"playToWin": true,
"win": false,
"kind": "exchange",
"index": -1
},
{
"seed": "42",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 3,
"bagLen": 2,
"playToWin": true,
"win": true,
"kind": "pass",
"index": -1
},
{
"seed": "42",
"moveCount": 15,
"myScore": 300,
"oppScore": 10,
"candScores": [
80,
40,
10
],
"rackLen": 7,
"bagLen": 6,
"playToWin": true,
"win": true,
"kind": "play",
"index": 2
},
{
"seed": "-7",
"moveCount": 3,
"myScore": 40,
"oppScore": 55,
"candScores": [
30,
12,
8
],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "-7",
"moveCount": 10,
"myScore": 120,
"oppScore": 90,
"candScores": [
25,
18,
5
],
"rackLen": 7,
"bagLen": 8,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
},
{
"seed": "-7",
"moveCount": 1,
"myScore": 0,
"oppScore": 0,
"candScores": [
60,
30,
15,
7
],
"rackLen": 7,
"bagLen": 30,
"playToWin": false,
"win": false,
"kind": "play",
"index": 3
},
{
"seed": "-7",
"moveCount": 20,
"myScore": 200,
"oppScore": 40,
"candScores": [
50,
20
],
"rackLen": 5,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "-7",
"moveCount": 5,
"myScore": 30,
"oppScore": 30,
"candScores": [
20,
20,
20
],
"rackLen": 7,
"bagLen": 14,
"playToWin": false,
"win": true,
"kind": "play",
"index": 0
},
{
"seed": "-7",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": false,
"kind": "exchange",
"index": -1
},
{
"seed": "-7",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 3,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "pass",
"index": -1
},
{
"seed": "-7",
"moveCount": 15,
"myScore": 300,
"oppScore": 10,
"candScores": [
80,
40,
10
],
"rackLen": 7,
"bagLen": 6,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
},
{
"seed": "1000003",
"moveCount": 3,
"myScore": 40,
"oppScore": 55,
"candScores": [
30,
12,
8
],
"rackLen": 7,
"bagLen": 20,
"playToWin": true,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "1000003",
"moveCount": 10,
"myScore": 120,
"oppScore": 90,
"candScores": [
25,
18,
5
],
"rackLen": 7,
"bagLen": 8,
"playToWin": true,
"win": true,
"kind": "play",
"index": 2
},
{
"seed": "1000003",
"moveCount": 1,
"myScore": 0,
"oppScore": 0,
"candScores": [
60,
30,
15,
7
],
"rackLen": 7,
"bagLen": 30,
"playToWin": true,
"win": true,
"kind": "play",
"index": 3
},
{
"seed": "1000003",
"moveCount": 20,
"myScore": 200,
"oppScore": 40,
"candScores": [
50,
20
],
"rackLen": 5,
"bagLen": 2,
"playToWin": true,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "1000003",
"moveCount": 5,
"myScore": 30,
"oppScore": 30,
"candScores": [
20,
20,
20
],
"rackLen": 7,
"bagLen": 14,
"playToWin": true,
"win": true,
"kind": "play",
"index": 0
},
{
"seed": "1000003",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 7,
"bagLen": 20,
"playToWin": true,
"win": true,
"kind": "exchange",
"index": -1
},
{
"seed": "1000003",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 3,
"bagLen": 2,
"playToWin": true,
"win": true,
"kind": "pass",
"index": -1
},
{
"seed": "1000003",
"moveCount": 15,
"myScore": 300,
"oppScore": 10,
"candScores": [
80,
40,
10
],
"rackLen": 7,
"bagLen": 6,
"playToWin": true,
"win": true,
"kind": "play",
"index": 2
},
{
"seed": "9999999999",
"moveCount": 3,
"myScore": 40,
"oppScore": 55,
"candScores": [
30,
12,
8
],
"rackLen": 7,
"bagLen": 20,
"playToWin": true,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "9999999999",
"moveCount": 10,
"myScore": 120,
"oppScore": 90,
"candScores": [
25,
18,
5
],
"rackLen": 7,
"bagLen": 8,
"playToWin": true,
"win": true,
"kind": "play",
"index": 2
},
{
"seed": "9999999999",
"moveCount": 1,
"myScore": 0,
"oppScore": 0,
"candScores": [
60,
30,
15,
7
],
"rackLen": 7,
"bagLen": 30,
"playToWin": true,
"win": true,
"kind": "play",
"index": 3
},
{
"seed": "9999999999",
"moveCount": 20,
"myScore": 200,
"oppScore": 40,
"candScores": [
50,
20
],
"rackLen": 5,
"bagLen": 2,
"playToWin": true,
"win": true,
"kind": "play",
"index": 1
},
{
"seed": "9999999999",
"moveCount": 5,
"myScore": 30,
"oppScore": 30,
"candScores": [
20,
20,
20
],
"rackLen": 7,
"bagLen": 14,
"playToWin": true,
"win": true,
"kind": "play",
"index": 0
},
{
"seed": "9999999999",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 7,
"bagLen": 20,
"playToWin": true,
"win": true,
"kind": "exchange",
"index": -1
},
{
"seed": "9999999999",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 3,
"bagLen": 2,
"playToWin": true,
"win": true,
"kind": "pass",
"index": -1
},
{
"seed": "9999999999",
"moveCount": 15,
"myScore": 300,
"oppScore": 10,
"candScores": [
80,
40,
10
],
"rackLen": 7,
"bagLen": 6,
"playToWin": true,
"win": true,
"kind": "play",
"index": 2
},
{
"seed": "123456789",
"moveCount": 3,
"myScore": 40,
"oppScore": 55,
"candScores": [
30,
12,
8
],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": true,
"kind": "play",
"index": 0
},
{
"seed": "123456789",
"moveCount": 10,
"myScore": 120,
"oppScore": 90,
"candScores": [
25,
18,
5
],
"rackLen": 7,
"bagLen": 8,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
},
{
"seed": "123456789",
"moveCount": 1,
"myScore": 0,
"oppScore": 0,
"candScores": [
60,
30,
15,
7
],
"rackLen": 7,
"bagLen": 30,
"playToWin": false,
"win": false,
"kind": "play",
"index": 3
},
{
"seed": "123456789",
"moveCount": 20,
"myScore": 200,
"oppScore": 40,
"candScores": [
50,
20
],
"rackLen": 5,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "123456789",
"moveCount": 5,
"myScore": 30,
"oppScore": 30,
"candScores": [
20,
20,
20
],
"rackLen": 7,
"bagLen": 14,
"playToWin": false,
"win": false,
"kind": "play",
"index": 0
},
{
"seed": "123456789",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": false,
"kind": "exchange",
"index": -1
},
{
"seed": "123456789",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 3,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "pass",
"index": -1
},
{
"seed": "123456789",
"moveCount": 15,
"myScore": 300,
"oppScore": 10,
"candScores": [
80,
40,
10
],
"rackLen": 7,
"bagLen": 6,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
},
{
"seed": "-123456789",
"moveCount": 3,
"myScore": 40,
"oppScore": 55,
"candScores": [
30,
12,
8
],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "-123456789",
"moveCount": 10,
"myScore": 120,
"oppScore": 90,
"candScores": [
25,
18,
5
],
"rackLen": 7,
"bagLen": 8,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
},
{
"seed": "-123456789",
"moveCount": 1,
"myScore": 0,
"oppScore": 0,
"candScores": [
60,
30,
15,
7
],
"rackLen": 7,
"bagLen": 30,
"playToWin": false,
"win": false,
"kind": "play",
"index": 3
},
{
"seed": "-123456789",
"moveCount": 20,
"myScore": 200,
"oppScore": 40,
"candScores": [
50,
20
],
"rackLen": 5,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "play",
"index": 1
},
{
"seed": "-123456789",
"moveCount": 5,
"myScore": 30,
"oppScore": 30,
"candScores": [
20,
20,
20
],
"rackLen": 7,
"bagLen": 14,
"playToWin": false,
"win": false,
"kind": "play",
"index": 0
},
{
"seed": "-123456789",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 7,
"bagLen": 20,
"playToWin": false,
"win": false,
"kind": "exchange",
"index": -1
},
{
"seed": "-123456789",
"moveCount": 8,
"myScore": 50,
"oppScore": 60,
"candScores": [],
"rackLen": 3,
"bagLen": 2,
"playToWin": false,
"win": false,
"kind": "pass",
"index": -1
},
{
"seed": "-123456789",
"moveCount": 15,
"myScore": 300,
"oppScore": 10,
"candScores": [
80,
40,
10
],
"rackLen": 7,
"bagLen": 6,
"playToWin": false,
"win": false,
"kind": "play",
"index": 2
}
]
}