Spaces:
Running
Running
Commit
·
f6a5e63
1
Parent(s):
172c3de
Update: fix disconnect state sync and dynamic socket URL
Browse filesChanges:
- Fix room showing "full" after player disconnect
- Clean up stale disconnected player entries
- Use window.location.origin for dynamic socket URL
- Add Vite proxy for socket.io support
- Rebuild frontend with latest changes
Co-Authored-By: Claude <noreply@anthropic.com>
- trigo-web/.env +1 -2
- trigo-web/app/dist/assets/{index-BCjNK5tk.js → index-BbzW5u0H.js} +0 -0
- trigo-web/app/dist/index.html +1 -1
- trigo-web/app/package-lock.json +755 -424
- trigo-web/app/package.json +30 -30
- trigo-web/app/src/composables/useSocket.ts +5 -13
- trigo-web/app/src/views/TrigoView.vue +9 -0
- trigo-web/app/tsconfig.json +31 -0
- trigo-web/app/tsconfig.node.json +10 -0
- trigo-web/app/vite.config.ts +14 -10
- trigo-web/backend/.env.local +0 -2
- trigo-web/backend/dist/backend/src/server.js +0 -2104
- trigo-web/backend/dist/server.js +0 -2104
- trigo-web/backend/package.json +2 -2
- trigo-web/backend/src/server.ts +1 -1
- trigo-web/backend/src/services/gameManager.ts +16 -5
- trigo-web/backend/src/sockets/gameSocket.ts +35 -7
- trigo-web/package.json +62 -63
- trigo-web/tests/game/debug_capture.test.ts +0 -44
- trigo-web/tests/game/debug_redo.test.ts +0 -42
- trigo-web/tests/game/trigoGame.core.test.ts +0 -300
- trigo-web/tests/game/trigoGame.fromTGN.test.ts +0 -319
- trigo-web/tests/game/trigoGame.history.test.ts +0 -301
- trigo-web/tests/game/trigoGame.parserInit.test.ts +0 -160
- trigo-web/tests/game/trigoGame.rules.test.ts +0 -356
- trigo-web/tests/game/trigoGame.state.test.ts +0 -406
- trigo-web/tests/game/trigoGame.tgn.test.ts +0 -229
- trigo-web/tests/game/verify_capture.test.ts +0 -79
- trigo-web/tests/mctsTerminalPropagation.test.ts +0 -257
- trigo-web/tests/testMCTSSingleStep.ts +0 -159
- trigo-web/tests/testMCTSWithVisits.ts +0 -212
- trigo-web/tests/tgn/19x19.tgn +0 -6
- trigo-web/tests/tgn/meta.tgn +0 -11
trigo-web/.env
CHANGED
|
@@ -14,7 +14,6 @@
|
|
| 14 |
# ============================================================================
|
| 15 |
|
| 16 |
# Backend Server URL
|
| 17 |
-
VITE_SERVER_URL=http://localhost:8157
|
| 18 |
|
| 19 |
# Vite Dev Server Configuration
|
| 20 |
VITE_HOST=0.0.0.0
|
|
@@ -39,7 +38,7 @@ PORT=3000
|
|
| 39 |
CLIENT_URL=http://localhost:5173
|
| 40 |
|
| 41 |
# Environment mode
|
| 42 |
-
NODE_ENV=
|
| 43 |
|
| 44 |
|
| 45 |
# ============================================================================
|
|
|
|
| 14 |
# ============================================================================
|
| 15 |
|
| 16 |
# Backend Server URL
|
|
|
|
| 17 |
|
| 18 |
# Vite Dev Server Configuration
|
| 19 |
VITE_HOST=0.0.0.0
|
|
|
|
| 38 |
CLIENT_URL=http://localhost:5173
|
| 39 |
|
| 40 |
# Environment mode
|
| 41 |
+
NODE_ENV=production
|
| 42 |
|
| 43 |
|
| 44 |
# ============================================================================
|
trigo-web/app/dist/assets/{index-BCjNK5tk.js → index-BbzW5u0H.js}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
trigo-web/app/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Trigo - 3D Go Game</title>
|
| 8 |
-
<script type="module" crossorigin src="/assets/index-
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-Siwlapuk.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
|
|
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Trigo - 3D Go Game</title>
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-BbzW5u0H.js"></script>
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-Siwlapuk.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
trigo-web/app/package-lock.json
CHANGED
|
@@ -10,7 +10,6 @@
|
|
| 10 |
"dependencies": {
|
| 11 |
"d3": "^7.9.0",
|
| 12 |
"d3-scale-chromatic": "^3.1.0",
|
| 13 |
-
"onnxruntime-web": "1.23.2",
|
| 14 |
"pinia": "^2.1.6",
|
| 15 |
"socket.io-client": "^4.5.2",
|
| 16 |
"three": "^0.156.1",
|
|
@@ -21,7 +20,7 @@
|
|
| 21 |
"@types/d3": "^7.4.3",
|
| 22 |
"@types/three": "^0.156.0",
|
| 23 |
"@vitejs/plugin-vue": "^5.2.4",
|
| 24 |
-
"sass": "^1.
|
| 25 |
"typescript": "^5.2.2",
|
| 26 |
"vite": "^5.4.21",
|
| 27 |
"vue-tsc": "^2.2.12"
|
|
@@ -36,19 +35,19 @@
|
|
| 36 |
}
|
| 37 |
},
|
| 38 |
"node_modules/@babel/helper-validator-identifier": {
|
| 39 |
-
"version": "7.
|
| 40 |
-
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.
|
| 41 |
-
"integrity": "sha512-
|
| 42 |
"engines": {
|
| 43 |
"node": ">=6.9.0"
|
| 44 |
}
|
| 45 |
},
|
| 46 |
"node_modules/@babel/parser": {
|
| 47 |
-
"version": "7.28.
|
| 48 |
-
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.
|
| 49 |
-
"integrity": "sha512-
|
| 50 |
"dependencies": {
|
| 51 |
-
"@babel/types": "^7.28.
|
| 52 |
},
|
| 53 |
"bin": {
|
| 54 |
"parser": "bin/babel-parser.js"
|
|
@@ -58,17 +57,23 @@
|
|
| 58 |
}
|
| 59 |
},
|
| 60 |
"node_modules/@babel/types": {
|
| 61 |
-
"version": "7.28.
|
| 62 |
-
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.
|
| 63 |
-
"integrity": "sha512-
|
| 64 |
"dependencies": {
|
| 65 |
"@babel/helper-string-parser": "^7.27.1",
|
| 66 |
-
"@babel/helper-validator-identifier": "^7.
|
| 67 |
},
|
| 68 |
"engines": {
|
| 69 |
"node": ">=6.9.0"
|
| 70 |
}
|
| 71 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
"node_modules/@esbuild/aix-ppc64": {
|
| 73 |
"version": "0.21.5",
|
| 74 |
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
|
@@ -443,17 +448,17 @@
|
|
| 443 |
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
| 444 |
},
|
| 445 |
"node_modules/@parcel/watcher": {
|
| 446 |
-
"version": "2.5.
|
| 447 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.
|
| 448 |
-
"integrity": "sha512-
|
| 449 |
"dev": true,
|
| 450 |
"hasInstallScript": true,
|
| 451 |
"optional": true,
|
| 452 |
"dependencies": {
|
| 453 |
-
"detect-libc": "^
|
| 454 |
"is-glob": "^4.0.3",
|
| 455 |
-
"
|
| 456 |
-
"
|
| 457 |
},
|
| 458 |
"engines": {
|
| 459 |
"node": ">= 10.0.0"
|
|
@@ -463,25 +468,25 @@
|
|
| 463 |
"url": "https://opencollective.com/parcel"
|
| 464 |
},
|
| 465 |
"optionalDependencies": {
|
| 466 |
-
"@parcel/watcher-android-arm64": "2.5.
|
| 467 |
-
"@parcel/watcher-darwin-arm64": "2.5.
|
| 468 |
-
"@parcel/watcher-darwin-x64": "2.5.
|
| 469 |
-
"@parcel/watcher-freebsd-x64": "2.5.
|
| 470 |
-
"@parcel/watcher-linux-arm-glibc": "2.5.
|
| 471 |
-
"@parcel/watcher-linux-arm-musl": "2.5.
|
| 472 |
-
"@parcel/watcher-linux-arm64-glibc": "2.5.
|
| 473 |
-
"@parcel/watcher-linux-arm64-musl": "2.5.
|
| 474 |
-
"@parcel/watcher-linux-x64-glibc": "2.5.
|
| 475 |
-
"@parcel/watcher-linux-x64-musl": "2.5.
|
| 476 |
-
"@parcel/watcher-win32-arm64": "2.5.
|
| 477 |
-
"@parcel/watcher-win32-ia32": "2.5.
|
| 478 |
-
"@parcel/watcher-win32-x64": "2.5.
|
| 479 |
}
|
| 480 |
},
|
| 481 |
"node_modules/@parcel/watcher-android-arm64": {
|
| 482 |
-
"version": "2.5.
|
| 483 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.
|
| 484 |
-
"integrity": "sha512-
|
| 485 |
"cpu": [
|
| 486 |
"arm64"
|
| 487 |
],
|
|
@@ -499,9 +504,9 @@
|
|
| 499 |
}
|
| 500 |
},
|
| 501 |
"node_modules/@parcel/watcher-darwin-arm64": {
|
| 502 |
-
"version": "2.5.
|
| 503 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.
|
| 504 |
-
"integrity": "sha512-
|
| 505 |
"cpu": [
|
| 506 |
"arm64"
|
| 507 |
],
|
|
@@ -519,9 +524,9 @@
|
|
| 519 |
}
|
| 520 |
},
|
| 521 |
"node_modules/@parcel/watcher-darwin-x64": {
|
| 522 |
-
"version": "2.5.
|
| 523 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.
|
| 524 |
-
"integrity": "sha512-
|
| 525 |
"cpu": [
|
| 526 |
"x64"
|
| 527 |
],
|
|
@@ -539,9 +544,9 @@
|
|
| 539 |
}
|
| 540 |
},
|
| 541 |
"node_modules/@parcel/watcher-freebsd-x64": {
|
| 542 |
-
"version": "2.5.
|
| 543 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.
|
| 544 |
-
"integrity": "sha512-
|
| 545 |
"cpu": [
|
| 546 |
"x64"
|
| 547 |
],
|
|
@@ -559,9 +564,9 @@
|
|
| 559 |
}
|
| 560 |
},
|
| 561 |
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
| 562 |
-
"version": "2.5.
|
| 563 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.
|
| 564 |
-
"integrity": "sha512-
|
| 565 |
"cpu": [
|
| 566 |
"arm"
|
| 567 |
],
|
|
@@ -579,9 +584,9 @@
|
|
| 579 |
}
|
| 580 |
},
|
| 581 |
"node_modules/@parcel/watcher-linux-arm-musl": {
|
| 582 |
-
"version": "2.5.
|
| 583 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.
|
| 584 |
-
"integrity": "sha512-
|
| 585 |
"cpu": [
|
| 586 |
"arm"
|
| 587 |
],
|
|
@@ -599,9 +604,9 @@
|
|
| 599 |
}
|
| 600 |
},
|
| 601 |
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
| 602 |
-
"version": "2.5.
|
| 603 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.
|
| 604 |
-
"integrity": "sha512-
|
| 605 |
"cpu": [
|
| 606 |
"arm64"
|
| 607 |
],
|
|
@@ -619,9 +624,9 @@
|
|
| 619 |
}
|
| 620 |
},
|
| 621 |
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
| 622 |
-
"version": "2.5.
|
| 623 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.
|
| 624 |
-
"integrity": "sha512-
|
| 625 |
"cpu": [
|
| 626 |
"arm64"
|
| 627 |
],
|
|
@@ -639,9 +644,9 @@
|
|
| 639 |
}
|
| 640 |
},
|
| 641 |
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
| 642 |
-
"version": "2.5.
|
| 643 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.
|
| 644 |
-
"integrity": "sha512-
|
| 645 |
"cpu": [
|
| 646 |
"x64"
|
| 647 |
],
|
|
@@ -659,9 +664,9 @@
|
|
| 659 |
}
|
| 660 |
},
|
| 661 |
"node_modules/@parcel/watcher-linux-x64-musl": {
|
| 662 |
-
"version": "2.5.
|
| 663 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.
|
| 664 |
-
"integrity": "sha512-
|
| 665 |
"cpu": [
|
| 666 |
"x64"
|
| 667 |
],
|
|
@@ -679,9 +684,9 @@
|
|
| 679 |
}
|
| 680 |
},
|
| 681 |
"node_modules/@parcel/watcher-win32-arm64": {
|
| 682 |
-
"version": "2.5.
|
| 683 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.
|
| 684 |
-
"integrity": "sha512-
|
| 685 |
"cpu": [
|
| 686 |
"arm64"
|
| 687 |
],
|
|
@@ -699,9 +704,9 @@
|
|
| 699 |
}
|
| 700 |
},
|
| 701 |
"node_modules/@parcel/watcher-win32-ia32": {
|
| 702 |
-
"version": "2.5.
|
| 703 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.
|
| 704 |
-
"integrity": "sha512-
|
| 705 |
"cpu": [
|
| 706 |
"ia32"
|
| 707 |
],
|
|
@@ -719,9 +724,9 @@
|
|
| 719 |
}
|
| 720 |
},
|
| 721 |
"node_modules/@parcel/watcher-win32-x64": {
|
| 722 |
-
"version": "2.5.
|
| 723 |
-
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.
|
| 724 |
-
"integrity": "sha512-
|
| 725 |
"cpu": [
|
| 726 |
"x64"
|
| 727 |
],
|
|
@@ -738,64 +743,10 @@
|
|
| 738 |
"url": "https://opencollective.com/parcel"
|
| 739 |
}
|
| 740 |
},
|
| 741 |
-
"node_modules/@protobufjs/aspromise": {
|
| 742 |
-
"version": "1.1.2",
|
| 743 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
| 744 |
-
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
| 745 |
-
},
|
| 746 |
-
"node_modules/@protobufjs/base64": {
|
| 747 |
-
"version": "1.1.2",
|
| 748 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
| 749 |
-
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
| 750 |
-
},
|
| 751 |
-
"node_modules/@protobufjs/codegen": {
|
| 752 |
-
"version": "2.0.4",
|
| 753 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
| 754 |
-
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
| 755 |
-
},
|
| 756 |
-
"node_modules/@protobufjs/eventemitter": {
|
| 757 |
-
"version": "1.1.0",
|
| 758 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
| 759 |
-
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
| 760 |
-
},
|
| 761 |
-
"node_modules/@protobufjs/fetch": {
|
| 762 |
-
"version": "1.1.0",
|
| 763 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
| 764 |
-
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
| 765 |
-
"dependencies": {
|
| 766 |
-
"@protobufjs/aspromise": "^1.1.1",
|
| 767 |
-
"@protobufjs/inquire": "^1.1.0"
|
| 768 |
-
}
|
| 769 |
-
},
|
| 770 |
-
"node_modules/@protobufjs/float": {
|
| 771 |
-
"version": "1.0.2",
|
| 772 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
| 773 |
-
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
| 774 |
-
},
|
| 775 |
-
"node_modules/@protobufjs/inquire": {
|
| 776 |
-
"version": "1.1.0",
|
| 777 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
| 778 |
-
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
| 779 |
-
},
|
| 780 |
-
"node_modules/@protobufjs/path": {
|
| 781 |
-
"version": "1.1.2",
|
| 782 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
| 783 |
-
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
| 784 |
-
},
|
| 785 |
-
"node_modules/@protobufjs/pool": {
|
| 786 |
-
"version": "1.1.0",
|
| 787 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
| 788 |
-
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
| 789 |
-
},
|
| 790 |
-
"node_modules/@protobufjs/utf8": {
|
| 791 |
-
"version": "1.1.0",
|
| 792 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
| 793 |
-
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
| 794 |
-
},
|
| 795 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 796 |
-
"version": "4.
|
| 797 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.
|
| 798 |
-
"integrity": "sha512-
|
| 799 |
"cpu": [
|
| 800 |
"arm"
|
| 801 |
],
|
|
@@ -806,9 +757,9 @@
|
|
| 806 |
]
|
| 807 |
},
|
| 808 |
"node_modules/@rollup/rollup-android-arm64": {
|
| 809 |
-
"version": "4.
|
| 810 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.
|
| 811 |
-
"integrity": "sha512-
|
| 812 |
"cpu": [
|
| 813 |
"arm64"
|
| 814 |
],
|
|
@@ -819,9 +770,9 @@
|
|
| 819 |
]
|
| 820 |
},
|
| 821 |
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 822 |
-
"version": "4.
|
| 823 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.
|
| 824 |
-
"integrity": "sha512-
|
| 825 |
"cpu": [
|
| 826 |
"arm64"
|
| 827 |
],
|
|
@@ -832,9 +783,9 @@
|
|
| 832 |
]
|
| 833 |
},
|
| 834 |
"node_modules/@rollup/rollup-darwin-x64": {
|
| 835 |
-
"version": "4.
|
| 836 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.
|
| 837 |
-
"integrity": "sha512-
|
| 838 |
"cpu": [
|
| 839 |
"x64"
|
| 840 |
],
|
|
@@ -845,9 +796,9 @@
|
|
| 845 |
]
|
| 846 |
},
|
| 847 |
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 848 |
-
"version": "4.
|
| 849 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.
|
| 850 |
-
"integrity": "sha512
|
| 851 |
"cpu": [
|
| 852 |
"arm64"
|
| 853 |
],
|
|
@@ -858,9 +809,9 @@
|
|
| 858 |
]
|
| 859 |
},
|
| 860 |
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 861 |
-
"version": "4.
|
| 862 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.
|
| 863 |
-
"integrity": "sha512-
|
| 864 |
"cpu": [
|
| 865 |
"x64"
|
| 866 |
],
|
|
@@ -871,9 +822,9 @@
|
|
| 871 |
]
|
| 872 |
},
|
| 873 |
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 874 |
-
"version": "4.
|
| 875 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.
|
| 876 |
-
"integrity": "sha512-
|
| 877 |
"cpu": [
|
| 878 |
"arm"
|
| 879 |
],
|
|
@@ -884,9 +835,9 @@
|
|
| 884 |
]
|
| 885 |
},
|
| 886 |
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 887 |
-
"version": "4.
|
| 888 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.
|
| 889 |
-
"integrity": "sha512-
|
| 890 |
"cpu": [
|
| 891 |
"arm"
|
| 892 |
],
|
|
@@ -897,9 +848,9 @@
|
|
| 897 |
]
|
| 898 |
},
|
| 899 |
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 900 |
-
"version": "4.
|
| 901 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.
|
| 902 |
-
"integrity": "sha512-
|
| 903 |
"cpu": [
|
| 904 |
"arm64"
|
| 905 |
],
|
|
@@ -910,9 +861,9 @@
|
|
| 910 |
]
|
| 911 |
},
|
| 912 |
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 913 |
-
"version": "4.
|
| 914 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.
|
| 915 |
-
"integrity": "sha512-
|
| 916 |
"cpu": [
|
| 917 |
"arm64"
|
| 918 |
],
|
|
@@ -923,22 +874,9 @@
|
|
| 923 |
]
|
| 924 |
},
|
| 925 |
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 926 |
-
"version": "4.
|
| 927 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.
|
| 928 |
-
"integrity": "sha512-
|
| 929 |
-
"cpu": [
|
| 930 |
-
"loong64"
|
| 931 |
-
],
|
| 932 |
-
"dev": true,
|
| 933 |
-
"optional": true,
|
| 934 |
-
"os": [
|
| 935 |
-
"linux"
|
| 936 |
-
]
|
| 937 |
-
},
|
| 938 |
-
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 939 |
-
"version": "4.55.1",
|
| 940 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
|
| 941 |
-
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
|
| 942 |
"cpu": [
|
| 943 |
"loong64"
|
| 944 |
],
|
|
@@ -949,22 +887,9 @@
|
|
| 949 |
]
|
| 950 |
},
|
| 951 |
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 952 |
-
"version": "4.
|
| 953 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.
|
| 954 |
-
"integrity": "sha512-
|
| 955 |
-
"cpu": [
|
| 956 |
-
"ppc64"
|
| 957 |
-
],
|
| 958 |
-
"dev": true,
|
| 959 |
-
"optional": true,
|
| 960 |
-
"os": [
|
| 961 |
-
"linux"
|
| 962 |
-
]
|
| 963 |
-
},
|
| 964 |
-
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 965 |
-
"version": "4.55.1",
|
| 966 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
|
| 967 |
-
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
|
| 968 |
"cpu": [
|
| 969 |
"ppc64"
|
| 970 |
],
|
|
@@ -975,9 +900,9 @@
|
|
| 975 |
]
|
| 976 |
},
|
| 977 |
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 978 |
-
"version": "4.
|
| 979 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.
|
| 980 |
-
"integrity": "sha512-
|
| 981 |
"cpu": [
|
| 982 |
"riscv64"
|
| 983 |
],
|
|
@@ -988,9 +913,9 @@
|
|
| 988 |
]
|
| 989 |
},
|
| 990 |
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 991 |
-
"version": "4.
|
| 992 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.
|
| 993 |
-
"integrity": "sha512-
|
| 994 |
"cpu": [
|
| 995 |
"riscv64"
|
| 996 |
],
|
|
@@ -1001,9 +926,9 @@
|
|
| 1001 |
]
|
| 1002 |
},
|
| 1003 |
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 1004 |
-
"version": "4.
|
| 1005 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.
|
| 1006 |
-
"integrity": "sha512
|
| 1007 |
"cpu": [
|
| 1008 |
"s390x"
|
| 1009 |
],
|
|
@@ -1014,9 +939,9 @@
|
|
| 1014 |
]
|
| 1015 |
},
|
| 1016 |
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 1017 |
-
"version": "4.
|
| 1018 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.
|
| 1019 |
-
"integrity": "sha512-
|
| 1020 |
"cpu": [
|
| 1021 |
"x64"
|
| 1022 |
],
|
|
@@ -1027,9 +952,9 @@
|
|
| 1027 |
]
|
| 1028 |
},
|
| 1029 |
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 1030 |
-
"version": "4.
|
| 1031 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.
|
| 1032 |
-
"integrity": "sha512-
|
| 1033 |
"cpu": [
|
| 1034 |
"x64"
|
| 1035 |
],
|
|
@@ -1039,23 +964,10 @@
|
|
| 1039 |
"linux"
|
| 1040 |
]
|
| 1041 |
},
|
| 1042 |
-
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 1043 |
-
"version": "4.55.1",
|
| 1044 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
|
| 1045 |
-
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
|
| 1046 |
-
"cpu": [
|
| 1047 |
-
"x64"
|
| 1048 |
-
],
|
| 1049 |
-
"dev": true,
|
| 1050 |
-
"optional": true,
|
| 1051 |
-
"os": [
|
| 1052 |
-
"openbsd"
|
| 1053 |
-
]
|
| 1054 |
-
},
|
| 1055 |
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 1056 |
-
"version": "4.
|
| 1057 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.
|
| 1058 |
-
"integrity": "sha512-
|
| 1059 |
"cpu": [
|
| 1060 |
"arm64"
|
| 1061 |
],
|
|
@@ -1066,9 +978,9 @@
|
|
| 1066 |
]
|
| 1067 |
},
|
| 1068 |
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1069 |
-
"version": "4.
|
| 1070 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.
|
| 1071 |
-
"integrity": "sha512-
|
| 1072 |
"cpu": [
|
| 1073 |
"arm64"
|
| 1074 |
],
|
|
@@ -1079,9 +991,9 @@
|
|
| 1079 |
]
|
| 1080 |
},
|
| 1081 |
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1082 |
-
"version": "4.
|
| 1083 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.
|
| 1084 |
-
"integrity": "sha512-
|
| 1085 |
"cpu": [
|
| 1086 |
"ia32"
|
| 1087 |
],
|
|
@@ -1092,9 +1004,9 @@
|
|
| 1092 |
]
|
| 1093 |
},
|
| 1094 |
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1095 |
-
"version": "4.
|
| 1096 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.
|
| 1097 |
-
"integrity": "sha512-
|
| 1098 |
"cpu": [
|
| 1099 |
"x64"
|
| 1100 |
],
|
|
@@ -1105,9 +1017,9 @@
|
|
| 1105 |
]
|
| 1106 |
},
|
| 1107 |
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1108 |
-
"version": "4.
|
| 1109 |
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.
|
| 1110 |
-
"integrity": "sha512-
|
| 1111 |
"cpu": [
|
| 1112 |
"x64"
|
| 1113 |
],
|
|
@@ -1330,9 +1242,9 @@
|
|
| 1330 |
"dev": true
|
| 1331 |
},
|
| 1332 |
"node_modules/@types/d3-shape": {
|
| 1333 |
-
"version": "3.1.
|
| 1334 |
-
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.
|
| 1335 |
-
"integrity": "sha512-
|
| 1336 |
"dev": true,
|
| 1337 |
"dependencies": {
|
| 1338 |
"@types/d3-path": "*"
|
|
@@ -1388,9 +1300,12 @@
|
|
| 1388 |
"dev": true
|
| 1389 |
},
|
| 1390 |
"node_modules/@types/node": {
|
| 1391 |
-
"version": "
|
| 1392 |
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-
|
| 1393 |
-
"integrity": "sha512-
|
|
|
|
|
|
|
|
|
|
| 1394 |
"dependencies": {
|
| 1395 |
"undici-types": "~7.16.0"
|
| 1396 |
}
|
|
@@ -1459,49 +1374,49 @@
|
|
| 1459 |
}
|
| 1460 |
},
|
| 1461 |
"node_modules/@vue/compiler-core": {
|
| 1462 |
-
"version": "3.5.
|
| 1463 |
-
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.
|
| 1464 |
-
"integrity": "sha512-
|
| 1465 |
"dependencies": {
|
| 1466 |
-
"@babel/parser": "^7.28.
|
| 1467 |
-
"@vue/shared": "3.5.
|
| 1468 |
-
"entities": "^
|
| 1469 |
"estree-walker": "^2.0.2",
|
| 1470 |
"source-map-js": "^1.2.1"
|
| 1471 |
}
|
| 1472 |
},
|
| 1473 |
"node_modules/@vue/compiler-dom": {
|
| 1474 |
-
"version": "3.5.
|
| 1475 |
-
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.
|
| 1476 |
-
"integrity": "sha512-
|
| 1477 |
"dependencies": {
|
| 1478 |
-
"@vue/compiler-core": "3.5.
|
| 1479 |
-
"@vue/shared": "3.5.
|
| 1480 |
}
|
| 1481 |
},
|
| 1482 |
"node_modules/@vue/compiler-sfc": {
|
| 1483 |
-
"version": "3.5.
|
| 1484 |
-
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.
|
| 1485 |
-
"integrity": "sha512-
|
| 1486 |
-
"dependencies": {
|
| 1487 |
-
"@babel/parser": "^7.28.
|
| 1488 |
-
"@vue/compiler-core": "3.5.
|
| 1489 |
-
"@vue/compiler-dom": "3.5.
|
| 1490 |
-
"@vue/compiler-ssr": "3.5.
|
| 1491 |
-
"@vue/shared": "3.5.
|
| 1492 |
"estree-walker": "^2.0.2",
|
| 1493 |
-
"magic-string": "^0.30.
|
| 1494 |
"postcss": "^8.5.6",
|
| 1495 |
"source-map-js": "^1.2.1"
|
| 1496 |
}
|
| 1497 |
},
|
| 1498 |
"node_modules/@vue/compiler-ssr": {
|
| 1499 |
-
"version": "3.5.
|
| 1500 |
-
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.
|
| 1501 |
-
"integrity": "sha512-
|
| 1502 |
"dependencies": {
|
| 1503 |
-
"@vue/compiler-dom": "3.5.
|
| 1504 |
-
"@vue/shared": "3.5.
|
| 1505 |
}
|
| 1506 |
},
|
| 1507 |
"node_modules/@vue/compiler-vue2": {
|
|
@@ -1544,49 +1459,49 @@
|
|
| 1544 |
}
|
| 1545 |
},
|
| 1546 |
"node_modules/@vue/reactivity": {
|
| 1547 |
-
"version": "3.5.
|
| 1548 |
-
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.
|
| 1549 |
-
"integrity": "sha512-
|
| 1550 |
"dependencies": {
|
| 1551 |
-
"@vue/shared": "3.5.
|
| 1552 |
}
|
| 1553 |
},
|
| 1554 |
"node_modules/@vue/runtime-core": {
|
| 1555 |
-
"version": "3.5.
|
| 1556 |
-
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.
|
| 1557 |
-
"integrity": "sha512-
|
| 1558 |
"dependencies": {
|
| 1559 |
-
"@vue/reactivity": "3.5.
|
| 1560 |
-
"@vue/shared": "3.5.
|
| 1561 |
}
|
| 1562 |
},
|
| 1563 |
"node_modules/@vue/runtime-dom": {
|
| 1564 |
-
"version": "3.5.
|
| 1565 |
-
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.
|
| 1566 |
-
"integrity": "sha512-
|
| 1567 |
"dependencies": {
|
| 1568 |
-
"@vue/reactivity": "3.5.
|
| 1569 |
-
"@vue/runtime-core": "3.5.
|
| 1570 |
-
"@vue/shared": "3.5.
|
| 1571 |
-
"csstype": "^3.
|
| 1572 |
}
|
| 1573 |
},
|
| 1574 |
"node_modules/@vue/server-renderer": {
|
| 1575 |
-
"version": "3.5.
|
| 1576 |
-
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.
|
| 1577 |
-
"integrity": "sha512-
|
| 1578 |
"dependencies": {
|
| 1579 |
-
"@vue/compiler-ssr": "3.5.
|
| 1580 |
-
"@vue/shared": "3.5.
|
| 1581 |
},
|
| 1582 |
"peerDependencies": {
|
| 1583 |
-
"vue": "3.5.
|
| 1584 |
}
|
| 1585 |
},
|
| 1586 |
"node_modules/@vue/shared": {
|
| 1587 |
-
"version": "3.5.
|
| 1588 |
-
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.
|
| 1589 |
-
"integrity": "sha512-
|
| 1590 |
},
|
| 1591 |
"node_modules/alien-signals": {
|
| 1592 |
"version": "1.0.13",
|
|
@@ -1609,11 +1524,31 @@
|
|
| 1609 |
"balanced-match": "^1.0.0"
|
| 1610 |
}
|
| 1611 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1612 |
"node_modules/chokidar": {
|
| 1613 |
"version": "4.0.3",
|
| 1614 |
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
| 1615 |
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
| 1616 |
"dev": true,
|
|
|
|
| 1617 |
"dependencies": {
|
| 1618 |
"readdirp": "^4.0.1"
|
| 1619 |
},
|
|
@@ -1624,6 +1559,12 @@
|
|
| 1624 |
"url": "https://paulmillr.com/funding/"
|
| 1625 |
}
|
| 1626 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1627 |
"node_modules/commander": {
|
| 1628 |
"version": "7.2.0",
|
| 1629 |
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
|
@@ -1633,9 +1574,9 @@
|
|
| 1633 |
}
|
| 1634 |
},
|
| 1635 |
"node_modules/csstype": {
|
| 1636 |
-
"version": "3.
|
| 1637 |
-
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.
|
| 1638 |
-
"integrity": "sha512-
|
| 1639 |
},
|
| 1640 |
"node_modules/d3": {
|
| 1641 |
"version": "7.9.0",
|
|
@@ -2014,9 +1955,9 @@
|
|
| 2014 |
"dev": true
|
| 2015 |
},
|
| 2016 |
"node_modules/debug": {
|
| 2017 |
-
"version": "4.
|
| 2018 |
-
"resolved": "https://registry.npmjs.org/debug/-/debug-4.
|
| 2019 |
-
"integrity": "sha512-
|
| 2020 |
"dependencies": {
|
| 2021 |
"ms": "^2.1.3"
|
| 2022 |
},
|
|
@@ -2038,24 +1979,27 @@
|
|
| 2038 |
}
|
| 2039 |
},
|
| 2040 |
"node_modules/detect-libc": {
|
| 2041 |
-
"version": "
|
| 2042 |
-
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-
|
| 2043 |
-
"integrity": "sha512-
|
| 2044 |
"dev": true,
|
| 2045 |
"optional": true,
|
|
|
|
|
|
|
|
|
|
| 2046 |
"engines": {
|
| 2047 |
-
"node": ">=
|
| 2048 |
}
|
| 2049 |
},
|
| 2050 |
"node_modules/engine.io-client": {
|
| 2051 |
-
"version": "6.6.
|
| 2052 |
-
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.
|
| 2053 |
-
"integrity": "sha512
|
| 2054 |
"dependencies": {
|
| 2055 |
"@socket.io/component-emitter": "~3.1.0",
|
| 2056 |
-
"debug": "~4.
|
| 2057 |
"engine.io-parser": "~5.2.1",
|
| 2058 |
-
"ws": "~8.
|
| 2059 |
"xmlhttprequest-ssl": "~2.1.1"
|
| 2060 |
}
|
| 2061 |
},
|
|
@@ -2068,9 +2012,9 @@
|
|
| 2068 |
}
|
| 2069 |
},
|
| 2070 |
"node_modules/entities": {
|
| 2071 |
-
"version": "
|
| 2072 |
-
"resolved": "https://registry.npmjs.org/entities/-/entities-
|
| 2073 |
-
"integrity": "sha512-
|
| 2074 |
"engines": {
|
| 2075 |
"node": ">=0.12"
|
| 2076 |
},
|
|
@@ -2127,10 +2071,18 @@
|
|
| 2127 |
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
| 2128 |
"dev": true
|
| 2129 |
},
|
| 2130 |
-
"node_modules/
|
| 2131 |
-
"version": "
|
| 2132 |
-
"resolved": "https://registry.npmjs.org/
|
| 2133 |
-
"integrity": "sha512-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2134 |
},
|
| 2135 |
"node_modules/fsevents": {
|
| 2136 |
"version": "2.3.3",
|
|
@@ -2146,10 +2098,14 @@
|
|
| 2146 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 2147 |
}
|
| 2148 |
},
|
| 2149 |
-
"node_modules/
|
| 2150 |
-
"version": "
|
| 2151 |
-
"resolved": "https://registry.npmjs.org/
|
| 2152 |
-
"integrity": "sha512-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2153 |
},
|
| 2154 |
"node_modules/he": {
|
| 2155 |
"version": "1.2.0",
|
|
@@ -2208,15 +2164,20 @@
|
|
| 2208 |
"node": ">=0.10.0"
|
| 2209 |
}
|
| 2210 |
},
|
| 2211 |
-
"node_modules/
|
| 2212 |
-
"version": "
|
| 2213 |
-
"resolved": "https://registry.npmjs.org/
|
| 2214 |
-
"integrity": "sha512-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2215 |
},
|
| 2216 |
"node_modules/magic-string": {
|
| 2217 |
-
"version": "0.30.
|
| 2218 |
-
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.
|
| 2219 |
-
"integrity": "sha512-
|
| 2220 |
"dependencies": {
|
| 2221 |
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 2222 |
}
|
|
@@ -2227,6 +2188,20 @@
|
|
| 2227 |
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
| 2228 |
"dev": true
|
| 2229 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2230 |
"node_modules/minimatch": {
|
| 2231 |
"version": "9.0.5",
|
| 2232 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
|
@@ -2277,24 +2252,6 @@
|
|
| 2277 |
"dev": true,
|
| 2278 |
"optional": true
|
| 2279 |
},
|
| 2280 |
-
"node_modules/onnxruntime-common": {
|
| 2281 |
-
"version": "1.23.2",
|
| 2282 |
-
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.23.2.tgz",
|
| 2283 |
-
"integrity": "sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w=="
|
| 2284 |
-
},
|
| 2285 |
-
"node_modules/onnxruntime-web": {
|
| 2286 |
-
"version": "1.23.2",
|
| 2287 |
-
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.23.2.tgz",
|
| 2288 |
-
"integrity": "sha512-T09JUtMn+CZLk3mFwqiH0lgQf+4S7+oYHHtk6uhaYAAJI95bTcKi5bOOZYwORXfS/RLZCjDDEXGWIuOCAFlEjg==",
|
| 2289 |
-
"dependencies": {
|
| 2290 |
-
"flatbuffers": "^25.1.24",
|
| 2291 |
-
"guid-typescript": "^1.0.9",
|
| 2292 |
-
"long": "^5.2.3",
|
| 2293 |
-
"onnxruntime-common": "1.23.2",
|
| 2294 |
-
"platform": "^1.3.6",
|
| 2295 |
-
"protobufjs": "^7.2.4"
|
| 2296 |
-
}
|
| 2297 |
-
},
|
| 2298 |
"node_modules/path-browserify": {
|
| 2299 |
"version": "1.0.1",
|
| 2300 |
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
|
@@ -2307,13 +2264,13 @@
|
|
| 2307 |
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
| 2308 |
},
|
| 2309 |
"node_modules/picomatch": {
|
| 2310 |
-
"version": "
|
| 2311 |
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-
|
| 2312 |
-
"integrity": "sha512-
|
| 2313 |
"dev": true,
|
| 2314 |
"optional": true,
|
| 2315 |
"engines": {
|
| 2316 |
-
"node": ">=
|
| 2317 |
},
|
| 2318 |
"funding": {
|
| 2319 |
"url": "https://github.com/sponsors/jonschlinkert"
|
|
@@ -2340,11 +2297,6 @@
|
|
| 2340 |
}
|
| 2341 |
}
|
| 2342 |
},
|
| 2343 |
-
"node_modules/platform": {
|
| 2344 |
-
"version": "1.3.6",
|
| 2345 |
-
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
|
| 2346 |
-
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
|
| 2347 |
-
},
|
| 2348 |
"node_modules/postcss": {
|
| 2349 |
"version": "8.5.6",
|
| 2350 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
|
@@ -2372,34 +2324,12 @@
|
|
| 2372 |
"node": "^10 || ^12 || >=14"
|
| 2373 |
}
|
| 2374 |
},
|
| 2375 |
-
"node_modules/protobufjs": {
|
| 2376 |
-
"version": "7.5.4",
|
| 2377 |
-
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
| 2378 |
-
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
| 2379 |
-
"hasInstallScript": true,
|
| 2380 |
-
"dependencies": {
|
| 2381 |
-
"@protobufjs/aspromise": "^1.1.2",
|
| 2382 |
-
"@protobufjs/base64": "^1.1.2",
|
| 2383 |
-
"@protobufjs/codegen": "^2.0.4",
|
| 2384 |
-
"@protobufjs/eventemitter": "^1.1.0",
|
| 2385 |
-
"@protobufjs/fetch": "^1.1.0",
|
| 2386 |
-
"@protobufjs/float": "^1.0.2",
|
| 2387 |
-
"@protobufjs/inquire": "^1.1.0",
|
| 2388 |
-
"@protobufjs/path": "^1.1.2",
|
| 2389 |
-
"@protobufjs/pool": "^1.1.0",
|
| 2390 |
-
"@protobufjs/utf8": "^1.1.0",
|
| 2391 |
-
"@types/node": ">=13.7.0",
|
| 2392 |
-
"long": "^5.0.0"
|
| 2393 |
-
},
|
| 2394 |
-
"engines": {
|
| 2395 |
-
"node": ">=12.0.0"
|
| 2396 |
-
}
|
| 2397 |
-
},
|
| 2398 |
"node_modules/readdirp": {
|
| 2399 |
"version": "4.1.2",
|
| 2400 |
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
| 2401 |
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
| 2402 |
"dev": true,
|
|
|
|
| 2403 |
"engines": {
|
| 2404 |
"node": ">= 14.18.0"
|
| 2405 |
},
|
|
@@ -2414,9 +2344,9 @@
|
|
| 2414 |
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
| 2415 |
},
|
| 2416 |
"node_modules/rollup": {
|
| 2417 |
-
"version": "4.
|
| 2418 |
-
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.
|
| 2419 |
-
"integrity": "sha512-
|
| 2420 |
"dev": true,
|
| 2421 |
"dependencies": {
|
| 2422 |
"@types/estree": "1.0.8"
|
|
@@ -2429,31 +2359,28 @@
|
|
| 2429 |
"npm": ">=8.0.0"
|
| 2430 |
},
|
| 2431 |
"optionalDependencies": {
|
| 2432 |
-
"@rollup/rollup-android-arm-eabi": "4.
|
| 2433 |
-
"@rollup/rollup-android-arm64": "4.
|
| 2434 |
-
"@rollup/rollup-darwin-arm64": "4.
|
| 2435 |
-
"@rollup/rollup-darwin-x64": "4.
|
| 2436 |
-
"@rollup/rollup-freebsd-arm64": "4.
|
| 2437 |
-
"@rollup/rollup-freebsd-x64": "4.
|
| 2438 |
-
"@rollup/rollup-linux-arm-gnueabihf": "4.
|
| 2439 |
-
"@rollup/rollup-linux-arm-musleabihf": "4.
|
| 2440 |
-
"@rollup/rollup-linux-arm64-gnu": "4.
|
| 2441 |
-
"@rollup/rollup-linux-arm64-musl": "4.
|
| 2442 |
-
"@rollup/rollup-linux-loong64-gnu": "4.
|
| 2443 |
-
"@rollup/rollup-linux-
|
| 2444 |
-
"@rollup/rollup-linux-
|
| 2445 |
-
"@rollup/rollup-linux-
|
| 2446 |
-
"@rollup/rollup-linux-
|
| 2447 |
-
"@rollup/rollup-linux-
|
| 2448 |
-
"@rollup/rollup-linux-
|
| 2449 |
-
"@rollup/rollup-
|
| 2450 |
-
"@rollup/rollup-
|
| 2451 |
-
"@rollup/rollup-
|
| 2452 |
-
"@rollup/rollup-
|
| 2453 |
-
"@rollup/rollup-win32-
|
| 2454 |
-
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
|
| 2455 |
-
"@rollup/rollup-win32-x64-gnu": "4.55.1",
|
| 2456 |
-
"@rollup/rollup-win32-x64-msvc": "4.55.1",
|
| 2457 |
"fsevents": "~2.3.2"
|
| 2458 |
}
|
| 2459 |
},
|
|
@@ -2462,16 +2389,26 @@
|
|
| 2462 |
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
| 2463 |
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
|
| 2464 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2465 |
"node_modules/safer-buffer": {
|
| 2466 |
"version": "2.1.2",
|
| 2467 |
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 2468 |
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
| 2469 |
},
|
| 2470 |
"node_modules/sass": {
|
| 2471 |
-
"version": "1.
|
| 2472 |
-
"resolved": "https://registry.npmjs.org/sass/-/sass-1.
|
| 2473 |
-
"integrity": "sha512-
|
| 2474 |
"dev": true,
|
|
|
|
| 2475 |
"dependencies": {
|
| 2476 |
"chokidar": "^4.0.0",
|
| 2477 |
"immutable": "^5.0.2",
|
|
@@ -2487,13 +2424,343 @@
|
|
| 2487 |
"@parcel/watcher": "^2.4.1"
|
| 2488 |
}
|
| 2489 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2490 |
"node_modules/socket.io-client": {
|
| 2491 |
-
"version": "4.8.
|
| 2492 |
-
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.
|
| 2493 |
-
"integrity": "sha512-
|
| 2494 |
"dependencies": {
|
| 2495 |
"@socket.io/component-emitter": "~3.1.0",
|
| 2496 |
-
"debug": "~4.
|
| 2497 |
"engine.io-client": "~6.6.1",
|
| 2498 |
"socket.io-parser": "~4.2.4"
|
| 2499 |
},
|
|
@@ -2502,12 +2769,12 @@
|
|
| 2502 |
}
|
| 2503 |
},
|
| 2504 |
"node_modules/socket.io-parser": {
|
| 2505 |
-
"version": "4.2.
|
| 2506 |
-
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.
|
| 2507 |
-
"integrity": "sha512
|
| 2508 |
"dependencies": {
|
| 2509 |
"@socket.io/component-emitter": "~3.1.0",
|
| 2510 |
-
"debug": "~4.
|
| 2511 |
},
|
| 2512 |
"engines": {
|
| 2513 |
"node": ">=10.0.0"
|
|
@@ -2521,11 +2788,66 @@
|
|
| 2521 |
"node": ">=0.10.0"
|
| 2522 |
}
|
| 2523 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2524 |
"node_modules/three": {
|
| 2525 |
"version": "0.156.1",
|
| 2526 |
"resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
|
| 2527 |
"integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ=="
|
| 2528 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2529 |
"node_modules/typescript": {
|
| 2530 |
"version": "5.9.3",
|
| 2531 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
@@ -2542,7 +2864,16 @@
|
|
| 2542 |
"node_modules/undici-types": {
|
| 2543 |
"version": "7.16.0",
|
| 2544 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
| 2545 |
-
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2546 |
},
|
| 2547 |
"node_modules/vite": {
|
| 2548 |
"version": "5.4.21",
|
|
@@ -2610,15 +2941,15 @@
|
|
| 2610 |
"dev": true
|
| 2611 |
},
|
| 2612 |
"node_modules/vue": {
|
| 2613 |
-
"version": "3.5.
|
| 2614 |
-
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.
|
| 2615 |
-
"integrity": "sha512-
|
| 2616 |
"dependencies": {
|
| 2617 |
-
"@vue/compiler-dom": "3.5.
|
| 2618 |
-
"@vue/compiler-sfc": "3.5.
|
| 2619 |
-
"@vue/runtime-dom": "3.5.
|
| 2620 |
-
"@vue/server-renderer": "3.5.
|
| 2621 |
-
"@vue/shared": "3.5.
|
| 2622 |
},
|
| 2623 |
"peerDependencies": {
|
| 2624 |
"typescript": "*"
|
|
@@ -2655,9 +2986,9 @@
|
|
| 2655 |
}
|
| 2656 |
},
|
| 2657 |
"node_modules/vue-router": {
|
| 2658 |
-
"version": "4.6.
|
| 2659 |
-
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.
|
| 2660 |
-
"integrity": "sha512-
|
| 2661 |
"dependencies": {
|
| 2662 |
"@vue/devtools-api": "^6.6.4"
|
| 2663 |
},
|
|
@@ -2685,9 +3016,9 @@
|
|
| 2685 |
}
|
| 2686 |
},
|
| 2687 |
"node_modules/ws": {
|
| 2688 |
-
"version": "8.
|
| 2689 |
-
"resolved": "https://registry.npmjs.org/ws/-/ws-8.
|
| 2690 |
-
"integrity": "sha512-
|
| 2691 |
"engines": {
|
| 2692 |
"node": ">=10.0.0"
|
| 2693 |
},
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"d3": "^7.9.0",
|
| 12 |
"d3-scale-chromatic": "^3.1.0",
|
|
|
|
| 13 |
"pinia": "^2.1.6",
|
| 14 |
"socket.io-client": "^4.5.2",
|
| 15 |
"three": "^0.156.1",
|
|
|
|
| 20 |
"@types/d3": "^7.4.3",
|
| 21 |
"@types/three": "^0.156.0",
|
| 22 |
"@vitejs/plugin-vue": "^5.2.4",
|
| 23 |
+
"sass-embedded": "^1.93.2",
|
| 24 |
"typescript": "^5.2.2",
|
| 25 |
"vite": "^5.4.21",
|
| 26 |
"vue-tsc": "^2.2.12"
|
|
|
|
| 35 |
}
|
| 36 |
},
|
| 37 |
"node_modules/@babel/helper-validator-identifier": {
|
| 38 |
+
"version": "7.27.1",
|
| 39 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
| 40 |
+
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
| 41 |
"engines": {
|
| 42 |
"node": ">=6.9.0"
|
| 43 |
}
|
| 44 |
},
|
| 45 |
"node_modules/@babel/parser": {
|
| 46 |
+
"version": "7.28.4",
|
| 47 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
| 48 |
+
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
| 49 |
"dependencies": {
|
| 50 |
+
"@babel/types": "^7.28.4"
|
| 51 |
},
|
| 52 |
"bin": {
|
| 53 |
"parser": "bin/babel-parser.js"
|
|
|
|
| 57 |
}
|
| 58 |
},
|
| 59 |
"node_modules/@babel/types": {
|
| 60 |
+
"version": "7.28.4",
|
| 61 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
|
| 62 |
+
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
| 63 |
"dependencies": {
|
| 64 |
"@babel/helper-string-parser": "^7.27.1",
|
| 65 |
+
"@babel/helper-validator-identifier": "^7.27.1"
|
| 66 |
},
|
| 67 |
"engines": {
|
| 68 |
"node": ">=6.9.0"
|
| 69 |
}
|
| 70 |
},
|
| 71 |
+
"node_modules/@bufbuild/protobuf": {
|
| 72 |
+
"version": "2.10.0",
|
| 73 |
+
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz",
|
| 74 |
+
"integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==",
|
| 75 |
+
"dev": true
|
| 76 |
+
},
|
| 77 |
"node_modules/@esbuild/aix-ppc64": {
|
| 78 |
"version": "0.21.5",
|
| 79 |
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
|
|
|
| 448 |
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
| 449 |
},
|
| 450 |
"node_modules/@parcel/watcher": {
|
| 451 |
+
"version": "2.5.1",
|
| 452 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
| 453 |
+
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
| 454 |
"dev": true,
|
| 455 |
"hasInstallScript": true,
|
| 456 |
"optional": true,
|
| 457 |
"dependencies": {
|
| 458 |
+
"detect-libc": "^1.0.3",
|
| 459 |
"is-glob": "^4.0.3",
|
| 460 |
+
"micromatch": "^4.0.5",
|
| 461 |
+
"node-addon-api": "^7.0.0"
|
| 462 |
},
|
| 463 |
"engines": {
|
| 464 |
"node": ">= 10.0.0"
|
|
|
|
| 468 |
"url": "https://opencollective.com/parcel"
|
| 469 |
},
|
| 470 |
"optionalDependencies": {
|
| 471 |
+
"@parcel/watcher-android-arm64": "2.5.1",
|
| 472 |
+
"@parcel/watcher-darwin-arm64": "2.5.1",
|
| 473 |
+
"@parcel/watcher-darwin-x64": "2.5.1",
|
| 474 |
+
"@parcel/watcher-freebsd-x64": "2.5.1",
|
| 475 |
+
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
| 476 |
+
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
| 477 |
+
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
| 478 |
+
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
| 479 |
+
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
| 480 |
+
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
| 481 |
+
"@parcel/watcher-win32-arm64": "2.5.1",
|
| 482 |
+
"@parcel/watcher-win32-ia32": "2.5.1",
|
| 483 |
+
"@parcel/watcher-win32-x64": "2.5.1"
|
| 484 |
}
|
| 485 |
},
|
| 486 |
"node_modules/@parcel/watcher-android-arm64": {
|
| 487 |
+
"version": "2.5.1",
|
| 488 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
| 489 |
+
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
| 490 |
"cpu": [
|
| 491 |
"arm64"
|
| 492 |
],
|
|
|
|
| 504 |
}
|
| 505 |
},
|
| 506 |
"node_modules/@parcel/watcher-darwin-arm64": {
|
| 507 |
+
"version": "2.5.1",
|
| 508 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
| 509 |
+
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
| 510 |
"cpu": [
|
| 511 |
"arm64"
|
| 512 |
],
|
|
|
|
| 524 |
}
|
| 525 |
},
|
| 526 |
"node_modules/@parcel/watcher-darwin-x64": {
|
| 527 |
+
"version": "2.5.1",
|
| 528 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
| 529 |
+
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
| 530 |
"cpu": [
|
| 531 |
"x64"
|
| 532 |
],
|
|
|
|
| 544 |
}
|
| 545 |
},
|
| 546 |
"node_modules/@parcel/watcher-freebsd-x64": {
|
| 547 |
+
"version": "2.5.1",
|
| 548 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
| 549 |
+
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
| 550 |
"cpu": [
|
| 551 |
"x64"
|
| 552 |
],
|
|
|
|
| 564 |
}
|
| 565 |
},
|
| 566 |
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
| 567 |
+
"version": "2.5.1",
|
| 568 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
| 569 |
+
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
| 570 |
"cpu": [
|
| 571 |
"arm"
|
| 572 |
],
|
|
|
|
| 584 |
}
|
| 585 |
},
|
| 586 |
"node_modules/@parcel/watcher-linux-arm-musl": {
|
| 587 |
+
"version": "2.5.1",
|
| 588 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
| 589 |
+
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
| 590 |
"cpu": [
|
| 591 |
"arm"
|
| 592 |
],
|
|
|
|
| 604 |
}
|
| 605 |
},
|
| 606 |
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
| 607 |
+
"version": "2.5.1",
|
| 608 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
| 609 |
+
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
| 610 |
"cpu": [
|
| 611 |
"arm64"
|
| 612 |
],
|
|
|
|
| 624 |
}
|
| 625 |
},
|
| 626 |
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
| 627 |
+
"version": "2.5.1",
|
| 628 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
| 629 |
+
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
| 630 |
"cpu": [
|
| 631 |
"arm64"
|
| 632 |
],
|
|
|
|
| 644 |
}
|
| 645 |
},
|
| 646 |
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
| 647 |
+
"version": "2.5.1",
|
| 648 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
| 649 |
+
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
| 650 |
"cpu": [
|
| 651 |
"x64"
|
| 652 |
],
|
|
|
|
| 664 |
}
|
| 665 |
},
|
| 666 |
"node_modules/@parcel/watcher-linux-x64-musl": {
|
| 667 |
+
"version": "2.5.1",
|
| 668 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
| 669 |
+
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
| 670 |
"cpu": [
|
| 671 |
"x64"
|
| 672 |
],
|
|
|
|
| 684 |
}
|
| 685 |
},
|
| 686 |
"node_modules/@parcel/watcher-win32-arm64": {
|
| 687 |
+
"version": "2.5.1",
|
| 688 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
| 689 |
+
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
| 690 |
"cpu": [
|
| 691 |
"arm64"
|
| 692 |
],
|
|
|
|
| 704 |
}
|
| 705 |
},
|
| 706 |
"node_modules/@parcel/watcher-win32-ia32": {
|
| 707 |
+
"version": "2.5.1",
|
| 708 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
| 709 |
+
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
| 710 |
"cpu": [
|
| 711 |
"ia32"
|
| 712 |
],
|
|
|
|
| 724 |
}
|
| 725 |
},
|
| 726 |
"node_modules/@parcel/watcher-win32-x64": {
|
| 727 |
+
"version": "2.5.1",
|
| 728 |
+
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
| 729 |
+
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
| 730 |
"cpu": [
|
| 731 |
"x64"
|
| 732 |
],
|
|
|
|
| 743 |
"url": "https://opencollective.com/parcel"
|
| 744 |
}
|
| 745 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 747 |
+
"version": "4.52.5",
|
| 748 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
| 749 |
+
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
|
| 750 |
"cpu": [
|
| 751 |
"arm"
|
| 752 |
],
|
|
|
|
| 757 |
]
|
| 758 |
},
|
| 759 |
"node_modules/@rollup/rollup-android-arm64": {
|
| 760 |
+
"version": "4.52.5",
|
| 761 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
|
| 762 |
+
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
|
| 763 |
"cpu": [
|
| 764 |
"arm64"
|
| 765 |
],
|
|
|
|
| 770 |
]
|
| 771 |
},
|
| 772 |
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 773 |
+
"version": "4.52.5",
|
| 774 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
|
| 775 |
+
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
|
| 776 |
"cpu": [
|
| 777 |
"arm64"
|
| 778 |
],
|
|
|
|
| 783 |
]
|
| 784 |
},
|
| 785 |
"node_modules/@rollup/rollup-darwin-x64": {
|
| 786 |
+
"version": "4.52.5",
|
| 787 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
|
| 788 |
+
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
|
| 789 |
"cpu": [
|
| 790 |
"x64"
|
| 791 |
],
|
|
|
|
| 796 |
]
|
| 797 |
},
|
| 798 |
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 799 |
+
"version": "4.52.5",
|
| 800 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
|
| 801 |
+
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
|
| 802 |
"cpu": [
|
| 803 |
"arm64"
|
| 804 |
],
|
|
|
|
| 809 |
]
|
| 810 |
},
|
| 811 |
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 812 |
+
"version": "4.52.5",
|
| 813 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
|
| 814 |
+
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
|
| 815 |
"cpu": [
|
| 816 |
"x64"
|
| 817 |
],
|
|
|
|
| 822 |
]
|
| 823 |
},
|
| 824 |
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 825 |
+
"version": "4.52.5",
|
| 826 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
|
| 827 |
+
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
|
| 828 |
"cpu": [
|
| 829 |
"arm"
|
| 830 |
],
|
|
|
|
| 835 |
]
|
| 836 |
},
|
| 837 |
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 838 |
+
"version": "4.52.5",
|
| 839 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
|
| 840 |
+
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
|
| 841 |
"cpu": [
|
| 842 |
"arm"
|
| 843 |
],
|
|
|
|
| 848 |
]
|
| 849 |
},
|
| 850 |
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 851 |
+
"version": "4.52.5",
|
| 852 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
|
| 853 |
+
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
|
| 854 |
"cpu": [
|
| 855 |
"arm64"
|
| 856 |
],
|
|
|
|
| 861 |
]
|
| 862 |
},
|
| 863 |
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 864 |
+
"version": "4.52.5",
|
| 865 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
|
| 866 |
+
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
|
| 867 |
"cpu": [
|
| 868 |
"arm64"
|
| 869 |
],
|
|
|
|
| 874 |
]
|
| 875 |
},
|
| 876 |
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 877 |
+
"version": "4.52.5",
|
| 878 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
|
| 879 |
+
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 880 |
"cpu": [
|
| 881 |
"loong64"
|
| 882 |
],
|
|
|
|
| 887 |
]
|
| 888 |
},
|
| 889 |
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 890 |
+
"version": "4.52.5",
|
| 891 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
|
| 892 |
+
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 893 |
"cpu": [
|
| 894 |
"ppc64"
|
| 895 |
],
|
|
|
|
| 900 |
]
|
| 901 |
},
|
| 902 |
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 903 |
+
"version": "4.52.5",
|
| 904 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
|
| 905 |
+
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
|
| 906 |
"cpu": [
|
| 907 |
"riscv64"
|
| 908 |
],
|
|
|
|
| 913 |
]
|
| 914 |
},
|
| 915 |
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 916 |
+
"version": "4.52.5",
|
| 917 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
|
| 918 |
+
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
|
| 919 |
"cpu": [
|
| 920 |
"riscv64"
|
| 921 |
],
|
|
|
|
| 926 |
]
|
| 927 |
},
|
| 928 |
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 929 |
+
"version": "4.52.5",
|
| 930 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
|
| 931 |
+
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
|
| 932 |
"cpu": [
|
| 933 |
"s390x"
|
| 934 |
],
|
|
|
|
| 939 |
]
|
| 940 |
},
|
| 941 |
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 942 |
+
"version": "4.52.5",
|
| 943 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
|
| 944 |
+
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
|
| 945 |
"cpu": [
|
| 946 |
"x64"
|
| 947 |
],
|
|
|
|
| 952 |
]
|
| 953 |
},
|
| 954 |
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 955 |
+
"version": "4.52.5",
|
| 956 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
|
| 957 |
+
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
|
| 958 |
"cpu": [
|
| 959 |
"x64"
|
| 960 |
],
|
|
|
|
| 964 |
"linux"
|
| 965 |
]
|
| 966 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 967 |
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 968 |
+
"version": "4.52.5",
|
| 969 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
|
| 970 |
+
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
|
| 971 |
"cpu": [
|
| 972 |
"arm64"
|
| 973 |
],
|
|
|
|
| 978 |
]
|
| 979 |
},
|
| 980 |
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 981 |
+
"version": "4.52.5",
|
| 982 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
|
| 983 |
+
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
|
| 984 |
"cpu": [
|
| 985 |
"arm64"
|
| 986 |
],
|
|
|
|
| 991 |
]
|
| 992 |
},
|
| 993 |
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 994 |
+
"version": "4.52.5",
|
| 995 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
|
| 996 |
+
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
|
| 997 |
"cpu": [
|
| 998 |
"ia32"
|
| 999 |
],
|
|
|
|
| 1004 |
]
|
| 1005 |
},
|
| 1006 |
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1007 |
+
"version": "4.52.5",
|
| 1008 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
|
| 1009 |
+
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
|
| 1010 |
"cpu": [
|
| 1011 |
"x64"
|
| 1012 |
],
|
|
|
|
| 1017 |
]
|
| 1018 |
},
|
| 1019 |
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1020 |
+
"version": "4.52.5",
|
| 1021 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
|
| 1022 |
+
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
|
| 1023 |
"cpu": [
|
| 1024 |
"x64"
|
| 1025 |
],
|
|
|
|
| 1242 |
"dev": true
|
| 1243 |
},
|
| 1244 |
"node_modules/@types/d3-shape": {
|
| 1245 |
+
"version": "3.1.7",
|
| 1246 |
+
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
| 1247 |
+
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
| 1248 |
"dev": true,
|
| 1249 |
"dependencies": {
|
| 1250 |
"@types/d3-path": "*"
|
|
|
|
| 1300 |
"dev": true
|
| 1301 |
},
|
| 1302 |
"node_modules/@types/node": {
|
| 1303 |
+
"version": "24.10.1",
|
| 1304 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
| 1305 |
+
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
| 1306 |
+
"dev": true,
|
| 1307 |
+
"optional": true,
|
| 1308 |
+
"peer": true,
|
| 1309 |
"dependencies": {
|
| 1310 |
"undici-types": "~7.16.0"
|
| 1311 |
}
|
|
|
|
| 1374 |
}
|
| 1375 |
},
|
| 1376 |
"node_modules/@vue/compiler-core": {
|
| 1377 |
+
"version": "3.5.22",
|
| 1378 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
|
| 1379 |
+
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
|
| 1380 |
"dependencies": {
|
| 1381 |
+
"@babel/parser": "^7.28.4",
|
| 1382 |
+
"@vue/shared": "3.5.22",
|
| 1383 |
+
"entities": "^4.5.0",
|
| 1384 |
"estree-walker": "^2.0.2",
|
| 1385 |
"source-map-js": "^1.2.1"
|
| 1386 |
}
|
| 1387 |
},
|
| 1388 |
"node_modules/@vue/compiler-dom": {
|
| 1389 |
+
"version": "3.5.22",
|
| 1390 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
|
| 1391 |
+
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
|
| 1392 |
"dependencies": {
|
| 1393 |
+
"@vue/compiler-core": "3.5.22",
|
| 1394 |
+
"@vue/shared": "3.5.22"
|
| 1395 |
}
|
| 1396 |
},
|
| 1397 |
"node_modules/@vue/compiler-sfc": {
|
| 1398 |
+
"version": "3.5.22",
|
| 1399 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
| 1400 |
+
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
| 1401 |
+
"dependencies": {
|
| 1402 |
+
"@babel/parser": "^7.28.4",
|
| 1403 |
+
"@vue/compiler-core": "3.5.22",
|
| 1404 |
+
"@vue/compiler-dom": "3.5.22",
|
| 1405 |
+
"@vue/compiler-ssr": "3.5.22",
|
| 1406 |
+
"@vue/shared": "3.5.22",
|
| 1407 |
"estree-walker": "^2.0.2",
|
| 1408 |
+
"magic-string": "^0.30.19",
|
| 1409 |
"postcss": "^8.5.6",
|
| 1410 |
"source-map-js": "^1.2.1"
|
| 1411 |
}
|
| 1412 |
},
|
| 1413 |
"node_modules/@vue/compiler-ssr": {
|
| 1414 |
+
"version": "3.5.22",
|
| 1415 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
|
| 1416 |
+
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
|
| 1417 |
"dependencies": {
|
| 1418 |
+
"@vue/compiler-dom": "3.5.22",
|
| 1419 |
+
"@vue/shared": "3.5.22"
|
| 1420 |
}
|
| 1421 |
},
|
| 1422 |
"node_modules/@vue/compiler-vue2": {
|
|
|
|
| 1459 |
}
|
| 1460 |
},
|
| 1461 |
"node_modules/@vue/reactivity": {
|
| 1462 |
+
"version": "3.5.22",
|
| 1463 |
+
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
| 1464 |
+
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
| 1465 |
"dependencies": {
|
| 1466 |
+
"@vue/shared": "3.5.22"
|
| 1467 |
}
|
| 1468 |
},
|
| 1469 |
"node_modules/@vue/runtime-core": {
|
| 1470 |
+
"version": "3.5.22",
|
| 1471 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
| 1472 |
+
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
| 1473 |
"dependencies": {
|
| 1474 |
+
"@vue/reactivity": "3.5.22",
|
| 1475 |
+
"@vue/shared": "3.5.22"
|
| 1476 |
}
|
| 1477 |
},
|
| 1478 |
"node_modules/@vue/runtime-dom": {
|
| 1479 |
+
"version": "3.5.22",
|
| 1480 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
| 1481 |
+
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
| 1482 |
"dependencies": {
|
| 1483 |
+
"@vue/reactivity": "3.5.22",
|
| 1484 |
+
"@vue/runtime-core": "3.5.22",
|
| 1485 |
+
"@vue/shared": "3.5.22",
|
| 1486 |
+
"csstype": "^3.1.3"
|
| 1487 |
}
|
| 1488 |
},
|
| 1489 |
"node_modules/@vue/server-renderer": {
|
| 1490 |
+
"version": "3.5.22",
|
| 1491 |
+
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
| 1492 |
+
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
| 1493 |
"dependencies": {
|
| 1494 |
+
"@vue/compiler-ssr": "3.5.22",
|
| 1495 |
+
"@vue/shared": "3.5.22"
|
| 1496 |
},
|
| 1497 |
"peerDependencies": {
|
| 1498 |
+
"vue": "3.5.22"
|
| 1499 |
}
|
| 1500 |
},
|
| 1501 |
"node_modules/@vue/shared": {
|
| 1502 |
+
"version": "3.5.22",
|
| 1503 |
+
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
|
| 1504 |
+
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w=="
|
| 1505 |
},
|
| 1506 |
"node_modules/alien-signals": {
|
| 1507 |
"version": "1.0.13",
|
|
|
|
| 1524 |
"balanced-match": "^1.0.0"
|
| 1525 |
}
|
| 1526 |
},
|
| 1527 |
+
"node_modules/braces": {
|
| 1528 |
+
"version": "3.0.3",
|
| 1529 |
+
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
| 1530 |
+
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
| 1531 |
+
"dev": true,
|
| 1532 |
+
"optional": true,
|
| 1533 |
+
"dependencies": {
|
| 1534 |
+
"fill-range": "^7.1.1"
|
| 1535 |
+
},
|
| 1536 |
+
"engines": {
|
| 1537 |
+
"node": ">=8"
|
| 1538 |
+
}
|
| 1539 |
+
},
|
| 1540 |
+
"node_modules/buffer-builder": {
|
| 1541 |
+
"version": "0.2.0",
|
| 1542 |
+
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
|
| 1543 |
+
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
|
| 1544 |
+
"dev": true
|
| 1545 |
+
},
|
| 1546 |
"node_modules/chokidar": {
|
| 1547 |
"version": "4.0.3",
|
| 1548 |
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
| 1549 |
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
| 1550 |
"dev": true,
|
| 1551 |
+
"optional": true,
|
| 1552 |
"dependencies": {
|
| 1553 |
"readdirp": "^4.0.1"
|
| 1554 |
},
|
|
|
|
| 1559 |
"url": "https://paulmillr.com/funding/"
|
| 1560 |
}
|
| 1561 |
},
|
| 1562 |
+
"node_modules/colorjs.io": {
|
| 1563 |
+
"version": "0.5.2",
|
| 1564 |
+
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
|
| 1565 |
+
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
| 1566 |
+
"dev": true
|
| 1567 |
+
},
|
| 1568 |
"node_modules/commander": {
|
| 1569 |
"version": "7.2.0",
|
| 1570 |
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
|
|
|
| 1574 |
}
|
| 1575 |
},
|
| 1576 |
"node_modules/csstype": {
|
| 1577 |
+
"version": "3.1.3",
|
| 1578 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 1579 |
+
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
| 1580 |
},
|
| 1581 |
"node_modules/d3": {
|
| 1582 |
"version": "7.9.0",
|
|
|
|
| 1955 |
"dev": true
|
| 1956 |
},
|
| 1957 |
"node_modules/debug": {
|
| 1958 |
+
"version": "4.3.7",
|
| 1959 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
| 1960 |
+
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
| 1961 |
"dependencies": {
|
| 1962 |
"ms": "^2.1.3"
|
| 1963 |
},
|
|
|
|
| 1979 |
}
|
| 1980 |
},
|
| 1981 |
"node_modules/detect-libc": {
|
| 1982 |
+
"version": "1.0.3",
|
| 1983 |
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
| 1984 |
+
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
| 1985 |
"dev": true,
|
| 1986 |
"optional": true,
|
| 1987 |
+
"bin": {
|
| 1988 |
+
"detect-libc": "bin/detect-libc.js"
|
| 1989 |
+
},
|
| 1990 |
"engines": {
|
| 1991 |
+
"node": ">=0.10"
|
| 1992 |
}
|
| 1993 |
},
|
| 1994 |
"node_modules/engine.io-client": {
|
| 1995 |
+
"version": "6.6.3",
|
| 1996 |
+
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
| 1997 |
+
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
| 1998 |
"dependencies": {
|
| 1999 |
"@socket.io/component-emitter": "~3.1.0",
|
| 2000 |
+
"debug": "~4.3.1",
|
| 2001 |
"engine.io-parser": "~5.2.1",
|
| 2002 |
+
"ws": "~8.17.1",
|
| 2003 |
"xmlhttprequest-ssl": "~2.1.1"
|
| 2004 |
}
|
| 2005 |
},
|
|
|
|
| 2012 |
}
|
| 2013 |
},
|
| 2014 |
"node_modules/entities": {
|
| 2015 |
+
"version": "4.5.0",
|
| 2016 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
| 2017 |
+
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
| 2018 |
"engines": {
|
| 2019 |
"node": ">=0.12"
|
| 2020 |
},
|
|
|
|
| 2071 |
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
| 2072 |
"dev": true
|
| 2073 |
},
|
| 2074 |
+
"node_modules/fill-range": {
|
| 2075 |
+
"version": "7.1.1",
|
| 2076 |
+
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
| 2077 |
+
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
| 2078 |
+
"dev": true,
|
| 2079 |
+
"optional": true,
|
| 2080 |
+
"dependencies": {
|
| 2081 |
+
"to-regex-range": "^5.0.1"
|
| 2082 |
+
},
|
| 2083 |
+
"engines": {
|
| 2084 |
+
"node": ">=8"
|
| 2085 |
+
}
|
| 2086 |
},
|
| 2087 |
"node_modules/fsevents": {
|
| 2088 |
"version": "2.3.3",
|
|
|
|
| 2098 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 2099 |
}
|
| 2100 |
},
|
| 2101 |
+
"node_modules/has-flag": {
|
| 2102 |
+
"version": "4.0.0",
|
| 2103 |
+
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
| 2104 |
+
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
| 2105 |
+
"dev": true,
|
| 2106 |
+
"engines": {
|
| 2107 |
+
"node": ">=8"
|
| 2108 |
+
}
|
| 2109 |
},
|
| 2110 |
"node_modules/he": {
|
| 2111 |
"version": "1.2.0",
|
|
|
|
| 2164 |
"node": ">=0.10.0"
|
| 2165 |
}
|
| 2166 |
},
|
| 2167 |
+
"node_modules/is-number": {
|
| 2168 |
+
"version": "7.0.0",
|
| 2169 |
+
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
| 2170 |
+
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
| 2171 |
+
"dev": true,
|
| 2172 |
+
"optional": true,
|
| 2173 |
+
"engines": {
|
| 2174 |
+
"node": ">=0.12.0"
|
| 2175 |
+
}
|
| 2176 |
},
|
| 2177 |
"node_modules/magic-string": {
|
| 2178 |
+
"version": "0.30.19",
|
| 2179 |
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
| 2180 |
+
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
| 2181 |
"dependencies": {
|
| 2182 |
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 2183 |
}
|
|
|
|
| 2188 |
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
| 2189 |
"dev": true
|
| 2190 |
},
|
| 2191 |
+
"node_modules/micromatch": {
|
| 2192 |
+
"version": "4.0.8",
|
| 2193 |
+
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
| 2194 |
+
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
| 2195 |
+
"dev": true,
|
| 2196 |
+
"optional": true,
|
| 2197 |
+
"dependencies": {
|
| 2198 |
+
"braces": "^3.0.3",
|
| 2199 |
+
"picomatch": "^2.3.1"
|
| 2200 |
+
},
|
| 2201 |
+
"engines": {
|
| 2202 |
+
"node": ">=8.6"
|
| 2203 |
+
}
|
| 2204 |
+
},
|
| 2205 |
"node_modules/minimatch": {
|
| 2206 |
"version": "9.0.5",
|
| 2207 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
|
|
|
| 2252 |
"dev": true,
|
| 2253 |
"optional": true
|
| 2254 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2255 |
"node_modules/path-browserify": {
|
| 2256 |
"version": "1.0.1",
|
| 2257 |
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
|
|
|
| 2264 |
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
| 2265 |
},
|
| 2266 |
"node_modules/picomatch": {
|
| 2267 |
+
"version": "2.3.1",
|
| 2268 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
| 2269 |
+
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
| 2270 |
"dev": true,
|
| 2271 |
"optional": true,
|
| 2272 |
"engines": {
|
| 2273 |
+
"node": ">=8.6"
|
| 2274 |
},
|
| 2275 |
"funding": {
|
| 2276 |
"url": "https://github.com/sponsors/jonschlinkert"
|
|
|
|
| 2297 |
}
|
| 2298 |
}
|
| 2299 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2300 |
"node_modules/postcss": {
|
| 2301 |
"version": "8.5.6",
|
| 2302 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
|
|
|
| 2324 |
"node": "^10 || ^12 || >=14"
|
| 2325 |
}
|
| 2326 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2327 |
"node_modules/readdirp": {
|
| 2328 |
"version": "4.1.2",
|
| 2329 |
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
| 2330 |
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
| 2331 |
"dev": true,
|
| 2332 |
+
"optional": true,
|
| 2333 |
"engines": {
|
| 2334 |
"node": ">= 14.18.0"
|
| 2335 |
},
|
|
|
|
| 2344 |
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
| 2345 |
},
|
| 2346 |
"node_modules/rollup": {
|
| 2347 |
+
"version": "4.52.5",
|
| 2348 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
| 2349 |
+
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
| 2350 |
"dev": true,
|
| 2351 |
"dependencies": {
|
| 2352 |
"@types/estree": "1.0.8"
|
|
|
|
| 2359 |
"npm": ">=8.0.0"
|
| 2360 |
},
|
| 2361 |
"optionalDependencies": {
|
| 2362 |
+
"@rollup/rollup-android-arm-eabi": "4.52.5",
|
| 2363 |
+
"@rollup/rollup-android-arm64": "4.52.5",
|
| 2364 |
+
"@rollup/rollup-darwin-arm64": "4.52.5",
|
| 2365 |
+
"@rollup/rollup-darwin-x64": "4.52.5",
|
| 2366 |
+
"@rollup/rollup-freebsd-arm64": "4.52.5",
|
| 2367 |
+
"@rollup/rollup-freebsd-x64": "4.52.5",
|
| 2368 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
|
| 2369 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
|
| 2370 |
+
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
|
| 2371 |
+
"@rollup/rollup-linux-arm64-musl": "4.52.5",
|
| 2372 |
+
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
|
| 2373 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
|
| 2374 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
|
| 2375 |
+
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
|
| 2376 |
+
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
|
| 2377 |
+
"@rollup/rollup-linux-x64-gnu": "4.52.5",
|
| 2378 |
+
"@rollup/rollup-linux-x64-musl": "4.52.5",
|
| 2379 |
+
"@rollup/rollup-openharmony-arm64": "4.52.5",
|
| 2380 |
+
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
|
| 2381 |
+
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
|
| 2382 |
+
"@rollup/rollup-win32-x64-gnu": "4.52.5",
|
| 2383 |
+
"@rollup/rollup-win32-x64-msvc": "4.52.5",
|
|
|
|
|
|
|
|
|
|
| 2384 |
"fsevents": "~2.3.2"
|
| 2385 |
}
|
| 2386 |
},
|
|
|
|
| 2389 |
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
| 2390 |
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
|
| 2391 |
},
|
| 2392 |
+
"node_modules/rxjs": {
|
| 2393 |
+
"version": "7.8.2",
|
| 2394 |
+
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
| 2395 |
+
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
| 2396 |
+
"dev": true,
|
| 2397 |
+
"dependencies": {
|
| 2398 |
+
"tslib": "^2.1.0"
|
| 2399 |
+
}
|
| 2400 |
+
},
|
| 2401 |
"node_modules/safer-buffer": {
|
| 2402 |
"version": "2.1.2",
|
| 2403 |
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 2404 |
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
| 2405 |
},
|
| 2406 |
"node_modules/sass": {
|
| 2407 |
+
"version": "1.93.2",
|
| 2408 |
+
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
| 2409 |
+
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
| 2410 |
"dev": true,
|
| 2411 |
+
"optional": true,
|
| 2412 |
"dependencies": {
|
| 2413 |
"chokidar": "^4.0.0",
|
| 2414 |
"immutable": "^5.0.2",
|
|
|
|
| 2424 |
"@parcel/watcher": "^2.4.1"
|
| 2425 |
}
|
| 2426 |
},
|
| 2427 |
+
"node_modules/sass-embedded": {
|
| 2428 |
+
"version": "1.93.2",
|
| 2429 |
+
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.2.tgz",
|
| 2430 |
+
"integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==",
|
| 2431 |
+
"dev": true,
|
| 2432 |
+
"dependencies": {
|
| 2433 |
+
"@bufbuild/protobuf": "^2.5.0",
|
| 2434 |
+
"buffer-builder": "^0.2.0",
|
| 2435 |
+
"colorjs.io": "^0.5.0",
|
| 2436 |
+
"immutable": "^5.0.2",
|
| 2437 |
+
"rxjs": "^7.4.0",
|
| 2438 |
+
"supports-color": "^8.1.1",
|
| 2439 |
+
"sync-child-process": "^1.0.2",
|
| 2440 |
+
"varint": "^6.0.0"
|
| 2441 |
+
},
|
| 2442 |
+
"bin": {
|
| 2443 |
+
"sass": "dist/bin/sass.js"
|
| 2444 |
+
},
|
| 2445 |
+
"engines": {
|
| 2446 |
+
"node": ">=16.0.0"
|
| 2447 |
+
},
|
| 2448 |
+
"optionalDependencies": {
|
| 2449 |
+
"sass-embedded-all-unknown": "1.93.2",
|
| 2450 |
+
"sass-embedded-android-arm": "1.93.2",
|
| 2451 |
+
"sass-embedded-android-arm64": "1.93.2",
|
| 2452 |
+
"sass-embedded-android-riscv64": "1.93.2",
|
| 2453 |
+
"sass-embedded-android-x64": "1.93.2",
|
| 2454 |
+
"sass-embedded-darwin-arm64": "1.93.2",
|
| 2455 |
+
"sass-embedded-darwin-x64": "1.93.2",
|
| 2456 |
+
"sass-embedded-linux-arm": "1.93.2",
|
| 2457 |
+
"sass-embedded-linux-arm64": "1.93.2",
|
| 2458 |
+
"sass-embedded-linux-musl-arm": "1.93.2",
|
| 2459 |
+
"sass-embedded-linux-musl-arm64": "1.93.2",
|
| 2460 |
+
"sass-embedded-linux-musl-riscv64": "1.93.2",
|
| 2461 |
+
"sass-embedded-linux-musl-x64": "1.93.2",
|
| 2462 |
+
"sass-embedded-linux-riscv64": "1.93.2",
|
| 2463 |
+
"sass-embedded-linux-x64": "1.93.2",
|
| 2464 |
+
"sass-embedded-unknown-all": "1.93.2",
|
| 2465 |
+
"sass-embedded-win32-arm64": "1.93.2",
|
| 2466 |
+
"sass-embedded-win32-x64": "1.93.2"
|
| 2467 |
+
}
|
| 2468 |
+
},
|
| 2469 |
+
"node_modules/sass-embedded-all-unknown": {
|
| 2470 |
+
"version": "1.93.2",
|
| 2471 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.93.2.tgz",
|
| 2472 |
+
"integrity": "sha512-GdEuPXIzmhRS5J7UKAwEvtk8YyHQuFZRcpnEnkA3rwRUI27kwjyXkNeIj38XjUQ3DzrfMe8HcKFaqWGHvblS7Q==",
|
| 2473 |
+
"cpu": [
|
| 2474 |
+
"!arm",
|
| 2475 |
+
"!arm64",
|
| 2476 |
+
"!riscv64",
|
| 2477 |
+
"!x64"
|
| 2478 |
+
],
|
| 2479 |
+
"dev": true,
|
| 2480 |
+
"optional": true,
|
| 2481 |
+
"dependencies": {
|
| 2482 |
+
"sass": "1.93.2"
|
| 2483 |
+
}
|
| 2484 |
+
},
|
| 2485 |
+
"node_modules/sass-embedded-android-arm": {
|
| 2486 |
+
"version": "1.93.2",
|
| 2487 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.93.2.tgz",
|
| 2488 |
+
"integrity": "sha512-I8bpO8meZNo5FvFx5FIiE7DGPVOYft0WjuwcCCdeJ6duwfkl6tZdatex1GrSigvTsuz9L0m4ngDcX/Tj/8yMow==",
|
| 2489 |
+
"cpu": [
|
| 2490 |
+
"arm"
|
| 2491 |
+
],
|
| 2492 |
+
"dev": true,
|
| 2493 |
+
"optional": true,
|
| 2494 |
+
"os": [
|
| 2495 |
+
"android"
|
| 2496 |
+
],
|
| 2497 |
+
"engines": {
|
| 2498 |
+
"node": ">=14.0.0"
|
| 2499 |
+
}
|
| 2500 |
+
},
|
| 2501 |
+
"node_modules/sass-embedded-android-arm64": {
|
| 2502 |
+
"version": "1.93.2",
|
| 2503 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.93.2.tgz",
|
| 2504 |
+
"integrity": "sha512-346f4iVGAPGcNP6V6IOOFkN5qnArAoXNTPr5eA/rmNpeGwomdb7kJyQ717r9rbJXxOG8OAAUado6J0qLsjnjXQ==",
|
| 2505 |
+
"cpu": [
|
| 2506 |
+
"arm64"
|
| 2507 |
+
],
|
| 2508 |
+
"dev": true,
|
| 2509 |
+
"optional": true,
|
| 2510 |
+
"os": [
|
| 2511 |
+
"android"
|
| 2512 |
+
],
|
| 2513 |
+
"engines": {
|
| 2514 |
+
"node": ">=14.0.0"
|
| 2515 |
+
}
|
| 2516 |
+
},
|
| 2517 |
+
"node_modules/sass-embedded-android-riscv64": {
|
| 2518 |
+
"version": "1.93.2",
|
| 2519 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.93.2.tgz",
|
| 2520 |
+
"integrity": "sha512-hSMW1s4yJf5guT9mrdkumluqrwh7BjbZ4MbBW9tmi1DRDdlw1Wh9Oy1HnnmOG8x9XcI1qkojtPL6LUuEJmsiDg==",
|
| 2521 |
+
"cpu": [
|
| 2522 |
+
"riscv64"
|
| 2523 |
+
],
|
| 2524 |
+
"dev": true,
|
| 2525 |
+
"optional": true,
|
| 2526 |
+
"os": [
|
| 2527 |
+
"android"
|
| 2528 |
+
],
|
| 2529 |
+
"engines": {
|
| 2530 |
+
"node": ">=14.0.0"
|
| 2531 |
+
}
|
| 2532 |
+
},
|
| 2533 |
+
"node_modules/sass-embedded-android-x64": {
|
| 2534 |
+
"version": "1.93.2",
|
| 2535 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.93.2.tgz",
|
| 2536 |
+
"integrity": "sha512-JqktiHZduvn+ldGBosE40ALgQ//tGCVNAObgcQ6UIZznEJbsHegqStqhRo8UW3x2cgOO2XYJcrInH6cc7wdKbw==",
|
| 2537 |
+
"cpu": [
|
| 2538 |
+
"x64"
|
| 2539 |
+
],
|
| 2540 |
+
"dev": true,
|
| 2541 |
+
"optional": true,
|
| 2542 |
+
"os": [
|
| 2543 |
+
"android"
|
| 2544 |
+
],
|
| 2545 |
+
"engines": {
|
| 2546 |
+
"node": ">=14.0.0"
|
| 2547 |
+
}
|
| 2548 |
+
},
|
| 2549 |
+
"node_modules/sass-embedded-darwin-arm64": {
|
| 2550 |
+
"version": "1.93.2",
|
| 2551 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.2.tgz",
|
| 2552 |
+
"integrity": "sha512-qI1X16qKNeBJp+M/5BNW7v/JHCDYWr1/mdoJ7+UMHmP0b5AVudIZtimtK0hnjrLnBECURifd6IkulybR+h+4UA==",
|
| 2553 |
+
"cpu": [
|
| 2554 |
+
"arm64"
|
| 2555 |
+
],
|
| 2556 |
+
"dev": true,
|
| 2557 |
+
"optional": true,
|
| 2558 |
+
"os": [
|
| 2559 |
+
"darwin"
|
| 2560 |
+
],
|
| 2561 |
+
"engines": {
|
| 2562 |
+
"node": ">=14.0.0"
|
| 2563 |
+
}
|
| 2564 |
+
},
|
| 2565 |
+
"node_modules/sass-embedded-darwin-x64": {
|
| 2566 |
+
"version": "1.93.2",
|
| 2567 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.93.2.tgz",
|
| 2568 |
+
"integrity": "sha512-4KeAvlkQ0m0enKUnDGQJZwpovYw99iiMb8CTZRSsQm8Eh7halbJZVmx67f4heFY/zISgVOCcxNg19GrM5NTwtA==",
|
| 2569 |
+
"cpu": [
|
| 2570 |
+
"x64"
|
| 2571 |
+
],
|
| 2572 |
+
"dev": true,
|
| 2573 |
+
"optional": true,
|
| 2574 |
+
"os": [
|
| 2575 |
+
"darwin"
|
| 2576 |
+
],
|
| 2577 |
+
"engines": {
|
| 2578 |
+
"node": ">=14.0.0"
|
| 2579 |
+
}
|
| 2580 |
+
},
|
| 2581 |
+
"node_modules/sass-embedded-linux-arm": {
|
| 2582 |
+
"version": "1.93.2",
|
| 2583 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.93.2.tgz",
|
| 2584 |
+
"integrity": "sha512-N3+D/ToHtzwLDO+lSH05Wo6/KRxFBPnbjVHASOlHzqJnK+g5cqex7IFAp6ozzlRStySk61Rp6d+YGrqZ6/P0PA==",
|
| 2585 |
+
"cpu": [
|
| 2586 |
+
"arm"
|
| 2587 |
+
],
|
| 2588 |
+
"dev": true,
|
| 2589 |
+
"optional": true,
|
| 2590 |
+
"os": [
|
| 2591 |
+
"linux"
|
| 2592 |
+
],
|
| 2593 |
+
"engines": {
|
| 2594 |
+
"node": ">=14.0.0"
|
| 2595 |
+
}
|
| 2596 |
+
},
|
| 2597 |
+
"node_modules/sass-embedded-linux-arm64": {
|
| 2598 |
+
"version": "1.93.2",
|
| 2599 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.93.2.tgz",
|
| 2600 |
+
"integrity": "sha512-9ftX6nd5CsShJqJ2WRg+ptaYvUW+spqZfJ88FbcKQBNFQm6L87luj3UI1rB6cP5EWrLwHA754OKxRJyzWiaN6g==",
|
| 2601 |
+
"cpu": [
|
| 2602 |
+
"arm64"
|
| 2603 |
+
],
|
| 2604 |
+
"dev": true,
|
| 2605 |
+
"optional": true,
|
| 2606 |
+
"os": [
|
| 2607 |
+
"linux"
|
| 2608 |
+
],
|
| 2609 |
+
"engines": {
|
| 2610 |
+
"node": ">=14.0.0"
|
| 2611 |
+
}
|
| 2612 |
+
},
|
| 2613 |
+
"node_modules/sass-embedded-linux-musl-arm": {
|
| 2614 |
+
"version": "1.93.2",
|
| 2615 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.93.2.tgz",
|
| 2616 |
+
"integrity": "sha512-XBTvx66yRenvEsp3VaJCb3HQSyqCsUh7R+pbxcN5TuzueybZi0LXvn9zneksdXcmjACMlMpIVXi6LyHPQkYc8A==",
|
| 2617 |
+
"cpu": [
|
| 2618 |
+
"arm"
|
| 2619 |
+
],
|
| 2620 |
+
"dev": true,
|
| 2621 |
+
"optional": true,
|
| 2622 |
+
"os": [
|
| 2623 |
+
"linux"
|
| 2624 |
+
],
|
| 2625 |
+
"engines": {
|
| 2626 |
+
"node": ">=14.0.0"
|
| 2627 |
+
}
|
| 2628 |
+
},
|
| 2629 |
+
"node_modules/sass-embedded-linux-musl-arm64": {
|
| 2630 |
+
"version": "1.93.2",
|
| 2631 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.93.2.tgz",
|
| 2632 |
+
"integrity": "sha512-+3EHuDPkMiAX5kytsjEC1bKZCawB9J6pm2eBIzzLMPWbf5xdx++vO1DpT7hD4bm4ZGn0eVHgSOKIfP6CVz6tVg==",
|
| 2633 |
+
"cpu": [
|
| 2634 |
+
"arm64"
|
| 2635 |
+
],
|
| 2636 |
+
"dev": true,
|
| 2637 |
+
"optional": true,
|
| 2638 |
+
"os": [
|
| 2639 |
+
"linux"
|
| 2640 |
+
],
|
| 2641 |
+
"engines": {
|
| 2642 |
+
"node": ">=14.0.0"
|
| 2643 |
+
}
|
| 2644 |
+
},
|
| 2645 |
+
"node_modules/sass-embedded-linux-musl-riscv64": {
|
| 2646 |
+
"version": "1.93.2",
|
| 2647 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.93.2.tgz",
|
| 2648 |
+
"integrity": "sha512-0sB5kmVZDKTYzmCSlTUnjh6mzOhzmQiW/NNI5g8JS4JiHw2sDNTvt1dsFTuqFkUHyEOY3ESTsfHHBQV8Ip4bEA==",
|
| 2649 |
+
"cpu": [
|
| 2650 |
+
"riscv64"
|
| 2651 |
+
],
|
| 2652 |
+
"dev": true,
|
| 2653 |
+
"optional": true,
|
| 2654 |
+
"os": [
|
| 2655 |
+
"linux"
|
| 2656 |
+
],
|
| 2657 |
+
"engines": {
|
| 2658 |
+
"node": ">=14.0.0"
|
| 2659 |
+
}
|
| 2660 |
+
},
|
| 2661 |
+
"node_modules/sass-embedded-linux-musl-x64": {
|
| 2662 |
+
"version": "1.93.2",
|
| 2663 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.93.2.tgz",
|
| 2664 |
+
"integrity": "sha512-t3ejQ+1LEVuHy7JHBI2tWHhoMfhedUNDjGJR2FKaLgrtJntGnyD1RyX0xb3nuqL/UXiEAtmTmZY+Uh3SLUe1Hg==",
|
| 2665 |
+
"cpu": [
|
| 2666 |
+
"x64"
|
| 2667 |
+
],
|
| 2668 |
+
"dev": true,
|
| 2669 |
+
"optional": true,
|
| 2670 |
+
"os": [
|
| 2671 |
+
"linux"
|
| 2672 |
+
],
|
| 2673 |
+
"engines": {
|
| 2674 |
+
"node": ">=14.0.0"
|
| 2675 |
+
}
|
| 2676 |
+
},
|
| 2677 |
+
"node_modules/sass-embedded-linux-riscv64": {
|
| 2678 |
+
"version": "1.93.2",
|
| 2679 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.93.2.tgz",
|
| 2680 |
+
"integrity": "sha512-e7AndEwAbFtXaLy6on4BfNGTr3wtGZQmypUgYpSNVcYDO+CWxatKVY4cxbehMPhxG9g5ru+eaMfynvhZt7fLaA==",
|
| 2681 |
+
"cpu": [
|
| 2682 |
+
"riscv64"
|
| 2683 |
+
],
|
| 2684 |
+
"dev": true,
|
| 2685 |
+
"optional": true,
|
| 2686 |
+
"os": [
|
| 2687 |
+
"linux"
|
| 2688 |
+
],
|
| 2689 |
+
"engines": {
|
| 2690 |
+
"node": ">=14.0.0"
|
| 2691 |
+
}
|
| 2692 |
+
},
|
| 2693 |
+
"node_modules/sass-embedded-linux-x64": {
|
| 2694 |
+
"version": "1.93.2",
|
| 2695 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.93.2.tgz",
|
| 2696 |
+
"integrity": "sha512-U3EIUZQL11DU0xDDHXexd4PYPHQaSQa2hzc4EzmhHqrAj+TyfYO94htjWOd+DdTPtSwmLp+9cTWwPZBODzC96w==",
|
| 2697 |
+
"cpu": [
|
| 2698 |
+
"x64"
|
| 2699 |
+
],
|
| 2700 |
+
"dev": true,
|
| 2701 |
+
"optional": true,
|
| 2702 |
+
"os": [
|
| 2703 |
+
"linux"
|
| 2704 |
+
],
|
| 2705 |
+
"engines": {
|
| 2706 |
+
"node": ">=14.0.0"
|
| 2707 |
+
}
|
| 2708 |
+
},
|
| 2709 |
+
"node_modules/sass-embedded-unknown-all": {
|
| 2710 |
+
"version": "1.93.2",
|
| 2711 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.93.2.tgz",
|
| 2712 |
+
"integrity": "sha512-7VnaOmyewcXohiuoFagJ3SK5ddP9yXpU0rzz+pZQmS1/+5O6vzyFCUoEt3HDRaLctH4GT3nUGoK1jg0ae62IfQ==",
|
| 2713 |
+
"dev": true,
|
| 2714 |
+
"optional": true,
|
| 2715 |
+
"os": [
|
| 2716 |
+
"!android",
|
| 2717 |
+
"!darwin",
|
| 2718 |
+
"!linux",
|
| 2719 |
+
"!win32"
|
| 2720 |
+
],
|
| 2721 |
+
"dependencies": {
|
| 2722 |
+
"sass": "1.93.2"
|
| 2723 |
+
}
|
| 2724 |
+
},
|
| 2725 |
+
"node_modules/sass-embedded-win32-arm64": {
|
| 2726 |
+
"version": "1.93.2",
|
| 2727 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.93.2.tgz",
|
| 2728 |
+
"integrity": "sha512-Y90DZDbQvtv4Bt0GTXKlcT9pn4pz8AObEjFF8eyul+/boXwyptPZ/A1EyziAeNaIEIfxyy87z78PUgCeGHsx3Q==",
|
| 2729 |
+
"cpu": [
|
| 2730 |
+
"arm64"
|
| 2731 |
+
],
|
| 2732 |
+
"dev": true,
|
| 2733 |
+
"optional": true,
|
| 2734 |
+
"os": [
|
| 2735 |
+
"win32"
|
| 2736 |
+
],
|
| 2737 |
+
"engines": {
|
| 2738 |
+
"node": ">=14.0.0"
|
| 2739 |
+
}
|
| 2740 |
+
},
|
| 2741 |
+
"node_modules/sass-embedded-win32-x64": {
|
| 2742 |
+
"version": "1.93.2",
|
| 2743 |
+
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.2.tgz",
|
| 2744 |
+
"integrity": "sha512-BbSucRP6PVRZGIwlEBkp+6VQl2GWdkWFMN+9EuOTPrLxCJZoq+yhzmbjspd3PeM8+7WJ7AdFu/uRYdO8tor1iQ==",
|
| 2745 |
+
"cpu": [
|
| 2746 |
+
"x64"
|
| 2747 |
+
],
|
| 2748 |
+
"dev": true,
|
| 2749 |
+
"optional": true,
|
| 2750 |
+
"os": [
|
| 2751 |
+
"win32"
|
| 2752 |
+
],
|
| 2753 |
+
"engines": {
|
| 2754 |
+
"node": ">=14.0.0"
|
| 2755 |
+
}
|
| 2756 |
+
},
|
| 2757 |
"node_modules/socket.io-client": {
|
| 2758 |
+
"version": "4.8.1",
|
| 2759 |
+
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
| 2760 |
+
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
| 2761 |
"dependencies": {
|
| 2762 |
"@socket.io/component-emitter": "~3.1.0",
|
| 2763 |
+
"debug": "~4.3.2",
|
| 2764 |
"engine.io-client": "~6.6.1",
|
| 2765 |
"socket.io-parser": "~4.2.4"
|
| 2766 |
},
|
|
|
|
| 2769 |
}
|
| 2770 |
},
|
| 2771 |
"node_modules/socket.io-parser": {
|
| 2772 |
+
"version": "4.2.4",
|
| 2773 |
+
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
| 2774 |
+
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
| 2775 |
"dependencies": {
|
| 2776 |
"@socket.io/component-emitter": "~3.1.0",
|
| 2777 |
+
"debug": "~4.3.1"
|
| 2778 |
},
|
| 2779 |
"engines": {
|
| 2780 |
"node": ">=10.0.0"
|
|
|
|
| 2788 |
"node": ">=0.10.0"
|
| 2789 |
}
|
| 2790 |
},
|
| 2791 |
+
"node_modules/supports-color": {
|
| 2792 |
+
"version": "8.1.1",
|
| 2793 |
+
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
| 2794 |
+
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
| 2795 |
+
"dev": true,
|
| 2796 |
+
"dependencies": {
|
| 2797 |
+
"has-flag": "^4.0.0"
|
| 2798 |
+
},
|
| 2799 |
+
"engines": {
|
| 2800 |
+
"node": ">=10"
|
| 2801 |
+
},
|
| 2802 |
+
"funding": {
|
| 2803 |
+
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
| 2804 |
+
}
|
| 2805 |
+
},
|
| 2806 |
+
"node_modules/sync-child-process": {
|
| 2807 |
+
"version": "1.0.2",
|
| 2808 |
+
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
|
| 2809 |
+
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
|
| 2810 |
+
"dev": true,
|
| 2811 |
+
"dependencies": {
|
| 2812 |
+
"sync-message-port": "^1.0.0"
|
| 2813 |
+
},
|
| 2814 |
+
"engines": {
|
| 2815 |
+
"node": ">=16.0.0"
|
| 2816 |
+
}
|
| 2817 |
+
},
|
| 2818 |
+
"node_modules/sync-message-port": {
|
| 2819 |
+
"version": "1.1.3",
|
| 2820 |
+
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
|
| 2821 |
+
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
|
| 2822 |
+
"dev": true,
|
| 2823 |
+
"engines": {
|
| 2824 |
+
"node": ">=16.0.0"
|
| 2825 |
+
}
|
| 2826 |
+
},
|
| 2827 |
"node_modules/three": {
|
| 2828 |
"version": "0.156.1",
|
| 2829 |
"resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
|
| 2830 |
"integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ=="
|
| 2831 |
},
|
| 2832 |
+
"node_modules/to-regex-range": {
|
| 2833 |
+
"version": "5.0.1",
|
| 2834 |
+
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
| 2835 |
+
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
| 2836 |
+
"dev": true,
|
| 2837 |
+
"optional": true,
|
| 2838 |
+
"dependencies": {
|
| 2839 |
+
"is-number": "^7.0.0"
|
| 2840 |
+
},
|
| 2841 |
+
"engines": {
|
| 2842 |
+
"node": ">=8.0"
|
| 2843 |
+
}
|
| 2844 |
+
},
|
| 2845 |
+
"node_modules/tslib": {
|
| 2846 |
+
"version": "2.8.1",
|
| 2847 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 2848 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 2849 |
+
"dev": true
|
| 2850 |
+
},
|
| 2851 |
"node_modules/typescript": {
|
| 2852 |
"version": "5.9.3",
|
| 2853 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
|
|
| 2864 |
"node_modules/undici-types": {
|
| 2865 |
"version": "7.16.0",
|
| 2866 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
| 2867 |
+
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
| 2868 |
+
"dev": true,
|
| 2869 |
+
"optional": true,
|
| 2870 |
+
"peer": true
|
| 2871 |
+
},
|
| 2872 |
+
"node_modules/varint": {
|
| 2873 |
+
"version": "6.0.0",
|
| 2874 |
+
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
| 2875 |
+
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
| 2876 |
+
"dev": true
|
| 2877 |
},
|
| 2878 |
"node_modules/vite": {
|
| 2879 |
"version": "5.4.21",
|
|
|
|
| 2941 |
"dev": true
|
| 2942 |
},
|
| 2943 |
"node_modules/vue": {
|
| 2944 |
+
"version": "3.5.22",
|
| 2945 |
+
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
| 2946 |
+
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
| 2947 |
"dependencies": {
|
| 2948 |
+
"@vue/compiler-dom": "3.5.22",
|
| 2949 |
+
"@vue/compiler-sfc": "3.5.22",
|
| 2950 |
+
"@vue/runtime-dom": "3.5.22",
|
| 2951 |
+
"@vue/server-renderer": "3.5.22",
|
| 2952 |
+
"@vue/shared": "3.5.22"
|
| 2953 |
},
|
| 2954 |
"peerDependencies": {
|
| 2955 |
"typescript": "*"
|
|
|
|
| 2986 |
}
|
| 2987 |
},
|
| 2988 |
"node_modules/vue-router": {
|
| 2989 |
+
"version": "4.6.3",
|
| 2990 |
+
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
|
| 2991 |
+
"integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==",
|
| 2992 |
"dependencies": {
|
| 2993 |
"@vue/devtools-api": "^6.6.4"
|
| 2994 |
},
|
|
|
|
| 3016 |
}
|
| 3017 |
},
|
| 3018 |
"node_modules/ws": {
|
| 3019 |
+
"version": "8.17.1",
|
| 3020 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
| 3021 |
+
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
| 3022 |
"engines": {
|
| 3023 |
"node": ">=10.0.0"
|
| 3024 |
},
|
trigo-web/app/package.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
| 1 |
{
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "trigo-app",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"dev:host": "vite --host",
|
| 9 |
+
"build": "vite build",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"d3": "^7.9.0",
|
| 14 |
+
"d3-scale-chromatic": "^3.1.0",
|
| 15 |
+
"pinia": "^2.1.6",
|
| 16 |
+
"socket.io-client": "^4.5.2",
|
| 17 |
+
"three": "^0.156.1",
|
| 18 |
+
"vue": "^3.3.4",
|
| 19 |
+
"vue-router": "^4.2.4",
|
| 20 |
+
"onnxruntime-web": "^1.23.2"
|
| 21 |
+
},
|
| 22 |
+
"devDependencies": {
|
| 23 |
+
"@types/d3": "^7.4.3",
|
| 24 |
+
"@types/three": "^0.156.0",
|
| 25 |
+
"@vitejs/plugin-vue": "^5.2.4",
|
| 26 |
+
"sass-embedded": "^1.93.2",
|
| 27 |
+
"typescript": "^5.2.2",
|
| 28 |
+
"vite": "^5.4.21",
|
| 29 |
+
"vue-tsc": "^2.2.12",
|
| 30 |
+
"@types/node": "^24.10.1"
|
| 31 |
+
}
|
| 32 |
}
|
trigo-web/app/src/composables/useSocket.ts
CHANGED
|
@@ -13,17 +13,9 @@ export function useSocket() {
|
|
| 13 |
// Get or create socket instance
|
| 14 |
const getSocket = (): Socket => {
|
| 15 |
if (!socketInstance) {
|
| 16 |
-
//
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
let serverUrl: string;
|
| 20 |
-
if (isDev) {
|
| 21 |
-
// Development: Use VITE_SERVER_URL from .env or .env.local
|
| 22 |
-
serverUrl = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
|
| 23 |
-
} else {
|
| 24 |
-
// Production: same origin
|
| 25 |
-
serverUrl = window.location.origin;
|
| 26 |
-
}
|
| 27 |
|
| 28 |
console.log("[Socket.io] Connecting to:", serverUrl);
|
| 29 |
|
|
@@ -32,8 +24,8 @@ export function useSocket() {
|
|
| 32 |
reconnection: true,
|
| 33 |
reconnectionDelay: 1000,
|
| 34 |
reconnectionAttempts: 5,
|
| 35 |
-
//
|
| 36 |
-
transports: ["
|
| 37 |
});
|
| 38 |
|
| 39 |
// Connection event handlers
|
|
|
|
| 13 |
// Get or create socket instance
|
| 14 |
const getSocket = (): Socket => {
|
| 15 |
if (!socketInstance) {
|
| 16 |
+
// Always use current origin - Vite dev server proxies /socket.io to backend
|
| 17 |
+
// This ensures socket connections work through tunnels and proxies
|
| 18 |
+
const serverUrl = window.location.origin;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
console.log("[Socket.io] Connecting to:", serverUrl);
|
| 21 |
|
|
|
|
| 24 |
reconnection: true,
|
| 25 |
reconnectionDelay: 1000,
|
| 26 |
reconnectionAttempts: 5,
|
| 27 |
+
// Use polling first for better tunnel/proxy compatibility, then upgrade to websocket
|
| 28 |
+
transports: ["polling", "websocket"]
|
| 29 |
});
|
| 30 |
|
| 31 |
// Connection event handlers
|
trigo-web/app/src/views/TrigoView.vue
CHANGED
|
@@ -1097,24 +1097,32 @@
|
|
| 1097 |
* Room list management for room selector dropdown
|
| 1098 |
*/
|
| 1099 |
function fetchRoomList() {
|
|
|
|
| 1100 |
isLoadingRooms.value = true;
|
| 1101 |
socketApi.listRooms((response: any) => {
|
|
|
|
| 1102 |
isLoadingRooms.value = false;
|
| 1103 |
if (response.success) {
|
| 1104 |
roomList.value = response.rooms;
|
|
|
|
| 1105 |
}
|
| 1106 |
});
|
| 1107 |
}
|
| 1108 |
|
| 1109 |
function setupRoomListeners() {
|
|
|
|
|
|
|
| 1110 |
socketApi.onRoomCreated((data: RoomSummary) => {
|
|
|
|
| 1111 |
// Add new room to list if not already present
|
| 1112 |
if (!roomList.value.find(r => r.id === data.id)) {
|
| 1113 |
roomList.value.push(data);
|
|
|
|
| 1114 |
}
|
| 1115 |
});
|
| 1116 |
|
| 1117 |
socketApi.onRoomUpdated((data: RoomSummary) => {
|
|
|
|
| 1118 |
const index = roomList.value.findIndex(r => r.id === data.id);
|
| 1119 |
if (index >= 0) {
|
| 1120 |
roomList.value[index] = data;
|
|
@@ -1124,6 +1132,7 @@
|
|
| 1124 |
});
|
| 1125 |
|
| 1126 |
socketApi.onRoomDeleted((data: { roomId: string }) => {
|
|
|
|
| 1127 |
roomList.value = roomList.value.filter(r => r.id !== data.roomId);
|
| 1128 |
});
|
| 1129 |
}
|
|
|
|
| 1097 |
* Room list management for room selector dropdown
|
| 1098 |
*/
|
| 1099 |
function fetchRoomList() {
|
| 1100 |
+
console.log("[TrigoView] Fetching room list...");
|
| 1101 |
isLoadingRooms.value = true;
|
| 1102 |
socketApi.listRooms((response: any) => {
|
| 1103 |
+
console.log("[TrigoView] listRooms response:", response);
|
| 1104 |
isLoadingRooms.value = false;
|
| 1105 |
if (response.success) {
|
| 1106 |
roomList.value = response.rooms;
|
| 1107 |
+
console.log("[TrigoView] Room list updated, count:", roomList.value.length);
|
| 1108 |
}
|
| 1109 |
});
|
| 1110 |
}
|
| 1111 |
|
| 1112 |
function setupRoomListeners() {
|
| 1113 |
+
console.log("[TrigoView] Setting up room listeners");
|
| 1114 |
+
|
| 1115 |
socketApi.onRoomCreated((data: RoomSummary) => {
|
| 1116 |
+
console.log("[TrigoView] Received roomCreated event:", data);
|
| 1117 |
// Add new room to list if not already present
|
| 1118 |
if (!roomList.value.find(r => r.id === data.id)) {
|
| 1119 |
roomList.value.push(data);
|
| 1120 |
+
console.log("[TrigoView] Added room to list, new count:", roomList.value.length);
|
| 1121 |
}
|
| 1122 |
});
|
| 1123 |
|
| 1124 |
socketApi.onRoomUpdated((data: RoomSummary) => {
|
| 1125 |
+
console.log("[TrigoView] Received roomUpdated event:", data);
|
| 1126 |
const index = roomList.value.findIndex(r => r.id === data.id);
|
| 1127 |
if (index >= 0) {
|
| 1128 |
roomList.value[index] = data;
|
|
|
|
| 1132 |
});
|
| 1133 |
|
| 1134 |
socketApi.onRoomDeleted((data: { roomId: string }) => {
|
| 1135 |
+
console.log("[TrigoView] Received roomDeleted event:", data);
|
| 1136 |
roomList.value = roomList.value.filter(r => r.id !== data.roomId);
|
| 1137 |
});
|
| 1138 |
}
|
trigo-web/app/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
|
| 9 |
+
/* Bundler mode */
|
| 10 |
+
"moduleResolution": "bundler",
|
| 11 |
+
"allowImportingTsExtensions": true,
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"noEmit": true,
|
| 15 |
+
"jsx": "preserve",
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"noFallthroughCasesInSwitch": true,
|
| 22 |
+
|
| 23 |
+
/* Path aliases */
|
| 24 |
+
"baseUrl": ".",
|
| 25 |
+
"paths": {
|
| 26 |
+
"@/*": ["./src/*"],
|
| 27 |
+
"@inc/*": ["../inc/*"]
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
| 31 |
+
}
|
trigo-web/app/tsconfig.node.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"skipLibCheck": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "bundler",
|
| 7 |
+
"allowSyntheticDefaultImports": true
|
| 8 |
+
},
|
| 9 |
+
"include": ["vite.config.ts"]
|
| 10 |
+
}
|
trigo-web/app/vite.config.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
| 1 |
import { defineConfig, loadEnv } from "vite";
|
| 2 |
import vue from "@vitejs/plugin-vue";
|
| 3 |
-
import
|
| 4 |
-
import { fileURLToPath } from "node:url";
|
| 5 |
-
|
| 6 |
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 7 |
|
| 8 |
// https://vitejs.dev/config/
|
| 9 |
export default defineConfig(({ mode }) => {
|
| 10 |
// Load env file from repository root (parent directory)
|
| 11 |
-
const
|
| 12 |
-
const env = loadEnv(mode, rootDir, "");
|
| 13 |
|
| 14 |
return {
|
| 15 |
// Load .env files from repository root
|
|
@@ -33,11 +29,11 @@ export default defineConfig(({ mode }) => {
|
|
| 33 |
}
|
| 34 |
],
|
| 35 |
// Point to parent project's public directory
|
| 36 |
-
publicDir:
|
| 37 |
resolve: {
|
| 38 |
alias: {
|
| 39 |
-
"@":
|
| 40 |
-
"@inc":
|
| 41 |
}
|
| 42 |
},
|
| 43 |
server: {
|
|
@@ -48,6 +44,14 @@ export default defineConfig(({ mode }) => {
|
|
| 48 |
fs: {
|
| 49 |
// Allow serving files from node_modules
|
| 50 |
allow: ["..", "../.."]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
},
|
| 53 |
optimizeDeps: {
|
|
@@ -61,7 +65,7 @@ export default defineConfig(({ mode }) => {
|
|
| 61 |
css: {
|
| 62 |
preprocessorOptions: {
|
| 63 |
scss: {
|
| 64 |
-
|
| 65 |
}
|
| 66 |
}
|
| 67 |
},
|
|
|
|
| 1 |
import { defineConfig, loadEnv } from "vite";
|
| 2 |
import vue from "@vitejs/plugin-vue";
|
| 3 |
+
import { fileURLToPath, URL } from "node:url";
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
// https://vitejs.dev/config/
|
| 6 |
export default defineConfig(({ mode }) => {
|
| 7 |
// Load env file from repository root (parent directory)
|
| 8 |
+
const env = loadEnv(mode, fileURLToPath(new URL("..", import.meta.url)), "");
|
|
|
|
| 9 |
|
| 10 |
return {
|
| 11 |
// Load .env files from repository root
|
|
|
|
| 29 |
}
|
| 30 |
],
|
| 31 |
// Point to parent project's public directory
|
| 32 |
+
publicDir: fileURLToPath(new URL("../public", import.meta.url)),
|
| 33 |
resolve: {
|
| 34 |
alias: {
|
| 35 |
+
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
| 36 |
+
"@inc": fileURLToPath(new URL("../inc", import.meta.url))
|
| 37 |
}
|
| 38 |
},
|
| 39 |
server: {
|
|
|
|
| 44 |
fs: {
|
| 45 |
// Allow serving files from node_modules
|
| 46 |
allow: ["..", "../.."]
|
| 47 |
+
},
|
| 48 |
+
// Proxy socket.io requests to backend server
|
| 49 |
+
proxy: {
|
| 50 |
+
"/socket.io": {
|
| 51 |
+
target: env.VITE_SERVER_URL || "http://localhost:8157",
|
| 52 |
+
changeOrigin: true,
|
| 53 |
+
ws: true // Enable WebSocket proxying
|
| 54 |
+
}
|
| 55 |
}
|
| 56 |
},
|
| 57 |
optimizeDeps: {
|
|
|
|
| 65 |
css: {
|
| 66 |
preprocessorOptions: {
|
| 67 |
scss: {
|
| 68 |
+
api: "modern" // Use modern Sass API
|
| 69 |
}
|
| 70 |
}
|
| 71 |
},
|
trigo-web/backend/.env.local
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
PORT=8157
|
|
|
|
|
|
|
|
|
trigo-web/backend/dist/backend/src/server.js
DELETED
|
@@ -1,2104 +0,0 @@
|
|
| 1 |
-
// backend/src/server.ts
|
| 2 |
-
import express from "express";
|
| 3 |
-
import { createServer } from "http";
|
| 4 |
-
import { Server } from "socket.io";
|
| 5 |
-
import cors from "cors";
|
| 6 |
-
import dotenv from "dotenv";
|
| 7 |
-
import path from "path";
|
| 8 |
-
import fs from "fs";
|
| 9 |
-
import { fileURLToPath } from "url";
|
| 10 |
-
|
| 11 |
-
// backend/src/services/gameManager.ts
|
| 12 |
-
import { v4 as uuidv4 } from "uuid";
|
| 13 |
-
|
| 14 |
-
// inc/trigo/gameUtils.ts
|
| 15 |
-
var StoneType = {
|
| 16 |
-
EMPTY: 0,
|
| 17 |
-
BLACK: 1,
|
| 18 |
-
WHITE: 2
|
| 19 |
-
};
|
| 20 |
-
function getEnemyColor(color) {
|
| 21 |
-
if (color === StoneType.BLACK) return StoneType.WHITE;
|
| 22 |
-
if (color === StoneType.WHITE) return StoneType.BLACK;
|
| 23 |
-
return StoneType.EMPTY;
|
| 24 |
-
}
|
| 25 |
-
function isInBounds(pos, shape) {
|
| 26 |
-
return pos.x >= 0 && pos.x < shape.x && pos.y >= 0 && pos.y < shape.y && pos.z >= 0 && pos.z < shape.z;
|
| 27 |
-
}
|
| 28 |
-
function getNeighbors(pos, shape) {
|
| 29 |
-
const neighbors = [];
|
| 30 |
-
const directions = [
|
| 31 |
-
{ x: 1, y: 0, z: 0 },
|
| 32 |
-
{ x: -1, y: 0, z: 0 },
|
| 33 |
-
{ x: 0, y: 1, z: 0 },
|
| 34 |
-
{ x: 0, y: -1, z: 0 },
|
| 35 |
-
{ x: 0, y: 0, z: 1 },
|
| 36 |
-
{ x: 0, y: 0, z: -1 }
|
| 37 |
-
];
|
| 38 |
-
for (const dir of directions) {
|
| 39 |
-
const neighbor = {
|
| 40 |
-
x: pos.x + dir.x,
|
| 41 |
-
y: pos.y + dir.y,
|
| 42 |
-
z: pos.z + dir.z
|
| 43 |
-
};
|
| 44 |
-
if (isInBounds(neighbor, shape)) {
|
| 45 |
-
neighbors.push(neighbor);
|
| 46 |
-
}
|
| 47 |
-
}
|
| 48 |
-
return neighbors;
|
| 49 |
-
}
|
| 50 |
-
function positionsEqual(p1, p2) {
|
| 51 |
-
return p1.x === p2.x && p1.y === p2.y && p1.z === p2.z;
|
| 52 |
-
}
|
| 53 |
-
var CoordSet = class {
|
| 54 |
-
constructor() {
|
| 55 |
-
this.positions = [];
|
| 56 |
-
}
|
| 57 |
-
has(pos) {
|
| 58 |
-
return this.positions.some((p) => positionsEqual(p, pos));
|
| 59 |
-
}
|
| 60 |
-
insert(pos) {
|
| 61 |
-
if (!this.has(pos)) {
|
| 62 |
-
this.positions.push(pos);
|
| 63 |
-
return true;
|
| 64 |
-
}
|
| 65 |
-
return false;
|
| 66 |
-
}
|
| 67 |
-
remove(pos) {
|
| 68 |
-
this.positions = this.positions.filter((p) => !positionsEqual(p, pos));
|
| 69 |
-
}
|
| 70 |
-
size() {
|
| 71 |
-
return this.positions.length;
|
| 72 |
-
}
|
| 73 |
-
empty() {
|
| 74 |
-
return this.positions.length === 0;
|
| 75 |
-
}
|
| 76 |
-
forEach(callback) {
|
| 77 |
-
this.positions.forEach(callback);
|
| 78 |
-
}
|
| 79 |
-
toArray() {
|
| 80 |
-
return [...this.positions];
|
| 81 |
-
}
|
| 82 |
-
clear() {
|
| 83 |
-
this.positions = [];
|
| 84 |
-
}
|
| 85 |
-
};
|
| 86 |
-
var Patch = class {
|
| 87 |
-
constructor(color = StoneType.EMPTY) {
|
| 88 |
-
this.positions = new CoordSet();
|
| 89 |
-
this.color = StoneType.EMPTY;
|
| 90 |
-
this.color = color;
|
| 91 |
-
}
|
| 92 |
-
addStone(pos) {
|
| 93 |
-
this.positions.insert(pos);
|
| 94 |
-
}
|
| 95 |
-
size() {
|
| 96 |
-
return this.positions.size();
|
| 97 |
-
}
|
| 98 |
-
/**
|
| 99 |
-
* Get all liberties (empty adjacent positions) for this group
|
| 100 |
-
*
|
| 101 |
-
* Equivalent to StoneArray.patchAir() in prototype
|
| 102 |
-
* Returns a CoordSet of empty positions adjacent to this patch
|
| 103 |
-
*/
|
| 104 |
-
getLiberties(board, shape) {
|
| 105 |
-
const liberties = new CoordSet();
|
| 106 |
-
this.positions.forEach((stonePos) => {
|
| 107 |
-
const neighbors = getNeighbors(stonePos, shape);
|
| 108 |
-
for (const neighbor of neighbors) {
|
| 109 |
-
if (board[neighbor.x][neighbor.y][neighbor.z] === StoneType.EMPTY) {
|
| 110 |
-
liberties.insert(neighbor);
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
});
|
| 114 |
-
return liberties;
|
| 115 |
-
}
|
| 116 |
-
};
|
| 117 |
-
function findGroup(pos, board, shape) {
|
| 118 |
-
const color = board[pos.x][pos.y][pos.z];
|
| 119 |
-
const group = new Patch(color);
|
| 120 |
-
if (color === StoneType.EMPTY) {
|
| 121 |
-
return group;
|
| 122 |
-
}
|
| 123 |
-
const visited = new CoordSet();
|
| 124 |
-
const stack = [pos];
|
| 125 |
-
while (stack.length > 0) {
|
| 126 |
-
const current = stack.pop();
|
| 127 |
-
if (visited.has(current)) {
|
| 128 |
-
continue;
|
| 129 |
-
}
|
| 130 |
-
visited.insert(current);
|
| 131 |
-
if (board[current.x][current.y][current.z] === color) {
|
| 132 |
-
group.addStone(current);
|
| 133 |
-
const neighbors = getNeighbors(current, shape);
|
| 134 |
-
for (const neighbor of neighbors) {
|
| 135 |
-
if (!visited.has(neighbor)) {
|
| 136 |
-
stack.push(neighbor);
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
}
|
| 140 |
-
}
|
| 141 |
-
return group;
|
| 142 |
-
}
|
| 143 |
-
function getNeighborGroups(pos, board, shape, excludeEmpty = false) {
|
| 144 |
-
const neighbors = getNeighbors(pos, shape);
|
| 145 |
-
const groups = [];
|
| 146 |
-
const processedPositions = new CoordSet();
|
| 147 |
-
for (const neighbor of neighbors) {
|
| 148 |
-
if (processedPositions.has(neighbor)) {
|
| 149 |
-
continue;
|
| 150 |
-
}
|
| 151 |
-
const stone = board[neighbor.x][neighbor.y][neighbor.z];
|
| 152 |
-
if (excludeEmpty && stone === StoneType.EMPTY) {
|
| 153 |
-
continue;
|
| 154 |
-
}
|
| 155 |
-
if (stone !== StoneType.EMPTY) {
|
| 156 |
-
const group = findGroup(neighbor, board, shape);
|
| 157 |
-
group.positions.forEach((p) => processedPositions.insert(p));
|
| 158 |
-
groups.push(group);
|
| 159 |
-
}
|
| 160 |
-
}
|
| 161 |
-
return groups;
|
| 162 |
-
}
|
| 163 |
-
function isGroupCaptured(group, board, shape) {
|
| 164 |
-
const liberties = group.getLiberties(board, shape);
|
| 165 |
-
return liberties.size() === 0;
|
| 166 |
-
}
|
| 167 |
-
function findCapturedGroups(pos, playerColor, board, shape) {
|
| 168 |
-
const enemyColor = getEnemyColor(playerColor);
|
| 169 |
-
const captured = [];
|
| 170 |
-
const tempBoard = board.map((plane) => plane.map((row) => [...row]));
|
| 171 |
-
tempBoard[pos.x][pos.y][pos.z] = playerColor;
|
| 172 |
-
const neighborGroups = getNeighborGroups(pos, tempBoard, shape, true);
|
| 173 |
-
for (const group of neighborGroups) {
|
| 174 |
-
if (group.color === enemyColor) {
|
| 175 |
-
if (isGroupCaptured(group, tempBoard, shape)) {
|
| 176 |
-
captured.push(group);
|
| 177 |
-
}
|
| 178 |
-
}
|
| 179 |
-
}
|
| 180 |
-
return captured;
|
| 181 |
-
}
|
| 182 |
-
function isSuicideMove(pos, playerColor, board, shape) {
|
| 183 |
-
const tempBoard = board.map((plane) => plane.map((row) => [...row]));
|
| 184 |
-
tempBoard[pos.x][pos.y][pos.z] = playerColor;
|
| 185 |
-
const capturedGroups = findCapturedGroups(pos, playerColor, board, shape);
|
| 186 |
-
if (capturedGroups.length > 0) {
|
| 187 |
-
return false;
|
| 188 |
-
}
|
| 189 |
-
const placedGroup = findGroup(pos, tempBoard, shape);
|
| 190 |
-
const liberties = placedGroup.getLiberties(tempBoard, shape);
|
| 191 |
-
return liberties.size() === 0;
|
| 192 |
-
}
|
| 193 |
-
function isKoViolation(pos, playerColor, board, shape, lastCapturedPositions) {
|
| 194 |
-
if (!lastCapturedPositions || lastCapturedPositions.length !== 1) {
|
| 195 |
-
return false;
|
| 196 |
-
}
|
| 197 |
-
const capturedGroups = findCapturedGroups(pos, playerColor, board, shape);
|
| 198 |
-
if (capturedGroups.length !== 1 || capturedGroups[0].size() !== 1) {
|
| 199 |
-
return false;
|
| 200 |
-
}
|
| 201 |
-
const previouslyCaptured = lastCapturedPositions[0];
|
| 202 |
-
if (positionsEqual(pos, previouslyCaptured)) {
|
| 203 |
-
return true;
|
| 204 |
-
}
|
| 205 |
-
return false;
|
| 206 |
-
}
|
| 207 |
-
function executeCaptures(capturedGroups, board) {
|
| 208 |
-
const capturedPositions = [];
|
| 209 |
-
for (const group of capturedGroups) {
|
| 210 |
-
group.positions.forEach((pos) => {
|
| 211 |
-
board[pos.x][pos.y][pos.z] = StoneType.EMPTY;
|
| 212 |
-
capturedPositions.push(pos);
|
| 213 |
-
});
|
| 214 |
-
}
|
| 215 |
-
return capturedPositions;
|
| 216 |
-
}
|
| 217 |
-
function findEmptyRegion(startPos, board, shape, visited) {
|
| 218 |
-
const region = new CoordSet();
|
| 219 |
-
const stack = [startPos];
|
| 220 |
-
while (stack.length > 0) {
|
| 221 |
-
const pos = stack.pop();
|
| 222 |
-
if (visited.has(pos)) {
|
| 223 |
-
continue;
|
| 224 |
-
}
|
| 225 |
-
visited.insert(pos);
|
| 226 |
-
if (board[pos.x][pos.y][pos.z] === StoneType.EMPTY) {
|
| 227 |
-
region.insert(pos);
|
| 228 |
-
const neighbors = getNeighbors(pos, shape);
|
| 229 |
-
for (const neighbor of neighbors) {
|
| 230 |
-
if (!visited.has(neighbor)) {
|
| 231 |
-
stack.push(neighbor);
|
| 232 |
-
}
|
| 233 |
-
}
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
return region;
|
| 237 |
-
}
|
| 238 |
-
function determineRegionOwner(region, board, shape) {
|
| 239 |
-
let owner = StoneType.EMPTY;
|
| 240 |
-
let solved = false;
|
| 241 |
-
region.forEach((pos) => {
|
| 242 |
-
if (solved) return;
|
| 243 |
-
const neighbors = getNeighbors(pos, shape);
|
| 244 |
-
for (const neighbor of neighbors) {
|
| 245 |
-
if (solved) break;
|
| 246 |
-
const stone = board[neighbor.x][neighbor.y][neighbor.z];
|
| 247 |
-
if (stone !== StoneType.EMPTY) {
|
| 248 |
-
if (owner === StoneType.EMPTY) {
|
| 249 |
-
owner = stone;
|
| 250 |
-
} else if (owner !== stone) {
|
| 251 |
-
owner = StoneType.EMPTY;
|
| 252 |
-
solved = true;
|
| 253 |
-
}
|
| 254 |
-
}
|
| 255 |
-
}
|
| 256 |
-
});
|
| 257 |
-
return owner;
|
| 258 |
-
}
|
| 259 |
-
function calculateTerritory(board, shape) {
|
| 260 |
-
const result = {
|
| 261 |
-
black: 0,
|
| 262 |
-
white: 0,
|
| 263 |
-
neutral: 0,
|
| 264 |
-
blackTerritory: [],
|
| 265 |
-
whiteTerritory: [],
|
| 266 |
-
neutralTerritory: []
|
| 267 |
-
};
|
| 268 |
-
const visited = new CoordSet();
|
| 269 |
-
const emptyRegions = [];
|
| 270 |
-
for (let x = 0; x < shape.x; x++) {
|
| 271 |
-
for (let y = 0; y < shape.y; y++) {
|
| 272 |
-
for (let z = 0; z < shape.z; z++) {
|
| 273 |
-
const pos = { x, y, z };
|
| 274 |
-
const stone = board[x][y][z];
|
| 275 |
-
if (stone === StoneType.BLACK) {
|
| 276 |
-
result.black++;
|
| 277 |
-
result.blackTerritory.push(pos);
|
| 278 |
-
} else if (stone === StoneType.WHITE) {
|
| 279 |
-
result.white++;
|
| 280 |
-
result.whiteTerritory.push(pos);
|
| 281 |
-
} else if (!visited.has(pos)) {
|
| 282 |
-
const region = findEmptyRegion(pos, board, shape, visited);
|
| 283 |
-
emptyRegions.push(region);
|
| 284 |
-
}
|
| 285 |
-
}
|
| 286 |
-
}
|
| 287 |
-
}
|
| 288 |
-
for (const region of emptyRegions) {
|
| 289 |
-
const owner = determineRegionOwner(region, board, shape);
|
| 290 |
-
const regionArray = region.toArray();
|
| 291 |
-
if (owner === StoneType.BLACK) {
|
| 292 |
-
result.black += region.size();
|
| 293 |
-
result.blackTerritory.push(...regionArray);
|
| 294 |
-
} else if (owner === StoneType.WHITE) {
|
| 295 |
-
result.white += region.size();
|
| 296 |
-
result.whiteTerritory.push(...regionArray);
|
| 297 |
-
} else {
|
| 298 |
-
result.neutral += region.size();
|
| 299 |
-
result.neutralTerritory.push(...regionArray);
|
| 300 |
-
}
|
| 301 |
-
}
|
| 302 |
-
return result;
|
| 303 |
-
}
|
| 304 |
-
function validateMove(pos, playerColor, board, shape, lastCapturedPositions = null) {
|
| 305 |
-
if (!isInBounds(pos, shape)) {
|
| 306 |
-
return { valid: false, reason: "Position out of bounds" };
|
| 307 |
-
}
|
| 308 |
-
if (board[pos.x][pos.y][pos.z] !== StoneType.EMPTY) {
|
| 309 |
-
return { valid: false, reason: "Position already occupied" };
|
| 310 |
-
}
|
| 311 |
-
if (isKoViolation(pos, playerColor, board, shape, lastCapturedPositions)) {
|
| 312 |
-
return { valid: false, reason: "Ko rule violation" };
|
| 313 |
-
}
|
| 314 |
-
if (isSuicideMove(pos, playerColor, board, shape)) {
|
| 315 |
-
return { valid: false, reason: "suicide move not allowed" };
|
| 316 |
-
}
|
| 317 |
-
return { valid: true };
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
// inc/trigo/ab0yz.ts
|
| 321 |
-
var compactShape = (shape) => shape[shape.length - 1] === 1 ? compactShape(shape.slice(0, shape.length - 1)) : shape;
|
| 322 |
-
var encodeAb0yz = (pos, boardShape) => {
|
| 323 |
-
const compactedShape = compactShape(boardShape);
|
| 324 |
-
const result = [];
|
| 325 |
-
for (let i = 0; i < compactedShape.length; i++) {
|
| 326 |
-
const size = compactedShape[i];
|
| 327 |
-
const center = (size - 1) / 2;
|
| 328 |
-
const index = pos[i];
|
| 329 |
-
if (index === center) {
|
| 330 |
-
result.push("0");
|
| 331 |
-
} else if (index < center) {
|
| 332 |
-
result.push(String.fromCharCode(97 + index));
|
| 333 |
-
} else {
|
| 334 |
-
const offset = size - 1 - index;
|
| 335 |
-
result.push(String.fromCharCode(122 - offset));
|
| 336 |
-
}
|
| 337 |
-
}
|
| 338 |
-
return result.join("");
|
| 339 |
-
};
|
| 340 |
-
var decodeAb0yz = (code, boardShape) => {
|
| 341 |
-
const compactedShape = compactShape(boardShape);
|
| 342 |
-
if (code.length !== compactedShape.length) {
|
| 343 |
-
throw new Error(
|
| 344 |
-
`Invalid TGN coordinate: "${code}" (must be ${compactedShape.length} characters for board shape ${boardShape.join("x")})`
|
| 345 |
-
);
|
| 346 |
-
}
|
| 347 |
-
const result = [];
|
| 348 |
-
for (let i = 0; i < compactedShape.length; i++) {
|
| 349 |
-
const char = code[i];
|
| 350 |
-
const size = compactedShape[i];
|
| 351 |
-
const center = (size - 1) / 2;
|
| 352 |
-
if (char === "0") {
|
| 353 |
-
console.assert(Number.isInteger(center));
|
| 354 |
-
result.push(center);
|
| 355 |
-
} else {
|
| 356 |
-
const charCode = char.charCodeAt(0);
|
| 357 |
-
if (charCode >= 97 && charCode <= 122) {
|
| 358 |
-
const distFromA = charCode - 97;
|
| 359 |
-
const distFromZ = 122 - charCode;
|
| 360 |
-
if (distFromA < distFromZ) {
|
| 361 |
-
const index = distFromA;
|
| 362 |
-
if (index >= center) {
|
| 363 |
-
throw new Error(
|
| 364 |
-
`Invalid TGN coordinate: "${code}" (position ${index} >= center ${center} on axis ${i})`
|
| 365 |
-
);
|
| 366 |
-
}
|
| 367 |
-
result.push(index);
|
| 368 |
-
} else {
|
| 369 |
-
const index = size - 1 - distFromZ;
|
| 370 |
-
if (index <= center) {
|
| 371 |
-
throw new Error(
|
| 372 |
-
`Invalid TGN coordinate: "${code}" (position ${index} <= center ${center} on axis ${i})`
|
| 373 |
-
);
|
| 374 |
-
}
|
| 375 |
-
result.push(index);
|
| 376 |
-
}
|
| 377 |
-
} else {
|
| 378 |
-
throw new Error(
|
| 379 |
-
`Invalid TGN coordinate: "${code}" (character '${char}' at position ${i} must be '0' or a-z)`
|
| 380 |
-
);
|
| 381 |
-
}
|
| 382 |
-
}
|
| 383 |
-
}
|
| 384 |
-
while (result.length < boardShape.length) {
|
| 385 |
-
result.push(0);
|
| 386 |
-
}
|
| 387 |
-
return result;
|
| 388 |
-
};
|
| 389 |
-
|
| 390 |
-
// inc/tgn/tgnParser.ts
|
| 391 |
-
var TGNParseError = class extends Error {
|
| 392 |
-
constructor(message, line, column, hash) {
|
| 393 |
-
super(message);
|
| 394 |
-
this.line = line;
|
| 395 |
-
this.column = column;
|
| 396 |
-
this.hash = hash;
|
| 397 |
-
this.name = "TGNParseError";
|
| 398 |
-
}
|
| 399 |
-
};
|
| 400 |
-
var parserModule = null;
|
| 401 |
-
function getParser() {
|
| 402 |
-
if (!parserModule) {
|
| 403 |
-
throw new Error(
|
| 404 |
-
"TGN parser not loaded. Please ensure the parser has been built.\nRun: npm run build:parsers"
|
| 405 |
-
);
|
| 406 |
-
}
|
| 407 |
-
return parserModule;
|
| 408 |
-
}
|
| 409 |
-
function parseTGN(tgnString) {
|
| 410 |
-
const parser = getParser();
|
| 411 |
-
if (!parser.parse) {
|
| 412 |
-
throw new Error("TGN parser parse method not available");
|
| 413 |
-
}
|
| 414 |
-
try {
|
| 415 |
-
const result = parser.parse(tgnString);
|
| 416 |
-
return result;
|
| 417 |
-
} catch (error) {
|
| 418 |
-
throw new TGNParseError(
|
| 419 |
-
error.message || "Unknown parse error",
|
| 420 |
-
error.hash?.line,
|
| 421 |
-
error.hash?.loc?.first_column,
|
| 422 |
-
error.hash
|
| 423 |
-
);
|
| 424 |
-
}
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
// inc/trigo/game.ts
|
| 428 |
-
var TrigoGame = class _TrigoGame {
|
| 429 |
-
/**
|
| 430 |
-
* Constructor
|
| 431 |
-
* Equivalent to trigo.Game constructor (lines 75-85)
|
| 432 |
-
*/
|
| 433 |
-
constructor(shape = { x: 5, y: 5, z: 5 }, callbacks = {}) {
|
| 434 |
-
// Last captured stones for Ko rule detection
|
| 435 |
-
this.lastCapturedPositions = null;
|
| 436 |
-
// Static analysis cache (territory, capturing moves)
|
| 437 |
-
// Invalidated on any board state change
|
| 438 |
-
this.cachedTerritory = null;
|
| 439 |
-
this.cachedCapturingMove = /* @__PURE__ */ new Map();
|
| 440 |
-
this.shape = shape;
|
| 441 |
-
this.callbacks = callbacks;
|
| 442 |
-
this.board = this.createEmptyBoard();
|
| 443 |
-
this.currentPlayer = StoneType.BLACK;
|
| 444 |
-
this.stepHistory = [];
|
| 445 |
-
this.currentStepIndex = 0;
|
| 446 |
-
this.gameStatus = "idle";
|
| 447 |
-
this.gameResult = void 0;
|
| 448 |
-
this.passCount = 0;
|
| 449 |
-
}
|
| 450 |
-
/**
|
| 451 |
-
* Create an empty board
|
| 452 |
-
*/
|
| 453 |
-
createEmptyBoard() {
|
| 454 |
-
const board = [];
|
| 455 |
-
for (let x = 0; x < this.shape.x; x++) {
|
| 456 |
-
board[x] = [];
|
| 457 |
-
for (let y = 0; y < this.shape.y; y++) {
|
| 458 |
-
board[x][y] = [];
|
| 459 |
-
for (let z = 0; z < this.shape.z; z++) {
|
| 460 |
-
board[x][y][z] = StoneType.EMPTY;
|
| 461 |
-
}
|
| 462 |
-
}
|
| 463 |
-
}
|
| 464 |
-
return board;
|
| 465 |
-
}
|
| 466 |
-
/**
|
| 467 |
-
* Reset the game to initial state
|
| 468 |
-
* Equivalent to Game.reset() (lines 153-163)
|
| 469 |
-
*/
|
| 470 |
-
reset() {
|
| 471 |
-
this.board = this.createEmptyBoard();
|
| 472 |
-
this.currentPlayer = StoneType.BLACK;
|
| 473 |
-
this.stepHistory = [];
|
| 474 |
-
this.currentStepIndex = 0;
|
| 475 |
-
this.lastCapturedPositions = null;
|
| 476 |
-
this.invalidateAnalysisCache();
|
| 477 |
-
this.gameStatus = "idle";
|
| 478 |
-
this.gameResult = void 0;
|
| 479 |
-
this.passCount = 0;
|
| 480 |
-
}
|
| 481 |
-
/**
|
| 482 |
-
* Invalidate all static analysis caches
|
| 483 |
-
* Called when board state changes
|
| 484 |
-
*/
|
| 485 |
-
invalidateAnalysisCache() {
|
| 486 |
-
this.cachedTerritory = null;
|
| 487 |
-
this.cachedCapturingMove.clear();
|
| 488 |
-
}
|
| 489 |
-
/**
|
| 490 |
-
* Clone the game state (deep copy)
|
| 491 |
-
* Creates an independent copy with all state preserved
|
| 492 |
-
*/
|
| 493 |
-
clone() {
|
| 494 |
-
const cloned = new _TrigoGame(this.shape, {});
|
| 495 |
-
cloned.board = this.board.map((plane) => plane.map((row) => [...row]));
|
| 496 |
-
cloned.currentPlayer = this.currentPlayer;
|
| 497 |
-
cloned.currentStepIndex = this.currentStepIndex;
|
| 498 |
-
cloned.gameStatus = this.gameStatus;
|
| 499 |
-
cloned.passCount = this.passCount;
|
| 500 |
-
cloned.stepHistory = this.stepHistory.map((step) => ({
|
| 501 |
-
...step,
|
| 502 |
-
position: step.position ? { ...step.position } : void 0,
|
| 503 |
-
capturedPositions: step.capturedPositions ? step.capturedPositions.map((pos) => ({ ...pos })) : []
|
| 504 |
-
}));
|
| 505 |
-
cloned.lastCapturedPositions = this.lastCapturedPositions ? this.lastCapturedPositions.map((pos) => ({ ...pos })) : null;
|
| 506 |
-
if (this.gameResult) {
|
| 507 |
-
cloned.gameResult = {
|
| 508 |
-
...this.gameResult,
|
| 509 |
-
score: this.gameResult.score ? { ...this.gameResult.score } : void 0
|
| 510 |
-
};
|
| 511 |
-
}
|
| 512 |
-
cloned.invalidateAnalysisCache();
|
| 513 |
-
return cloned;
|
| 514 |
-
}
|
| 515 |
-
/**
|
| 516 |
-
* Get current board state (read-only)
|
| 517 |
-
*/
|
| 518 |
-
getBoard() {
|
| 519 |
-
return this.board.map((plane) => plane.map((row) => [...row]));
|
| 520 |
-
}
|
| 521 |
-
/**
|
| 522 |
-
* Get stone at specific position
|
| 523 |
-
* Equivalent to Game.stone() (lines 95-97)
|
| 524 |
-
*/
|
| 525 |
-
getStone(pos) {
|
| 526 |
-
return this.board[pos.x][pos.y][pos.z];
|
| 527 |
-
}
|
| 528 |
-
/**
|
| 529 |
-
* Get current player
|
| 530 |
-
*/
|
| 531 |
-
getCurrentPlayer() {
|
| 532 |
-
return this.currentPlayer;
|
| 533 |
-
}
|
| 534 |
-
/**
|
| 535 |
-
* Get current step number
|
| 536 |
-
* Equivalent to Game.currentStep() (lines 99-101)
|
| 537 |
-
*/
|
| 538 |
-
getCurrentStep() {
|
| 539 |
-
return this.currentStepIndex;
|
| 540 |
-
}
|
| 541 |
-
/**
|
| 542 |
-
* Get move history
|
| 543 |
-
* Equivalent to Game.routine() (lines 103-105)
|
| 544 |
-
*/
|
| 545 |
-
getHistory() {
|
| 546 |
-
return [...this.stepHistory];
|
| 547 |
-
}
|
| 548 |
-
/**
|
| 549 |
-
* Get last move
|
| 550 |
-
* Equivalent to Game.lastStep() (lines 107-110)
|
| 551 |
-
*/
|
| 552 |
-
getLastStep() {
|
| 553 |
-
if (this.currentStepIndex > 0) {
|
| 554 |
-
return this.stepHistory[this.currentStepIndex - 1];
|
| 555 |
-
}
|
| 556 |
-
return null;
|
| 557 |
-
}
|
| 558 |
-
/**
|
| 559 |
-
* Get board shape
|
| 560 |
-
* Equivalent to Game.shape() (lines 87-89)
|
| 561 |
-
*/
|
| 562 |
-
getShape() {
|
| 563 |
-
return { ...this.shape };
|
| 564 |
-
}
|
| 565 |
-
/**
|
| 566 |
-
* Get game status
|
| 567 |
-
*/
|
| 568 |
-
getGameStatus() {
|
| 569 |
-
return this.gameStatus;
|
| 570 |
-
}
|
| 571 |
-
/**
|
| 572 |
-
* Set game status
|
| 573 |
-
*/
|
| 574 |
-
setGameStatus(status) {
|
| 575 |
-
this.gameStatus = status;
|
| 576 |
-
}
|
| 577 |
-
/**
|
| 578 |
-
* Get game result
|
| 579 |
-
*/
|
| 580 |
-
getGameResult() {
|
| 581 |
-
return this.gameResult;
|
| 582 |
-
}
|
| 583 |
-
/**
|
| 584 |
-
* Get consecutive pass count
|
| 585 |
-
*/
|
| 586 |
-
getPassCount() {
|
| 587 |
-
return this.passCount;
|
| 588 |
-
}
|
| 589 |
-
/**
|
| 590 |
-
* Recalculate consecutive pass count based on current history
|
| 591 |
-
* Counts consecutive PASS steps from the end of current history
|
| 592 |
-
*/
|
| 593 |
-
recalculatePassCount() {
|
| 594 |
-
this.passCount = 0;
|
| 595 |
-
for (let i = this.currentStepIndex - 1; i >= 0; i--) {
|
| 596 |
-
if (this.stepHistory[i].type === 1 /* PASS */) {
|
| 597 |
-
this.passCount++;
|
| 598 |
-
} else {
|
| 599 |
-
break;
|
| 600 |
-
}
|
| 601 |
-
}
|
| 602 |
-
}
|
| 603 |
-
/**
|
| 604 |
-
* Start the game
|
| 605 |
-
*/
|
| 606 |
-
startGame() {
|
| 607 |
-
if (this.gameStatus === "idle") {
|
| 608 |
-
this.gameStatus = "playing";
|
| 609 |
-
}
|
| 610 |
-
}
|
| 611 |
-
/**
|
| 612 |
-
* Check if game is active
|
| 613 |
-
*/
|
| 614 |
-
isGameActive() {
|
| 615 |
-
return this.gameStatus === "playing";
|
| 616 |
-
}
|
| 617 |
-
/**
|
| 618 |
-
* Check if a move is valid
|
| 619 |
-
* Equivalent to Game.isDropable() and Game.isValidStep() (lines 112-151)
|
| 620 |
-
*/
|
| 621 |
-
isValidMove(pos, player) {
|
| 622 |
-
const playerColor = player || this.currentPlayer;
|
| 623 |
-
return validateMove(pos, playerColor, this.board, this.shape, this.lastCapturedPositions);
|
| 624 |
-
}
|
| 625 |
-
/**
|
| 626 |
-
* Get all valid move positions for current player (efficient batch query)
|
| 627 |
-
*
|
| 628 |
-
* This method is optimized to avoid repeated validation checks by:
|
| 629 |
-
* 1. Only checking empty positions
|
| 630 |
-
* 2. Skipping bounds checking (iterator is already within bounds)
|
| 631 |
-
* 3. Using low-level validation functions directly
|
| 632 |
-
* 4. Batching board state access
|
| 633 |
-
*
|
| 634 |
-
* @param player - Optional player color (defaults to current player)
|
| 635 |
-
* @returns Array of all valid move positions
|
| 636 |
-
*/
|
| 637 |
-
validMovePositions(player) {
|
| 638 |
-
const playerColor = player || this.currentPlayer;
|
| 639 |
-
const validPositions = [];
|
| 640 |
-
for (let x = 0; x < this.shape.x; x++) {
|
| 641 |
-
for (let y = 0; y < this.shape.y; y++) {
|
| 642 |
-
for (let z = 0; z < this.shape.z; z++) {
|
| 643 |
-
if (this.board[x][y][z] !== StoneType.EMPTY) {
|
| 644 |
-
continue;
|
| 645 |
-
}
|
| 646 |
-
const pos = { x, y, z };
|
| 647 |
-
if (isKoViolation(
|
| 648 |
-
pos,
|
| 649 |
-
playerColor,
|
| 650 |
-
this.board,
|
| 651 |
-
this.shape,
|
| 652 |
-
this.lastCapturedPositions
|
| 653 |
-
)) {
|
| 654 |
-
continue;
|
| 655 |
-
}
|
| 656 |
-
if (isSuicideMove(pos, playerColor, this.board, this.shape)) {
|
| 657 |
-
continue;
|
| 658 |
-
}
|
| 659 |
-
validPositions.push(pos);
|
| 660 |
-
}
|
| 661 |
-
}
|
| 662 |
-
}
|
| 663 |
-
return validPositions;
|
| 664 |
-
}
|
| 665 |
-
/**
|
| 666 |
-
* Check if any valid move can capture enemy stones
|
| 667 |
-
* Used by MCTS to determine if a position is truly terminal
|
| 668 |
-
*
|
| 669 |
-
* Results are cached and invalidated when board state changes.
|
| 670 |
-
*
|
| 671 |
-
* @param player - Optional player color (defaults to current player)
|
| 672 |
-
* @returns true if at least one valid move would capture stones
|
| 673 |
-
*/
|
| 674 |
-
hasCapturingMove(player) {
|
| 675 |
-
const playerColor = player ?? this.currentPlayer;
|
| 676 |
-
if (this.cachedCapturingMove.has(playerColor)) {
|
| 677 |
-
return this.cachedCapturingMove.get(playerColor);
|
| 678 |
-
}
|
| 679 |
-
const result = this.computeHasCapturingMove(playerColor);
|
| 680 |
-
this.cachedCapturingMove.set(playerColor, result);
|
| 681 |
-
return result;
|
| 682 |
-
}
|
| 683 |
-
/**
|
| 684 |
-
* Internal: Compute whether a player has any capturing move
|
| 685 |
-
*/
|
| 686 |
-
computeHasCapturingMove(playerColor) {
|
| 687 |
-
for (let x = 0; x < this.shape.x; x++) {
|
| 688 |
-
for (let y = 0; y < this.shape.y; y++) {
|
| 689 |
-
for (let z = 0; z < this.shape.z; z++) {
|
| 690 |
-
if (this.board[x][y][z] !== StoneType.EMPTY) {
|
| 691 |
-
continue;
|
| 692 |
-
}
|
| 693 |
-
const pos = { x, y, z };
|
| 694 |
-
if (isKoViolation(
|
| 695 |
-
pos,
|
| 696 |
-
playerColor,
|
| 697 |
-
this.board,
|
| 698 |
-
this.shape,
|
| 699 |
-
this.lastCapturedPositions
|
| 700 |
-
)) {
|
| 701 |
-
continue;
|
| 702 |
-
}
|
| 703 |
-
if (isSuicideMove(pos, playerColor, this.board, this.shape)) {
|
| 704 |
-
continue;
|
| 705 |
-
}
|
| 706 |
-
const capturedGroups = findCapturedGroups(pos, playerColor, this.board, this.shape);
|
| 707 |
-
if (capturedGroups.length > 0) {
|
| 708 |
-
return true;
|
| 709 |
-
}
|
| 710 |
-
}
|
| 711 |
-
}
|
| 712 |
-
}
|
| 713 |
-
return false;
|
| 714 |
-
}
|
| 715 |
-
/**
|
| 716 |
-
* Check if the game has reached a natural terminal state
|
| 717 |
-
*
|
| 718 |
-
* A game is naturally terminal when:
|
| 719 |
-
* - All territory is claimed (neutral === 0)
|
| 720 |
-
* - AND neither player has any capturing moves available
|
| 721 |
-
*
|
| 722 |
-
* This is different from the "finished" status which requires double pass.
|
| 723 |
-
* Natural termination means the game state is completely settled and
|
| 724 |
-
* no further moves can meaningfully change the outcome.
|
| 725 |
-
*
|
| 726 |
-
* NOTE: This method is expensive due to territory calculation and capture move checking.
|
| 727 |
-
* Use coverage ratio check as a pre-filter when calling frequently.
|
| 728 |
-
*
|
| 729 |
-
* Used by:
|
| 730 |
-
* - MCTS agent (terminal detection for tree search)
|
| 731 |
-
* - Model battle (early stopping when settled)
|
| 732 |
-
* - Self-play games (early stopping when settled)
|
| 733 |
-
*
|
| 734 |
-
* @returns true if the game is naturally terminal, false otherwise
|
| 735 |
-
*/
|
| 736 |
-
isNaturallyTerminal() {
|
| 737 |
-
const territory = this.getTerritory();
|
| 738 |
-
if (territory.neutral !== 0) {
|
| 739 |
-
return false;
|
| 740 |
-
}
|
| 741 |
-
const blackCanCapture = this.hasCapturingMove(StoneType.BLACK);
|
| 742 |
-
const whiteCanCapture = this.hasCapturingMove(StoneType.WHITE);
|
| 743 |
-
return !blackCanCapture && !whiteCanCapture;
|
| 744 |
-
}
|
| 745 |
-
/**
|
| 746 |
-
* Reset pass count (called when a stone is placed)
|
| 747 |
-
*/
|
| 748 |
-
resetPassCount() {
|
| 749 |
-
this.passCount = 0;
|
| 750 |
-
}
|
| 751 |
-
/**
|
| 752 |
-
* Place a stone (drop move)
|
| 753 |
-
* Equivalent to Game.drop() and Game.appendStone() (lines 181-273)
|
| 754 |
-
*
|
| 755 |
-
* @returns true if move was successful, false otherwise
|
| 756 |
-
*/
|
| 757 |
-
drop(pos) {
|
| 758 |
-
const validation = this.isValidMove(pos);
|
| 759 |
-
if (!validation.valid) {
|
| 760 |
-
console.warn(`Invalid move at (${pos.x}, ${pos.y}, ${pos.z}): ${validation.reason}`);
|
| 761 |
-
return false;
|
| 762 |
-
}
|
| 763 |
-
const capturedGroups = findCapturedGroups(pos, this.currentPlayer, this.board, this.shape);
|
| 764 |
-
this.board[pos.x][pos.y][pos.z] = this.currentPlayer;
|
| 765 |
-
const capturedPositions = executeCaptures(capturedGroups, this.board);
|
| 766 |
-
this.lastCapturedPositions = capturedPositions.length > 0 ? capturedPositions : null;
|
| 767 |
-
this.invalidateAnalysisCache();
|
| 768 |
-
this.resetPassCount();
|
| 769 |
-
const step = {
|
| 770 |
-
type: 0 /* DROP */,
|
| 771 |
-
position: pos,
|
| 772 |
-
player: this.currentPlayer,
|
| 773 |
-
capturedPositions: capturedPositions.length > 0 ? capturedPositions : void 0,
|
| 774 |
-
timestamp: Date.now()
|
| 775 |
-
};
|
| 776 |
-
this.advanceStep(step);
|
| 777 |
-
if (capturedPositions.length > 0 && this.callbacks.onCapture) {
|
| 778 |
-
this.callbacks.onCapture(capturedPositions);
|
| 779 |
-
}
|
| 780 |
-
if (this.callbacks.onTerritoryChange) {
|
| 781 |
-
this.callbacks.onTerritoryChange(this.getTerritory());
|
| 782 |
-
}
|
| 783 |
-
return true;
|
| 784 |
-
}
|
| 785 |
-
/**
|
| 786 |
-
* Pass turn
|
| 787 |
-
* Equivalent to PASS step type in prototype
|
| 788 |
-
*/
|
| 789 |
-
pass() {
|
| 790 |
-
const step = {
|
| 791 |
-
type: 1 /* PASS */,
|
| 792 |
-
player: this.currentPlayer,
|
| 793 |
-
timestamp: Date.now()
|
| 794 |
-
};
|
| 795 |
-
this.lastCapturedPositions = null;
|
| 796 |
-
this.passCount++;
|
| 797 |
-
this.advanceStep(step);
|
| 798 |
-
if (this.passCount >= 2) {
|
| 799 |
-
const territory = this.getTerritory();
|
| 800 |
-
const capturedCounts = this.getCapturedCounts();
|
| 801 |
-
const blackTotal = territory.black + capturedCounts.white;
|
| 802 |
-
const whiteTotal = territory.white + capturedCounts.black;
|
| 803 |
-
let winner;
|
| 804 |
-
if (blackTotal > whiteTotal) {
|
| 805 |
-
winner = "black";
|
| 806 |
-
} else if (whiteTotal > blackTotal) {
|
| 807 |
-
winner = "white";
|
| 808 |
-
} else {
|
| 809 |
-
winner = "draw";
|
| 810 |
-
}
|
| 811 |
-
this.gameResult = {
|
| 812 |
-
winner,
|
| 813 |
-
reason: "double-pass",
|
| 814 |
-
score: territory
|
| 815 |
-
};
|
| 816 |
-
this.gameStatus = "finished";
|
| 817 |
-
if (this.callbacks.onWin) {
|
| 818 |
-
const winnerStone = winner === "black" ? StoneType.BLACK : winner === "white" ? StoneType.WHITE : StoneType.EMPTY;
|
| 819 |
-
this.callbacks.onWin(winnerStone);
|
| 820 |
-
}
|
| 821 |
-
}
|
| 822 |
-
return true;
|
| 823 |
-
}
|
| 824 |
-
/**
|
| 825 |
-
* Surrender/resign
|
| 826 |
-
* Equivalent to Game.step() with SURRENDER type (lines 176-178)
|
| 827 |
-
*/
|
| 828 |
-
surrender() {
|
| 829 |
-
const surrenderingPlayer = this.currentPlayer;
|
| 830 |
-
const step = {
|
| 831 |
-
type: 2 /* SURRENDER */,
|
| 832 |
-
player: this.currentPlayer,
|
| 833 |
-
timestamp: Date.now()
|
| 834 |
-
};
|
| 835 |
-
this.advanceStep(step);
|
| 836 |
-
const winner = surrenderingPlayer === StoneType.BLACK ? "white" : "black";
|
| 837 |
-
this.gameResult = {
|
| 838 |
-
winner,
|
| 839 |
-
reason: "resignation"
|
| 840 |
-
};
|
| 841 |
-
this.gameStatus = "finished";
|
| 842 |
-
const winnerStone = getEnemyColor(surrenderingPlayer);
|
| 843 |
-
if (this.callbacks.onWin) {
|
| 844 |
-
this.callbacks.onWin(winnerStone);
|
| 845 |
-
}
|
| 846 |
-
return true;
|
| 847 |
-
}
|
| 848 |
-
/**
|
| 849 |
-
* Undo last move
|
| 850 |
-
* Equivalent to Game.repent() (lines 197-230)
|
| 851 |
-
*
|
| 852 |
-
* @returns true if undo was successful, false if no moves to undo
|
| 853 |
-
*/
|
| 854 |
-
undo() {
|
| 855 |
-
if (this.currentStepIndex === 0 || this.stepHistory.length === 0) {
|
| 856 |
-
return false;
|
| 857 |
-
}
|
| 858 |
-
const lastStep = this.stepHistory[this.currentStepIndex - 1];
|
| 859 |
-
if (lastStep.type === 0 /* DROP */ && lastStep.position) {
|
| 860 |
-
this.board[lastStep.position.x][lastStep.position.y][lastStep.position.z] = StoneType.EMPTY;
|
| 861 |
-
if (lastStep.capturedPositions) {
|
| 862 |
-
const enemyColor = getEnemyColor(lastStep.player);
|
| 863 |
-
for (const pos of lastStep.capturedPositions) {
|
| 864 |
-
this.board[pos.x][pos.y][pos.z] = enemyColor;
|
| 865 |
-
}
|
| 866 |
-
}
|
| 867 |
-
}
|
| 868 |
-
this.currentStepIndex--;
|
| 869 |
-
this.currentPlayer = lastStep.player;
|
| 870 |
-
this.recalculatePassCount();
|
| 871 |
-
if (this.currentStepIndex > 0) {
|
| 872 |
-
const previousStep = this.stepHistory[this.currentStepIndex - 1];
|
| 873 |
-
this.lastCapturedPositions = previousStep.capturedPositions || null;
|
| 874 |
-
} else {
|
| 875 |
-
this.lastCapturedPositions = null;
|
| 876 |
-
}
|
| 877 |
-
this.invalidateAnalysisCache();
|
| 878 |
-
if (this.callbacks.onStepBack) {
|
| 879 |
-
this.callbacks.onStepBack(lastStep, this.stepHistory.slice(0, this.currentStepIndex));
|
| 880 |
-
}
|
| 881 |
-
return true;
|
| 882 |
-
}
|
| 883 |
-
/**
|
| 884 |
-
* Redo next move (after undo)
|
| 885 |
-
*
|
| 886 |
-
* @returns true if redo was successful, false if no moves to redo
|
| 887 |
-
*/
|
| 888 |
-
redo() {
|
| 889 |
-
if (this.currentStepIndex >= this.stepHistory.length) {
|
| 890 |
-
return false;
|
| 891 |
-
}
|
| 892 |
-
const nextStep = this.stepHistory[this.currentStepIndex];
|
| 893 |
-
if (nextStep.type === 0 /* DROP */ && nextStep.position) {
|
| 894 |
-
this.board[nextStep.position.x][nextStep.position.y][nextStep.position.z] = nextStep.player;
|
| 895 |
-
if (nextStep.capturedPositions) {
|
| 896 |
-
for (const pos of nextStep.capturedPositions) {
|
| 897 |
-
this.board[pos.x][pos.y][pos.z] = StoneType.EMPTY;
|
| 898 |
-
}
|
| 899 |
-
}
|
| 900 |
-
this.lastCapturedPositions = nextStep.capturedPositions || null;
|
| 901 |
-
} else if (nextStep.type === 1 /* PASS */) {
|
| 902 |
-
this.lastCapturedPositions = null;
|
| 903 |
-
}
|
| 904 |
-
this.currentStepIndex++;
|
| 905 |
-
this.currentPlayer = getEnemyColor(nextStep.player);
|
| 906 |
-
this.invalidateAnalysisCache();
|
| 907 |
-
if (this.callbacks.onStepAdvance) {
|
| 908 |
-
this.callbacks.onStepAdvance(
|
| 909 |
-
nextStep,
|
| 910 |
-
this.stepHistory.slice(0, this.currentStepIndex)
|
| 911 |
-
);
|
| 912 |
-
}
|
| 913 |
-
return true;
|
| 914 |
-
}
|
| 915 |
-
/**
|
| 916 |
-
* Check if redo is available
|
| 917 |
-
*/
|
| 918 |
-
canRedo() {
|
| 919 |
-
return this.currentStepIndex < this.stepHistory.length;
|
| 920 |
-
}
|
| 921 |
-
/**
|
| 922 |
-
* Jump to specific step in history
|
| 923 |
-
* Rebuilds board state after applying the first 'index' moves
|
| 924 |
-
*
|
| 925 |
-
* @param index Number of moves to apply from history (0 for initial state, 1 for after first move, etc.)
|
| 926 |
-
* @returns true if jump was successful
|
| 927 |
-
*/
|
| 928 |
-
jumpToStep(index) {
|
| 929 |
-
if (index < 0 || index > this.stepHistory.length) {
|
| 930 |
-
return false;
|
| 931 |
-
}
|
| 932 |
-
if (index === this.currentStepIndex) {
|
| 933 |
-
return false;
|
| 934 |
-
}
|
| 935 |
-
this.board = this.createEmptyBoard();
|
| 936 |
-
this.lastCapturedPositions = null;
|
| 937 |
-
for (let i = 0; i < index; i++) {
|
| 938 |
-
const step = this.stepHistory[i];
|
| 939 |
-
if (step.type === 0 /* DROP */ && step.position) {
|
| 940 |
-
const pos = step.position;
|
| 941 |
-
this.board[pos.x][pos.y][pos.z] = step.player;
|
| 942 |
-
if (step.capturedPositions) {
|
| 943 |
-
for (const capturedPos of step.capturedPositions) {
|
| 944 |
-
this.board[capturedPos.x][capturedPos.y][capturedPos.z] = StoneType.EMPTY;
|
| 945 |
-
}
|
| 946 |
-
}
|
| 947 |
-
}
|
| 948 |
-
}
|
| 949 |
-
if (index > 0) {
|
| 950 |
-
const lastAppliedStep = this.stepHistory[index - 1];
|
| 951 |
-
if (lastAppliedStep.type === 0 /* DROP */) {
|
| 952 |
-
this.lastCapturedPositions = lastAppliedStep.capturedPositions || null;
|
| 953 |
-
} else if (lastAppliedStep.type === 1 /* PASS */) {
|
| 954 |
-
this.lastCapturedPositions = null;
|
| 955 |
-
}
|
| 956 |
-
} else {
|
| 957 |
-
this.lastCapturedPositions = null;
|
| 958 |
-
}
|
| 959 |
-
const oldStepIndex = this.currentStepIndex;
|
| 960 |
-
this.currentStepIndex = index;
|
| 961 |
-
const movesPlayed = index;
|
| 962 |
-
this.currentPlayer = movesPlayed % 2 === 0 ? StoneType.BLACK : StoneType.WHITE;
|
| 963 |
-
this.recalculatePassCount();
|
| 964 |
-
this.invalidateAnalysisCache();
|
| 965 |
-
if (index < oldStepIndex && this.callbacks.onStepBack) {
|
| 966 |
-
const currentStep = this.stepHistory[index];
|
| 967 |
-
this.callbacks.onStepBack(currentStep, this.stepHistory.slice(0, index + 1));
|
| 968 |
-
} else if (index > oldStepIndex && this.callbacks.onStepAdvance) {
|
| 969 |
-
const currentStep = this.stepHistory[index];
|
| 970 |
-
this.callbacks.onStepAdvance(currentStep, this.stepHistory.slice(0, index + 1));
|
| 971 |
-
}
|
| 972 |
-
return true;
|
| 973 |
-
}
|
| 974 |
-
/**
|
| 975 |
-
* Advance to next step
|
| 976 |
-
* Equivalent to Game.stepAdvance() (lines 279-287)
|
| 977 |
-
*/
|
| 978 |
-
advanceStep(step) {
|
| 979 |
-
if (this.currentStepIndex < this.stepHistory.length) {
|
| 980 |
-
this.stepHistory = this.stepHistory.slice(0, this.currentStepIndex);
|
| 981 |
-
}
|
| 982 |
-
this.stepHistory.push(step);
|
| 983 |
-
this.currentStepIndex++;
|
| 984 |
-
this.currentPlayer = getEnemyColor(this.currentPlayer);
|
| 985 |
-
if (this.callbacks.onStepAdvance) {
|
| 986 |
-
this.callbacks.onStepAdvance(step, this.stepHistory);
|
| 987 |
-
}
|
| 988 |
-
}
|
| 989 |
-
/**
|
| 990 |
-
* Get territory calculation
|
| 991 |
-
* Equivalent to Game.blackDomain() and Game.whiteDomain() (lines 232-244)
|
| 992 |
-
*
|
| 993 |
-
* Returns cached result if territory hasn't changed
|
| 994 |
-
*/
|
| 995 |
-
getTerritory() {
|
| 996 |
-
if (!this.cachedTerritory)
|
| 997 |
-
this.cachedTerritory = calculateTerritory(this.board, this.shape);
|
| 998 |
-
return this.cachedTerritory;
|
| 999 |
-
}
|
| 1000 |
-
/**
|
| 1001 |
-
* Get captured stone counts up to current position in history
|
| 1002 |
-
* Only counts captures that have been played (up to currentStepIndex)
|
| 1003 |
-
*/
|
| 1004 |
-
getCapturedCounts() {
|
| 1005 |
-
const counts = { black: 0, white: 0 };
|
| 1006 |
-
for (let i = 0; i < this.currentStepIndex; i++) {
|
| 1007 |
-
const step = this.stepHistory[i];
|
| 1008 |
-
if (step.capturedPositions && step.capturedPositions.length > 0) {
|
| 1009 |
-
const enemyColor = getEnemyColor(step.player);
|
| 1010 |
-
if (enemyColor === StoneType.BLACK) {
|
| 1011 |
-
counts.black += step.capturedPositions.length;
|
| 1012 |
-
} else if (enemyColor === StoneType.WHITE) {
|
| 1013 |
-
counts.white += step.capturedPositions.length;
|
| 1014 |
-
}
|
| 1015 |
-
}
|
| 1016 |
-
}
|
| 1017 |
-
return counts;
|
| 1018 |
-
}
|
| 1019 |
-
/**
|
| 1020 |
-
* Serialize game state to JSON
|
| 1021 |
-
* Equivalent to Game.serialize() (lines 250-252)
|
| 1022 |
-
*/
|
| 1023 |
-
toJSON() {
|
| 1024 |
-
return {
|
| 1025 |
-
shape: this.shape,
|
| 1026 |
-
currentPlayer: this.currentPlayer,
|
| 1027 |
-
currentStepIndex: this.currentStepIndex,
|
| 1028 |
-
history: this.stepHistory,
|
| 1029 |
-
board: this.board,
|
| 1030 |
-
gameStatus: this.gameStatus,
|
| 1031 |
-
gameResult: this.gameResult,
|
| 1032 |
-
passCount: this.passCount
|
| 1033 |
-
};
|
| 1034 |
-
}
|
| 1035 |
-
/**
|
| 1036 |
-
* Load game state from JSON
|
| 1037 |
-
*/
|
| 1038 |
-
fromJSON(data) {
|
| 1039 |
-
try {
|
| 1040 |
-
if (!data || typeof data !== "object") {
|
| 1041 |
-
return false;
|
| 1042 |
-
}
|
| 1043 |
-
if (!data.shape || !data.board || !Array.isArray(data.history)) {
|
| 1044 |
-
return false;
|
| 1045 |
-
}
|
| 1046 |
-
this.shape = data.shape;
|
| 1047 |
-
this.currentPlayer = data.currentPlayer;
|
| 1048 |
-
this.currentStepIndex = data.currentStepIndex;
|
| 1049 |
-
this.stepHistory = data.history || [];
|
| 1050 |
-
this.board = data.board;
|
| 1051 |
-
this.gameStatus = data.gameStatus || "idle";
|
| 1052 |
-
this.gameResult = data.gameResult;
|
| 1053 |
-
this.passCount = data.passCount || 0;
|
| 1054 |
-
if (this.currentStepIndex > 0) {
|
| 1055 |
-
const lastStep = this.stepHistory[this.currentStepIndex - 1];
|
| 1056 |
-
this.lastCapturedPositions = lastStep.capturedPositions || null;
|
| 1057 |
-
} else {
|
| 1058 |
-
this.lastCapturedPositions = null;
|
| 1059 |
-
}
|
| 1060 |
-
this.invalidateAnalysisCache();
|
| 1061 |
-
return true;
|
| 1062 |
-
} catch (error) {
|
| 1063 |
-
console.error("Failed to load game state:", error);
|
| 1064 |
-
return false;
|
| 1065 |
-
}
|
| 1066 |
-
}
|
| 1067 |
-
/**
|
| 1068 |
-
* Get game statistics
|
| 1069 |
-
*/
|
| 1070 |
-
getStats() {
|
| 1071 |
-
const captured = this.getCapturedCounts();
|
| 1072 |
-
const territory = this.getTerritory();
|
| 1073 |
-
let blackMoves = 0;
|
| 1074 |
-
let whiteMoves = 0;
|
| 1075 |
-
for (const step of this.stepHistory.slice(0, this.currentStepIndex)) {
|
| 1076 |
-
if (step.type === 0 /* DROP */) {
|
| 1077 |
-
if (step.player === StoneType.BLACK) {
|
| 1078 |
-
blackMoves++;
|
| 1079 |
-
} else if (step.player === StoneType.WHITE) {
|
| 1080 |
-
whiteMoves++;
|
| 1081 |
-
}
|
| 1082 |
-
}
|
| 1083 |
-
}
|
| 1084 |
-
return {
|
| 1085 |
-
totalMoves: this.currentStepIndex,
|
| 1086 |
-
blackMoves,
|
| 1087 |
-
whiteMoves,
|
| 1088 |
-
capturedByBlack: captured.white,
|
| 1089 |
-
// Black captures white stones
|
| 1090 |
-
capturedByWhite: captured.black,
|
| 1091 |
-
// White captures black stones
|
| 1092 |
-
territory
|
| 1093 |
-
};
|
| 1094 |
-
}
|
| 1095 |
-
/**
|
| 1096 |
-
* Save game state to sessionStorage
|
| 1097 |
-
*
|
| 1098 |
-
* @param key Storage key (default: "trigoGameState")
|
| 1099 |
-
* @returns true if save was successful
|
| 1100 |
-
*/
|
| 1101 |
-
saveToSessionStorage(key = "trigoGameState") {
|
| 1102 |
-
if (typeof globalThis !== "undefined" && globalThis.sessionStorage) {
|
| 1103 |
-
try {
|
| 1104 |
-
const gameState = this.toJSON();
|
| 1105 |
-
globalThis.sessionStorage.setItem(key, JSON.stringify(gameState));
|
| 1106 |
-
return true;
|
| 1107 |
-
} catch (error) {
|
| 1108 |
-
console.error("Failed to save game state to sessionStorage:", error);
|
| 1109 |
-
return false;
|
| 1110 |
-
}
|
| 1111 |
-
}
|
| 1112 |
-
console.warn("sessionStorage is not available");
|
| 1113 |
-
return false;
|
| 1114 |
-
}
|
| 1115 |
-
/**
|
| 1116 |
-
* Load game state from sessionStorage
|
| 1117 |
-
*
|
| 1118 |
-
* @param key Storage key (default: "trigoGameState")
|
| 1119 |
-
* @returns true if load was successful
|
| 1120 |
-
*/
|
| 1121 |
-
loadFromSessionStorage(key = "trigoGameState") {
|
| 1122 |
-
if (typeof globalThis !== "undefined" && globalThis.sessionStorage) {
|
| 1123 |
-
try {
|
| 1124 |
-
const savedState = globalThis.sessionStorage.getItem(key);
|
| 1125 |
-
if (!savedState) {
|
| 1126 |
-
console.log("No saved game state found");
|
| 1127 |
-
return false;
|
| 1128 |
-
}
|
| 1129 |
-
const data = JSON.parse(savedState);
|
| 1130 |
-
return this.fromJSON(data);
|
| 1131 |
-
} catch (error) {
|
| 1132 |
-
console.error("Failed to load game state from sessionStorage:", error);
|
| 1133 |
-
return false;
|
| 1134 |
-
}
|
| 1135 |
-
}
|
| 1136 |
-
console.warn("sessionStorage is not available");
|
| 1137 |
-
return false;
|
| 1138 |
-
}
|
| 1139 |
-
/**
|
| 1140 |
-
* Clear saved game state from sessionStorage
|
| 1141 |
-
*
|
| 1142 |
-
* @param key Storage key (default: "trigoGameState")
|
| 1143 |
-
*/
|
| 1144 |
-
clearSessionStorage(key = "trigoGameState") {
|
| 1145 |
-
if (typeof globalThis !== "undefined" && globalThis.sessionStorage) {
|
| 1146 |
-
try {
|
| 1147 |
-
globalThis.sessionStorage.removeItem(key);
|
| 1148 |
-
} catch (error) {
|
| 1149 |
-
console.error("Failed to clear sessionStorage:", error);
|
| 1150 |
-
}
|
| 1151 |
-
} else {
|
| 1152 |
-
console.warn("sessionStorage is not available");
|
| 1153 |
-
}
|
| 1154 |
-
}
|
| 1155 |
-
/**
|
| 1156 |
-
* Export game to TGN (Trigo Game Notation) format
|
| 1157 |
-
*
|
| 1158 |
-
* TGN format is similar to PGN (Portable Game Notation) for chess.
|
| 1159 |
-
* It includes metadata tags and move sequence using ab0yz coordinate notation.
|
| 1160 |
-
*
|
| 1161 |
-
* @param metadata Optional metadata for the game (Event, Site, Date, Players, etc.)
|
| 1162 |
-
* @returns TGN-formatted string
|
| 1163 |
-
*
|
| 1164 |
-
* @example
|
| 1165 |
-
* const tgn = game.toTGN({
|
| 1166 |
-
* event: "World Championship",
|
| 1167 |
-
* site: "Tokyo",
|
| 1168 |
-
* date: "2025.10.31",
|
| 1169 |
-
* black: "Alice",
|
| 1170 |
-
* white: "Bob"
|
| 1171 |
-
* });
|
| 1172 |
-
*/
|
| 1173 |
-
toTGN(metadata, { markResult } = {}) {
|
| 1174 |
-
const lines = [];
|
| 1175 |
-
if (metadata) {
|
| 1176 |
-
if (metadata.event) lines.push(`[Event "${metadata.event}"]`);
|
| 1177 |
-
if (metadata.site) lines.push(`[Site "${metadata.site}"]`);
|
| 1178 |
-
if (metadata.date) lines.push(`[Date "${metadata.date}"]`);
|
| 1179 |
-
if (metadata.round) lines.push(`[Round "${metadata.round}"]`);
|
| 1180 |
-
if (metadata.black) lines.push(`[Black "${metadata.black}"]`);
|
| 1181 |
-
if (metadata.white) lines.push(`[White "${metadata.white}"]`);
|
| 1182 |
-
}
|
| 1183 |
-
if (this.gameStatus === "finished" && this.gameResult) {
|
| 1184 |
-
let resultStr = "";
|
| 1185 |
-
if (this.gameResult.winner === "black") {
|
| 1186 |
-
resultStr = "B+";
|
| 1187 |
-
} else if (this.gameResult.winner === "white") {
|
| 1188 |
-
resultStr = "W+";
|
| 1189 |
-
} else {
|
| 1190 |
-
resultStr = "=";
|
| 1191 |
-
}
|
| 1192 |
-
if (this.gameResult.score) {
|
| 1193 |
-
const { black, white } = this.gameResult.score;
|
| 1194 |
-
const diff = Math.abs(black - white);
|
| 1195 |
-
resultStr += `${diff}points`;
|
| 1196 |
-
} else if (this.gameResult.reason === "resignation") {
|
| 1197 |
-
resultStr += "Resign";
|
| 1198 |
-
}
|
| 1199 |
-
}
|
| 1200 |
-
const boardStr = this.shape.z === 1 ? `${this.shape.x}x${this.shape.y}` : `${this.shape.x}x${this.shape.y}x${this.shape.z}`;
|
| 1201 |
-
lines.push(`[Board ${boardStr}]`);
|
| 1202 |
-
if (metadata) {
|
| 1203 |
-
if (metadata.rules) lines.push(`[Rules "${metadata.rules}"]`);
|
| 1204 |
-
if (metadata.timeControl) lines.push(`[TimeControl "${metadata.timeControl}"]`);
|
| 1205 |
-
if (metadata.application) lines.push(`[Application "${metadata.application}"]`);
|
| 1206 |
-
}
|
| 1207 |
-
lines.push("");
|
| 1208 |
-
const moves = [];
|
| 1209 |
-
let moveNumber = 1;
|
| 1210 |
-
for (let i = 0; i < this.stepHistory.length; i++) {
|
| 1211 |
-
const step = this.stepHistory[i];
|
| 1212 |
-
let moveStr = "";
|
| 1213 |
-
if (step.player === StoneType.BLACK) {
|
| 1214 |
-
moveStr = `${moveNumber}. `;
|
| 1215 |
-
}
|
| 1216 |
-
if (step.type === 0 /* DROP */ && step.position) {
|
| 1217 |
-
const pos = [step.position.x, step.position.y, step.position.z];
|
| 1218 |
-
const boardShape = [this.shape.x, this.shape.y, this.shape.z];
|
| 1219 |
-
const coord = encodeAb0yz(pos, boardShape);
|
| 1220 |
-
moveStr += coord;
|
| 1221 |
-
} else if (step.type === 1 /* PASS */) {
|
| 1222 |
-
moveStr += "Pass";
|
| 1223 |
-
} else if (step.type === 2 /* SURRENDER */) {
|
| 1224 |
-
moveStr += "Resign";
|
| 1225 |
-
}
|
| 1226 |
-
moves.push(moveStr);
|
| 1227 |
-
if (step.player === StoneType.WHITE) {
|
| 1228 |
-
moveNumber++;
|
| 1229 |
-
}
|
| 1230 |
-
}
|
| 1231 |
-
let currentLine = "";
|
| 1232 |
-
for (let i = 0; i < moves.length; i++) {
|
| 1233 |
-
const move = moves[i];
|
| 1234 |
-
if (move.match(/^\d+\./)) {
|
| 1235 |
-
if (currentLine) {
|
| 1236 |
-
lines.push(currentLine);
|
| 1237 |
-
}
|
| 1238 |
-
currentLine = move;
|
| 1239 |
-
} else {
|
| 1240 |
-
currentLine += " " + move;
|
| 1241 |
-
}
|
| 1242 |
-
}
|
| 1243 |
-
if (currentLine) {
|
| 1244 |
-
lines.push(currentLine);
|
| 1245 |
-
}
|
| 1246 |
-
if (markResult) {
|
| 1247 |
-
const territory = this.getTerritory();
|
| 1248 |
-
const scoreDiff = territory.black - territory.white;
|
| 1249 |
-
lines.push(`; ${scoreDiff > 0 ? "-" : scoreDiff < 0 ? "+" : ""}${Math.abs(scoreDiff)}`);
|
| 1250 |
-
}
|
| 1251 |
-
lines.push("");
|
| 1252 |
-
return lines.join("\n");
|
| 1253 |
-
}
|
| 1254 |
-
/**
|
| 1255 |
-
* Import game from TGN (Trigo Game Notation) format
|
| 1256 |
-
*
|
| 1257 |
-
* Static factory method that parses a TGN string and creates a new TrigoGame instance
|
| 1258 |
-
* with the board configuration and moves from the TGN file.
|
| 1259 |
-
*
|
| 1260 |
-
* Synchronous operation - requires parser to be loaded via setParserModule()
|
| 1261 |
-
*
|
| 1262 |
-
* @param tgnString TGN-formatted game notation string
|
| 1263 |
-
* @param callbacks Optional game callbacks
|
| 1264 |
-
* @returns New TrigoGame instance with the imported game state
|
| 1265 |
-
* @throws TGNParseError if the TGN string is invalid
|
| 1266 |
-
*
|
| 1267 |
-
* @example
|
| 1268 |
-
* const tgnString = `
|
| 1269 |
-
* [Event "World Championship"]
|
| 1270 |
-
* [Board "5x5x5"]
|
| 1271 |
-
* [Black "Alice"]
|
| 1272 |
-
* [White "Bob"]
|
| 1273 |
-
*
|
| 1274 |
-
* 1. 000 y00
|
| 1275 |
-
* 2. 0y0 pass
|
| 1276 |
-
* `;
|
| 1277 |
-
* const game = TrigoGame.fromTGN(tgnString);
|
| 1278 |
-
*/
|
| 1279 |
-
static fromTGN(tgnString, callbacks) {
|
| 1280 |
-
const parsed = parseTGN(tgnString);
|
| 1281 |
-
let boardShape;
|
| 1282 |
-
if (parsed.tags.Board && Array.isArray(parsed.tags.Board)) {
|
| 1283 |
-
const shape = parsed.tags.Board;
|
| 1284 |
-
boardShape = {
|
| 1285 |
-
x: shape[0] || 5,
|
| 1286 |
-
y: shape[1] || 5,
|
| 1287 |
-
z: shape[2] || 1
|
| 1288 |
-
};
|
| 1289 |
-
} else {
|
| 1290 |
-
boardShape = { x: 5, y: 5, z: 5 };
|
| 1291 |
-
}
|
| 1292 |
-
const game = new this(boardShape, callbacks);
|
| 1293 |
-
game.startGame();
|
| 1294 |
-
if (parsed.moves && parsed.moves.length > 0) {
|
| 1295 |
-
for (const round of parsed.moves) {
|
| 1296 |
-
if (round.action_black) {
|
| 1297 |
-
game._applyParsedMove(round.action_black, boardShape);
|
| 1298 |
-
}
|
| 1299 |
-
if (round.action_white) {
|
| 1300 |
-
game._applyParsedMove(round.action_white, boardShape);
|
| 1301 |
-
}
|
| 1302 |
-
}
|
| 1303 |
-
}
|
| 1304 |
-
return game;
|
| 1305 |
-
}
|
| 1306 |
-
/**
|
| 1307 |
-
* Apply a parsed move action to the game
|
| 1308 |
-
* Private helper method for fromTGN
|
| 1309 |
-
*
|
| 1310 |
-
* @param action Parsed move action from TGN parser
|
| 1311 |
-
* @param boardShape Board dimensions for coordinate decoding
|
| 1312 |
-
*/
|
| 1313 |
-
_applyParsedMove(action, boardShape) {
|
| 1314 |
-
if (action.type === "pass") {
|
| 1315 |
-
this.pass();
|
| 1316 |
-
} else if (action.type === "resign") {
|
| 1317 |
-
this.surrender();
|
| 1318 |
-
} else if (action.type === "move" && action.position) {
|
| 1319 |
-
const coords = decodeAb0yz(action.position, [boardShape.x, boardShape.y, boardShape.z]);
|
| 1320 |
-
const position = {
|
| 1321 |
-
x: coords[0],
|
| 1322 |
-
y: coords[1],
|
| 1323 |
-
z: coords[2]
|
| 1324 |
-
};
|
| 1325 |
-
this.drop(position);
|
| 1326 |
-
}
|
| 1327 |
-
}
|
| 1328 |
-
};
|
| 1329 |
-
|
| 1330 |
-
// backend/src/services/gameManager.ts
|
| 1331 |
-
var GameManager = class {
|
| 1332 |
-
// Default 5x5x5 board
|
| 1333 |
-
constructor() {
|
| 1334 |
-
this.rooms = /* @__PURE__ */ new Map();
|
| 1335 |
-
this.playerRoomMap = /* @__PURE__ */ new Map();
|
| 1336 |
-
this.defaultBoardShape = { x: 5, y: 5, z: 5 };
|
| 1337 |
-
console.log("GameManager initialized");
|
| 1338 |
-
}
|
| 1339 |
-
createRoom(playerId, nickname, boardShape, preferredColor) {
|
| 1340 |
-
const roomId = this.generateRoomId();
|
| 1341 |
-
const shape = boardShape || this.defaultBoardShape;
|
| 1342 |
-
const playerColor = preferredColor || "black";
|
| 1343 |
-
const room = {
|
| 1344 |
-
id: roomId,
|
| 1345 |
-
adminId: playerId,
|
| 1346 |
-
// Room creator is admin
|
| 1347 |
-
players: {
|
| 1348 |
-
[playerId]: {
|
| 1349 |
-
id: playerId,
|
| 1350 |
-
nickname,
|
| 1351 |
-
color: playerColor,
|
| 1352 |
-
connected: true
|
| 1353 |
-
}
|
| 1354 |
-
},
|
| 1355 |
-
game: new TrigoGame(shape, {
|
| 1356 |
-
onStepAdvance: (_step, history) => {
|
| 1357 |
-
console.log(`Step ${history.length}: Player made move`);
|
| 1358 |
-
},
|
| 1359 |
-
onCapture: (captured) => {
|
| 1360 |
-
console.log(`Captured ${captured.length} stones`);
|
| 1361 |
-
},
|
| 1362 |
-
onWin: (winner) => {
|
| 1363 |
-
console.log(`Game won by ${winner}`);
|
| 1364 |
-
}
|
| 1365 |
-
}),
|
| 1366 |
-
gameState: {
|
| 1367 |
-
gameStatus: "waiting",
|
| 1368 |
-
winner: null
|
| 1369 |
-
},
|
| 1370 |
-
createdAt: /* @__PURE__ */ new Date(),
|
| 1371 |
-
startedAt: null
|
| 1372 |
-
};
|
| 1373 |
-
this.rooms.set(roomId, room);
|
| 1374 |
-
this.playerRoomMap.set(playerId, roomId);
|
| 1375 |
-
console.log(`Room ${roomId} created by ${playerId}`);
|
| 1376 |
-
return room;
|
| 1377 |
-
}
|
| 1378 |
-
joinRoom(roomId, playerId, nickname, preferredColor) {
|
| 1379 |
-
const room = this.rooms.get(roomId);
|
| 1380 |
-
if (!room) {
|
| 1381 |
-
return null;
|
| 1382 |
-
}
|
| 1383 |
-
const playerCount = Object.keys(room.players).length;
|
| 1384 |
-
if (playerCount >= 2) {
|
| 1385 |
-
return null;
|
| 1386 |
-
}
|
| 1387 |
-
const firstPlayer = Object.values(room.players)[0];
|
| 1388 |
-
let assignedColor;
|
| 1389 |
-
if (preferredColor && preferredColor !== firstPlayer.color) {
|
| 1390 |
-
assignedColor = preferredColor;
|
| 1391 |
-
} else {
|
| 1392 |
-
assignedColor = firstPlayer.color === "black" ? "white" : "black";
|
| 1393 |
-
}
|
| 1394 |
-
room.players[playerId] = {
|
| 1395 |
-
id: playerId,
|
| 1396 |
-
nickname,
|
| 1397 |
-
color: assignedColor,
|
| 1398 |
-
connected: true
|
| 1399 |
-
};
|
| 1400 |
-
this.playerRoomMap.set(playerId, roomId);
|
| 1401 |
-
if (playerCount === 1) {
|
| 1402 |
-
room.gameState.gameStatus = "playing";
|
| 1403 |
-
room.startedAt = /* @__PURE__ */ new Date();
|
| 1404 |
-
}
|
| 1405 |
-
return room;
|
| 1406 |
-
}
|
| 1407 |
-
leaveRoom(roomId, playerId) {
|
| 1408 |
-
const room = this.rooms.get(roomId);
|
| 1409 |
-
if (!room) return;
|
| 1410 |
-
if (room.players[playerId]) {
|
| 1411 |
-
room.players[playerId].connected = false;
|
| 1412 |
-
}
|
| 1413 |
-
this.playerRoomMap.delete(playerId);
|
| 1414 |
-
const connectedPlayers = Object.values(room.players).filter((p) => p.connected);
|
| 1415 |
-
if (connectedPlayers.length === 0) {
|
| 1416 |
-
this.rooms.delete(roomId);
|
| 1417 |
-
console.log(`Room ${roomId} deleted - no players remaining`);
|
| 1418 |
-
}
|
| 1419 |
-
}
|
| 1420 |
-
makeMove(roomId, playerId, move) {
|
| 1421 |
-
const room = this.rooms.get(roomId);
|
| 1422 |
-
if (!room) return false;
|
| 1423 |
-
const player = room.players[playerId];
|
| 1424 |
-
if (!player) return false;
|
| 1425 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1426 |
-
return false;
|
| 1427 |
-
}
|
| 1428 |
-
const expectedPlayer = player.color === "black" ? StoneType.BLACK : StoneType.WHITE;
|
| 1429 |
-
const currentPlayer = room.game.getCurrentPlayer();
|
| 1430 |
-
if (currentPlayer !== expectedPlayer) {
|
| 1431 |
-
return false;
|
| 1432 |
-
}
|
| 1433 |
-
const position = { x: move.x, y: move.y, z: move.z };
|
| 1434 |
-
const success = room.game.drop(position);
|
| 1435 |
-
return success;
|
| 1436 |
-
}
|
| 1437 |
-
passTurn(roomId, playerId) {
|
| 1438 |
-
const room = this.rooms.get(roomId);
|
| 1439 |
-
if (!room) return false;
|
| 1440 |
-
const player = room.players[playerId];
|
| 1441 |
-
if (!player) return false;
|
| 1442 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1443 |
-
return false;
|
| 1444 |
-
}
|
| 1445 |
-
const expectedPlayer = player.color === "black" ? StoneType.BLACK : StoneType.WHITE;
|
| 1446 |
-
const currentPlayer = room.game.getCurrentPlayer();
|
| 1447 |
-
if (currentPlayer !== expectedPlayer) {
|
| 1448 |
-
return false;
|
| 1449 |
-
}
|
| 1450 |
-
return room.game.pass();
|
| 1451 |
-
}
|
| 1452 |
-
resign(roomId, playerId) {
|
| 1453 |
-
const room = this.rooms.get(roomId);
|
| 1454 |
-
if (!room) return false;
|
| 1455 |
-
const player = room.players[playerId];
|
| 1456 |
-
if (!player) return false;
|
| 1457 |
-
room.game.surrender();
|
| 1458 |
-
room.gameState.gameStatus = "finished";
|
| 1459 |
-
room.gameState.winner = player.color === "black" ? "white" : "black";
|
| 1460 |
-
return true;
|
| 1461 |
-
}
|
| 1462 |
-
/**
|
| 1463 |
-
* Undo the last move (悔棋)
|
| 1464 |
-
*/
|
| 1465 |
-
undoMove(roomId, playerId) {
|
| 1466 |
-
const room = this.rooms.get(roomId);
|
| 1467 |
-
if (!room) return false;
|
| 1468 |
-
const player = room.players[playerId];
|
| 1469 |
-
if (!player) return false;
|
| 1470 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1471 |
-
return false;
|
| 1472 |
-
}
|
| 1473 |
-
return room.game.undo();
|
| 1474 |
-
}
|
| 1475 |
-
/**
|
| 1476 |
-
* Redo the last undone move (forward in history)
|
| 1477 |
-
*/
|
| 1478 |
-
redoMove(roomId, playerId) {
|
| 1479 |
-
const room = this.rooms.get(roomId);
|
| 1480 |
-
if (!room) return false;
|
| 1481 |
-
const player = room.players[playerId];
|
| 1482 |
-
if (!player) return false;
|
| 1483 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1484 |
-
return false;
|
| 1485 |
-
}
|
| 1486 |
-
return room.game.redo();
|
| 1487 |
-
}
|
| 1488 |
-
/**
|
| 1489 |
-
* Reset the game to initial state (new game in same room)
|
| 1490 |
-
* Only admin can reset the game
|
| 1491 |
-
*/
|
| 1492 |
-
resetGame(roomId, adminId, options) {
|
| 1493 |
-
const room = this.rooms.get(roomId);
|
| 1494 |
-
if (!room) return { success: false, error: "Room not found" };
|
| 1495 |
-
if (room.adminId !== adminId) {
|
| 1496 |
-
return { success: false, error: "Only room admin can reset the game" };
|
| 1497 |
-
}
|
| 1498 |
-
const boardShape = options?.boardShape;
|
| 1499 |
-
const playerColors = options?.playerColors;
|
| 1500 |
-
if (playerColors) {
|
| 1501 |
-
const playerIds = Object.keys(room.players);
|
| 1502 |
-
for (const playerId of playerIds) {
|
| 1503 |
-
if (playerColors[playerId]) {
|
| 1504 |
-
room.players[playerId].color = playerColors[playerId];
|
| 1505 |
-
}
|
| 1506 |
-
}
|
| 1507 |
-
console.log(`Player colors assigned:`, playerColors);
|
| 1508 |
-
}
|
| 1509 |
-
if (boardShape) {
|
| 1510 |
-
const currentShape = room.game.getShape();
|
| 1511 |
-
if (boardShape.x !== currentShape.x || boardShape.y !== currentShape.y || boardShape.z !== currentShape.z) {
|
| 1512 |
-
room.game = new TrigoGame(boardShape, {
|
| 1513 |
-
onStepAdvance: (_step, history) => {
|
| 1514 |
-
console.log(`Step ${history.length}: Player made move`);
|
| 1515 |
-
},
|
| 1516 |
-
onCapture: (captured) => {
|
| 1517 |
-
console.log(`Captured ${captured.length} stones`);
|
| 1518 |
-
},
|
| 1519 |
-
onWin: (winner) => {
|
| 1520 |
-
console.log(`Game won by ${winner}`);
|
| 1521 |
-
}
|
| 1522 |
-
});
|
| 1523 |
-
console.log(`Game ${roomId} reset with new board shape: ${boardShape.x}x${boardShape.y}x${boardShape.z}`);
|
| 1524 |
-
} else {
|
| 1525 |
-
room.game.reset();
|
| 1526 |
-
console.log(`Game ${roomId} reset to initial state`);
|
| 1527 |
-
}
|
| 1528 |
-
} else {
|
| 1529 |
-
room.game.reset();
|
| 1530 |
-
console.log(`Game ${roomId} reset to initial state`);
|
| 1531 |
-
}
|
| 1532 |
-
room.gameState.gameStatus = "playing";
|
| 1533 |
-
room.gameState.winner = null;
|
| 1534 |
-
room.startedAt = /* @__PURE__ */ new Date();
|
| 1535 |
-
return { success: true };
|
| 1536 |
-
}
|
| 1537 |
-
/**
|
| 1538 |
-
* Get game board state for a room
|
| 1539 |
-
*/
|
| 1540 |
-
getGameBoard(roomId) {
|
| 1541 |
-
const room = this.rooms.get(roomId);
|
| 1542 |
-
if (!room) return null;
|
| 1543 |
-
return room.game.getBoard();
|
| 1544 |
-
}
|
| 1545 |
-
/**
|
| 1546 |
-
* Get game statistics for a room
|
| 1547 |
-
*/
|
| 1548 |
-
getGameStats(roomId) {
|
| 1549 |
-
const room = this.rooms.get(roomId);
|
| 1550 |
-
if (!room) return null;
|
| 1551 |
-
return room.game.getStats();
|
| 1552 |
-
}
|
| 1553 |
-
/**
|
| 1554 |
-
* Get current player for a room
|
| 1555 |
-
*/
|
| 1556 |
-
getCurrentPlayer(roomId) {
|
| 1557 |
-
const room = this.rooms.get(roomId);
|
| 1558 |
-
if (!room) return null;
|
| 1559 |
-
const currentStone = room.game.getCurrentPlayer();
|
| 1560 |
-
return currentStone === StoneType.BLACK ? "black" : "white";
|
| 1561 |
-
}
|
| 1562 |
-
/**
|
| 1563 |
-
* Calculate and get territory for a room
|
| 1564 |
-
*/
|
| 1565 |
-
getTerritory(roomId) {
|
| 1566 |
-
const room = this.rooms.get(roomId);
|
| 1567 |
-
if (!room) return null;
|
| 1568 |
-
return room.game.getTerritory();
|
| 1569 |
-
}
|
| 1570 |
-
/**
|
| 1571 |
-
* End the game and determine winner based on territory
|
| 1572 |
-
*/
|
| 1573 |
-
endGameByTerritory(roomId) {
|
| 1574 |
-
const room = this.rooms.get(roomId);
|
| 1575 |
-
if (!room) return false;
|
| 1576 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1577 |
-
return false;
|
| 1578 |
-
}
|
| 1579 |
-
const territory = room.game.getTerritory();
|
| 1580 |
-
if (territory.black > territory.white) {
|
| 1581 |
-
room.gameState.winner = "black";
|
| 1582 |
-
} else if (territory.white > territory.black) {
|
| 1583 |
-
room.gameState.winner = "white";
|
| 1584 |
-
} else {
|
| 1585 |
-
room.gameState.winner = null;
|
| 1586 |
-
}
|
| 1587 |
-
room.gameState.gameStatus = "finished";
|
| 1588 |
-
console.log(
|
| 1589 |
-
`Game ${roomId} ended. Black: ${territory.black}, White: ${territory.white}, Winner: ${room.gameState.winner}`
|
| 1590 |
-
);
|
| 1591 |
-
return true;
|
| 1592 |
-
}
|
| 1593 |
-
/**
|
| 1594 |
-
* Check if both players passed consecutively (game should end)
|
| 1595 |
-
* Returns true if game was ended
|
| 1596 |
-
*/
|
| 1597 |
-
checkConsecutivePasses(roomId) {
|
| 1598 |
-
const room = this.rooms.get(roomId);
|
| 1599 |
-
if (!room) return false;
|
| 1600 |
-
const history = room.game.getHistory();
|
| 1601 |
-
if (history.length < 2) return false;
|
| 1602 |
-
const lastMove = history[history.length - 1];
|
| 1603 |
-
const secondLastMove = history[history.length - 2];
|
| 1604 |
-
if (lastMove.type === 1 /* PASS */ && secondLastMove.type === 1 /* PASS */) {
|
| 1605 |
-
this.endGameByTerritory(roomId);
|
| 1606 |
-
return true;
|
| 1607 |
-
}
|
| 1608 |
-
return false;
|
| 1609 |
-
}
|
| 1610 |
-
getRoom(roomId) {
|
| 1611 |
-
return this.rooms.get(roomId);
|
| 1612 |
-
}
|
| 1613 |
-
getPlayerRoom(playerId) {
|
| 1614 |
-
const roomId = this.playerRoomMap.get(playerId);
|
| 1615 |
-
if (!roomId) return void 0;
|
| 1616 |
-
return this.rooms.get(roomId);
|
| 1617 |
-
}
|
| 1618 |
-
getActiveRooms() {
|
| 1619 |
-
return Array.from(this.rooms.values()).filter(
|
| 1620 |
-
(room) => room.gameState.gameStatus !== "finished"
|
| 1621 |
-
);
|
| 1622 |
-
}
|
| 1623 |
-
generateRoomId() {
|
| 1624 |
-
return uuidv4().substring(0, 8).toUpperCase();
|
| 1625 |
-
}
|
| 1626 |
-
};
|
| 1627 |
-
|
| 1628 |
-
// backend/src/sockets/gameSocket.ts
|
| 1629 |
-
function getRoomSummary(room) {
|
| 1630 |
-
const connectedPlayers = Object.values(room.players).filter((p) => p.connected);
|
| 1631 |
-
return {
|
| 1632 |
-
id: room.id,
|
| 1633 |
-
playerCount: connectedPlayers.length,
|
| 1634 |
-
maxPlayers: 2,
|
| 1635 |
-
status: room.gameState.gameStatus,
|
| 1636 |
-
isFull: connectedPlayers.length >= 2,
|
| 1637 |
-
createdAt: room.createdAt.toISOString()
|
| 1638 |
-
};
|
| 1639 |
-
}
|
| 1640 |
-
function setupSocketHandlers(io2, socket, gameManager2) {
|
| 1641 |
-
console.log(`Setting up socket handlers for ${socket.id}`);
|
| 1642 |
-
socket.on("listRooms", (callback) => {
|
| 1643 |
-
const rooms = gameManager2.getActiveRooms();
|
| 1644 |
-
const roomList = rooms.map((room) => getRoomSummary(room));
|
| 1645 |
-
if (callback) {
|
| 1646 |
-
callback({ success: true, rooms: roomList });
|
| 1647 |
-
}
|
| 1648 |
-
});
|
| 1649 |
-
socket.on(
|
| 1650 |
-
"joinRoom",
|
| 1651 |
-
(data, callback) => {
|
| 1652 |
-
console.log("[gameSocket] joinRoom event received:", {
|
| 1653 |
-
roomId: data.roomId,
|
| 1654 |
-
nickname: data.nickname,
|
| 1655 |
-
preferredColor: data.preferredColor,
|
| 1656 |
-
hasCallback: !!callback,
|
| 1657 |
-
socketId: socket.id
|
| 1658 |
-
});
|
| 1659 |
-
const { roomId, nickname, preferredColor } = data;
|
| 1660 |
-
try {
|
| 1661 |
-
let room;
|
| 1662 |
-
if (roomId) {
|
| 1663 |
-
const existingRoom = gameManager2.getRoom(roomId);
|
| 1664 |
-
if (!existingRoom) {
|
| 1665 |
-
if (callback) {
|
| 1666 |
-
callback({
|
| 1667 |
-
success: false,
|
| 1668 |
-
error: "Room not found",
|
| 1669 |
-
errorCode: "ROOM_NOT_FOUND"
|
| 1670 |
-
});
|
| 1671 |
-
}
|
| 1672 |
-
return;
|
| 1673 |
-
}
|
| 1674 |
-
const playerCount = Object.keys(existingRoom.players).length;
|
| 1675 |
-
if (playerCount >= 2) {
|
| 1676 |
-
if (callback) {
|
| 1677 |
-
callback({
|
| 1678 |
-
success: false,
|
| 1679 |
-
error: "Room is full",
|
| 1680 |
-
errorCode: "ROOM_FULL"
|
| 1681 |
-
});
|
| 1682 |
-
}
|
| 1683 |
-
return;
|
| 1684 |
-
}
|
| 1685 |
-
room = gameManager2.joinRoom(roomId, socket.id, nickname, preferredColor);
|
| 1686 |
-
} else {
|
| 1687 |
-
room = gameManager2.createRoom(socket.id, nickname, void 0, preferredColor);
|
| 1688 |
-
}
|
| 1689 |
-
if (room) {
|
| 1690 |
-
socket.join(room.id);
|
| 1691 |
-
const roomSockets = io2.sockets.adapter.rooms.get(room.id);
|
| 1692 |
-
console.log(`[gameSocket] Socket ${socket.id} joined room ${room.id}`);
|
| 1693 |
-
console.log(`[gameSocket] Room ${room.id} now has sockets:`, roomSockets ? Array.from(roomSockets) : []);
|
| 1694 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1695 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1696 |
-
const tgn = room.game.toTGN();
|
| 1697 |
-
const players = {};
|
| 1698 |
-
for (const [pid, player] of Object.entries(room.players)) {
|
| 1699 |
-
players[pid] = {
|
| 1700 |
-
nickname: player.nickname,
|
| 1701 |
-
color: player.color
|
| 1702 |
-
};
|
| 1703 |
-
}
|
| 1704 |
-
const response = {
|
| 1705 |
-
success: true,
|
| 1706 |
-
roomId: room.id,
|
| 1707 |
-
playerId: socket.id,
|
| 1708 |
-
playerColor: room.players[socket.id]?.color,
|
| 1709 |
-
isAdmin: room.adminId === socket.id,
|
| 1710 |
-
adminId: room.adminId,
|
| 1711 |
-
players,
|
| 1712 |
-
// Include all players in room
|
| 1713 |
-
gameState: {
|
| 1714 |
-
boardShape: room.game.getShape(),
|
| 1715 |
-
currentPlayer,
|
| 1716 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1717 |
-
capturedStones: {
|
| 1718 |
-
black: stats?.capturedByBlack || 0,
|
| 1719 |
-
white: stats?.capturedByWhite || 0
|
| 1720 |
-
},
|
| 1721 |
-
gameStatus: room.gameState.gameStatus,
|
| 1722 |
-
winner: room.gameState.winner,
|
| 1723 |
-
tgn
|
| 1724 |
-
}
|
| 1725 |
-
};
|
| 1726 |
-
if (callback) {
|
| 1727 |
-
console.log("[gameSocket] Sending response via callback:", {
|
| 1728 |
-
roomId: response.roomId,
|
| 1729 |
-
playerColor: response.playerColor
|
| 1730 |
-
});
|
| 1731 |
-
callback(response);
|
| 1732 |
-
} else {
|
| 1733 |
-
console.log("[gameSocket] No callback, using roomJoined emit");
|
| 1734 |
-
socket.emit("roomJoined", response);
|
| 1735 |
-
}
|
| 1736 |
-
console.log(`[gameSocket] Broadcasting playerJoined to room ${room.id} (excluding ${socket.id})`);
|
| 1737 |
-
socket.to(room.id).emit("playerJoined", {
|
| 1738 |
-
playerId: socket.id,
|
| 1739 |
-
nickname
|
| 1740 |
-
});
|
| 1741 |
-
console.log(`[gameSocket] playerJoined broadcast sent`);
|
| 1742 |
-
const roomSummary = getRoomSummary(room);
|
| 1743 |
-
if (roomId) {
|
| 1744 |
-
io2.emit("roomUpdated", roomSummary);
|
| 1745 |
-
} else {
|
| 1746 |
-
io2.emit("roomCreated", roomSummary);
|
| 1747 |
-
}
|
| 1748 |
-
console.log(
|
| 1749 |
-
`Player ${socket.id} ${roomId ? "joined" : "created"} room ${room.id}`
|
| 1750 |
-
);
|
| 1751 |
-
} else {
|
| 1752 |
-
if (callback) {
|
| 1753 |
-
callback({
|
| 1754 |
-
success: false,
|
| 1755 |
-
error: "Failed to join or create room",
|
| 1756 |
-
errorCode: "UNKNOWN_ERROR"
|
| 1757 |
-
});
|
| 1758 |
-
}
|
| 1759 |
-
}
|
| 1760 |
-
} catch (error) {
|
| 1761 |
-
console.error(`Error in joinRoom handler:`, error);
|
| 1762 |
-
if (callback) {
|
| 1763 |
-
callback({
|
| 1764 |
-
success: false,
|
| 1765 |
-
error: "Server error",
|
| 1766 |
-
errorCode: "SERVER_ERROR"
|
| 1767 |
-
});
|
| 1768 |
-
}
|
| 1769 |
-
}
|
| 1770 |
-
}
|
| 1771 |
-
);
|
| 1772 |
-
socket.on("leaveRoom", () => {
|
| 1773 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1774 |
-
if (room) {
|
| 1775 |
-
const roomId = room.id;
|
| 1776 |
-
socket.leave(room.id);
|
| 1777 |
-
gameManager2.leaveRoom(room.id, socket.id);
|
| 1778 |
-
socket.to(roomId).emit("playerLeft", {
|
| 1779 |
-
playerId: socket.id
|
| 1780 |
-
});
|
| 1781 |
-
const updatedRoom = gameManager2.getRoom(roomId);
|
| 1782 |
-
if (updatedRoom) {
|
| 1783 |
-
io2.emit("roomUpdated", getRoomSummary(updatedRoom));
|
| 1784 |
-
} else {
|
| 1785 |
-
io2.emit("roomDeleted", { roomId });
|
| 1786 |
-
}
|
| 1787 |
-
}
|
| 1788 |
-
});
|
| 1789 |
-
socket.on("makeMove", (data) => {
|
| 1790 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1791 |
-
if (room && gameManager2.makeMove(room.id, socket.id, data)) {
|
| 1792 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1793 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1794 |
-
const lastStep = room.game.getLastStep();
|
| 1795 |
-
const tgn = room.game.toTGN();
|
| 1796 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1797 |
-
currentPlayer,
|
| 1798 |
-
action: "move",
|
| 1799 |
-
lastMove: data,
|
| 1800 |
-
capturedStones: {
|
| 1801 |
-
black: stats?.capturedByBlack || 0,
|
| 1802 |
-
white: stats?.capturedByWhite || 0
|
| 1803 |
-
},
|
| 1804 |
-
capturedPositions: lastStep?.capturedPositions,
|
| 1805 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1806 |
-
tgn
|
| 1807 |
-
});
|
| 1808 |
-
} else {
|
| 1809 |
-
socket.emit("error", { message: "Invalid move" });
|
| 1810 |
-
}
|
| 1811 |
-
});
|
| 1812 |
-
socket.on("pass", () => {
|
| 1813 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1814 |
-
if (room && gameManager2.passTurn(room.id, socket.id)) {
|
| 1815 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1816 |
-
const tgn = room.game.toTGN();
|
| 1817 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1818 |
-
currentPlayer,
|
| 1819 |
-
action: "pass",
|
| 1820 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1821 |
-
tgn
|
| 1822 |
-
});
|
| 1823 |
-
if (gameManager2.checkConsecutivePasses(room.id)) {
|
| 1824 |
-
const territory = gameManager2.getTerritory(room.id);
|
| 1825 |
-
io2.to(room.id).emit("gameEnded", {
|
| 1826 |
-
winner: room.gameState.winner,
|
| 1827 |
-
reason: "double-pass",
|
| 1828 |
-
territory
|
| 1829 |
-
});
|
| 1830 |
-
}
|
| 1831 |
-
}
|
| 1832 |
-
});
|
| 1833 |
-
socket.on("resign", () => {
|
| 1834 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1835 |
-
if (room && gameManager2.resign(room.id, socket.id)) {
|
| 1836 |
-
io2.to(room.id).emit("gameEnded", {
|
| 1837 |
-
winner: room.gameState.winner,
|
| 1838 |
-
reason: "resignation"
|
| 1839 |
-
});
|
| 1840 |
-
}
|
| 1841 |
-
});
|
| 1842 |
-
socket.on("undoMove", (callback) => {
|
| 1843 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1844 |
-
if (!room) {
|
| 1845 |
-
if (callback) callback({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 1846 |
-
return;
|
| 1847 |
-
}
|
| 1848 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1849 |
-
if (callback) callback({ success: false, error: "Game not active", errorCode: "GAME_NOT_ACTIVE" });
|
| 1850 |
-
return;
|
| 1851 |
-
}
|
| 1852 |
-
const success = gameManager2.undoMove(room.id, socket.id);
|
| 1853 |
-
if (success) {
|
| 1854 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1855 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1856 |
-
const tgn = room.game.toTGN();
|
| 1857 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1858 |
-
currentPlayer,
|
| 1859 |
-
action: "undo",
|
| 1860 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1861 |
-
capturedStones: {
|
| 1862 |
-
black: stats?.capturedByBlack || 0,
|
| 1863 |
-
white: stats?.capturedByWhite || 0
|
| 1864 |
-
},
|
| 1865 |
-
tgn
|
| 1866 |
-
});
|
| 1867 |
-
if (callback) callback({ success: true });
|
| 1868 |
-
} else {
|
| 1869 |
-
if (callback) callback({ success: false, error: "Cannot undo", errorCode: "UNDO_FAILED" });
|
| 1870 |
-
}
|
| 1871 |
-
});
|
| 1872 |
-
socket.on("redoMove", (callback) => {
|
| 1873 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1874 |
-
if (!room) {
|
| 1875 |
-
if (callback) callback({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 1876 |
-
return;
|
| 1877 |
-
}
|
| 1878 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1879 |
-
if (callback) callback({ success: false, error: "Game not active", errorCode: "GAME_NOT_ACTIVE" });
|
| 1880 |
-
return;
|
| 1881 |
-
}
|
| 1882 |
-
if (!room.game.canRedo()) {
|
| 1883 |
-
if (callback) callback({ success: false, error: "Nothing to redo", errorCode: "NOTHING_TO_REDO" });
|
| 1884 |
-
return;
|
| 1885 |
-
}
|
| 1886 |
-
const success = gameManager2.redoMove(room.id, socket.id);
|
| 1887 |
-
if (success) {
|
| 1888 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1889 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1890 |
-
const lastStep = room.game.getLastStep();
|
| 1891 |
-
const tgn = room.game.toTGN();
|
| 1892 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1893 |
-
currentPlayer,
|
| 1894 |
-
action: "redo",
|
| 1895 |
-
lastMove: lastStep?.position,
|
| 1896 |
-
capturedStones: {
|
| 1897 |
-
black: stats?.capturedByBlack || 0,
|
| 1898 |
-
white: stats?.capturedByWhite || 0
|
| 1899 |
-
},
|
| 1900 |
-
capturedPositions: lastStep?.capturedPositions,
|
| 1901 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1902 |
-
tgn
|
| 1903 |
-
});
|
| 1904 |
-
if (callback) callback({ success: true });
|
| 1905 |
-
} else {
|
| 1906 |
-
if (callback) callback({ success: false, error: "Redo failed", errorCode: "REDO_FAILED" });
|
| 1907 |
-
}
|
| 1908 |
-
});
|
| 1909 |
-
socket.on("resetGame", (data, callback) => {
|
| 1910 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1911 |
-
if (!room) {
|
| 1912 |
-
const cb = typeof data === "function" ? data : callback;
|
| 1913 |
-
if (cb) cb({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 1914 |
-
return;
|
| 1915 |
-
}
|
| 1916 |
-
const options = typeof data === "object" && data !== null ? {
|
| 1917 |
-
boardShape: data.boardShape,
|
| 1918 |
-
playerColors: data.playerColors
|
| 1919 |
-
} : void 0;
|
| 1920 |
-
const responseCb = typeof data === "function" ? data : callback;
|
| 1921 |
-
const result = gameManager2.resetGame(room.id, socket.id, options);
|
| 1922 |
-
if (result.success) {
|
| 1923 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1924 |
-
const tgn = room.game.toTGN();
|
| 1925 |
-
const players = {};
|
| 1926 |
-
for (const [pid, player] of Object.entries(room.players)) {
|
| 1927 |
-
players[pid] = {
|
| 1928 |
-
nickname: player.nickname,
|
| 1929 |
-
color: player.color
|
| 1930 |
-
};
|
| 1931 |
-
}
|
| 1932 |
-
io2.to(room.id).emit("gameReset", {
|
| 1933 |
-
currentPlayer,
|
| 1934 |
-
boardShape: room.game.getShape(),
|
| 1935 |
-
currentMoveIndex: 0,
|
| 1936 |
-
capturedStones: { black: 0, white: 0 },
|
| 1937 |
-
players,
|
| 1938 |
-
tgn
|
| 1939 |
-
});
|
| 1940 |
-
if (responseCb) responseCb({ success: true });
|
| 1941 |
-
} else {
|
| 1942 |
-
if (responseCb) responseCb({
|
| 1943 |
-
success: false,
|
| 1944 |
-
error: result.error || "Reset failed",
|
| 1945 |
-
errorCode: result.error === "Only room admin can reset the game" ? "NOT_ADMIN" : "RESET_FAILED"
|
| 1946 |
-
});
|
| 1947 |
-
}
|
| 1948 |
-
});
|
| 1949 |
-
socket.on("chatMessage", (data) => {
|
| 1950 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1951 |
-
if (room) {
|
| 1952 |
-
const player = room.players[socket.id];
|
| 1953 |
-
io2.to(room.id).emit("chatMessage", {
|
| 1954 |
-
author: player?.nickname || "Unknown",
|
| 1955 |
-
content: data.content,
|
| 1956 |
-
playerId: socket.id
|
| 1957 |
-
});
|
| 1958 |
-
}
|
| 1959 |
-
});
|
| 1960 |
-
socket.on(
|
| 1961 |
-
"changeNickname",
|
| 1962 |
-
(data, callback) => {
|
| 1963 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1964 |
-
if (!room) {
|
| 1965 |
-
const error = { success: false, error: "Not in a room" };
|
| 1966 |
-
if (callback) callback(error);
|
| 1967 |
-
return;
|
| 1968 |
-
}
|
| 1969 |
-
const validation = validateNickname(data.nickname);
|
| 1970 |
-
if (!validation.valid) {
|
| 1971 |
-
const error = { success: false, error: validation.error };
|
| 1972 |
-
if (callback) callback(error);
|
| 1973 |
-
return;
|
| 1974 |
-
}
|
| 1975 |
-
const player = room.players[socket.id];
|
| 1976 |
-
if (player) {
|
| 1977 |
-
const oldNickname = player.nickname;
|
| 1978 |
-
player.nickname = data.nickname;
|
| 1979 |
-
io2.to(room.id).emit("nicknameChanged", {
|
| 1980 |
-
playerId: socket.id,
|
| 1981 |
-
nickname: data.nickname,
|
| 1982 |
-
oldNickname
|
| 1983 |
-
});
|
| 1984 |
-
console.log(`Player ${socket.id} changed nickname: ${oldNickname} -> ${data.nickname}`);
|
| 1985 |
-
if (callback) {
|
| 1986 |
-
callback({ success: true, nickname: data.nickname });
|
| 1987 |
-
}
|
| 1988 |
-
}
|
| 1989 |
-
}
|
| 1990 |
-
);
|
| 1991 |
-
socket.on("disconnect", () => {
|
| 1992 |
-
console.log(`Client disconnected: ${socket.id}`);
|
| 1993 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1994 |
-
if (room) {
|
| 1995 |
-
const roomId = room.id;
|
| 1996 |
-
gameManager2.leaveRoom(room.id, socket.id);
|
| 1997 |
-
socket.to(room.id).emit("playerDisconnected", {
|
| 1998 |
-
playerId: socket.id
|
| 1999 |
-
});
|
| 2000 |
-
const updatedRoom = gameManager2.getRoom(roomId);
|
| 2001 |
-
if (updatedRoom) {
|
| 2002 |
-
io2.emit("roomUpdated", getRoomSummary(updatedRoom));
|
| 2003 |
-
} else {
|
| 2004 |
-
io2.emit("roomDeleted", { roomId });
|
| 2005 |
-
}
|
| 2006 |
-
}
|
| 2007 |
-
});
|
| 2008 |
-
}
|
| 2009 |
-
function validateNickname(nickname) {
|
| 2010 |
-
if (!nickname || typeof nickname !== "string") {
|
| 2011 |
-
return { valid: false, error: "Invalid nickname" };
|
| 2012 |
-
}
|
| 2013 |
-
const trimmed = nickname.trim();
|
| 2014 |
-
if (trimmed.length < 3) {
|
| 2015 |
-
return { valid: false, error: "Nickname must be at least 3 characters" };
|
| 2016 |
-
}
|
| 2017 |
-
if (trimmed.length > 20) {
|
| 2018 |
-
return { valid: false, error: "Nickname must be 20 characters or less" };
|
| 2019 |
-
}
|
| 2020 |
-
if (!/^[a-zA-Z0-9 ]+$/.test(trimmed)) {
|
| 2021 |
-
return { valid: false, error: "Only letters, numbers, and spaces allowed" };
|
| 2022 |
-
}
|
| 2023 |
-
if (trimmed !== nickname) {
|
| 2024 |
-
return { valid: false, error: "No leading or trailing spaces allowed" };
|
| 2025 |
-
}
|
| 2026 |
-
return { valid: true };
|
| 2027 |
-
}
|
| 2028 |
-
|
| 2029 |
-
// backend/src/server.ts
|
| 2030 |
-
var __filename = fileURLToPath(import.meta.url);
|
| 2031 |
-
var __dirname = path.dirname(__filename);
|
| 2032 |
-
var isDev = __dirname.includes("/src") && !__dirname.includes("/dist");
|
| 2033 |
-
var levelsUp = isDev ? "../" : "../../../";
|
| 2034 |
-
var envPath = path.join(__dirname, levelsUp, ".env");
|
| 2035 |
-
var envLocalPath = path.join(__dirname, levelsUp, ".env.local");
|
| 2036 |
-
if (fs.existsSync(envPath)) {
|
| 2037 |
-
dotenv.config({ path: envPath });
|
| 2038 |
-
console.log("[Config] Loaded .env");
|
| 2039 |
-
} else {
|
| 2040 |
-
console.log(`[Config] .env not found at: ${envPath}`);
|
| 2041 |
-
}
|
| 2042 |
-
if (fs.existsSync(envLocalPath)) {
|
| 2043 |
-
dotenv.config({ path: envLocalPath, override: true });
|
| 2044 |
-
console.log("[Config] Loaded .env.local (overriding .env)");
|
| 2045 |
-
} else {
|
| 2046 |
-
console.log(`[Config] .env.local not found at: ${envLocalPath}`);
|
| 2047 |
-
}
|
| 2048 |
-
var app = express();
|
| 2049 |
-
var httpServer = createServer(app);
|
| 2050 |
-
var io = new Server(httpServer, {
|
| 2051 |
-
cors: {
|
| 2052 |
-
origin: process.env.NODE_ENV === "production" ? process.env.CLIENT_URL || "http://localhost:5173" : true,
|
| 2053 |
-
// Allow all origins in development
|
| 2054 |
-
methods: ["GET", "POST"],
|
| 2055 |
-
credentials: true
|
| 2056 |
-
}
|
| 2057 |
-
});
|
| 2058 |
-
var gameManager = new GameManager();
|
| 2059 |
-
var PORT = parseInt(process.env.PORT || "3000", 10);
|
| 2060 |
-
var HOST = process.env.HOST || "0.0.0.0";
|
| 2061 |
-
console.log(`[Config] Server Configuration:`);
|
| 2062 |
-
console.log(`[Config] PORT: ${PORT}`);
|
| 2063 |
-
console.log(`[Config] HOST: ${HOST}`);
|
| 2064 |
-
console.log(`[Config] NODE_ENV: ${process.env.NODE_ENV || "development"}`);
|
| 2065 |
-
console.log(`[Config] CLIENT_URL: ${process.env.CLIENT_URL || "not set"}`);
|
| 2066 |
-
app.use(cors());
|
| 2067 |
-
app.use(express.json());
|
| 2068 |
-
if (process.env.NODE_ENV === "production") {
|
| 2069 |
-
const frontendPath = path.join(__dirname, "../../../../app/dist");
|
| 2070 |
-
app.use(express.static(frontendPath));
|
| 2071 |
-
app.get("*", (req, res, next) => {
|
| 2072 |
-
if (req.path.startsWith("/health") || req.path.startsWith("/socket.io")) {
|
| 2073 |
-
return next();
|
| 2074 |
-
}
|
| 2075 |
-
res.sendFile(path.join(frontendPath, "index.html"));
|
| 2076 |
-
});
|
| 2077 |
-
}
|
| 2078 |
-
app.get("/health", (_req, res) => {
|
| 2079 |
-
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
| 2080 |
-
});
|
| 2081 |
-
io.on("connection", (socket) => {
|
| 2082 |
-
console.log(`New client connected: ${socket.id}`);
|
| 2083 |
-
setupSocketHandlers(io, socket, gameManager);
|
| 2084 |
-
socket.on("echo", (data, callback) => {
|
| 2085 |
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
| 2086 |
-
const responseMessage = `Hello from server! Received: "${data.message}" at ${timestamp}`;
|
| 2087 |
-
console.log(`[Echo] Client ${socket.id}: ${data.message}`);
|
| 2088 |
-
if (callback && typeof callback === "function") {
|
| 2089 |
-
callback({
|
| 2090 |
-
message: responseMessage,
|
| 2091 |
-
serverTime: timestamp,
|
| 2092 |
-
clientTime: data.timestamp
|
| 2093 |
-
});
|
| 2094 |
-
}
|
| 2095 |
-
});
|
| 2096 |
-
socket.on("disconnect", () => {
|
| 2097 |
-
console.log(`Client disconnected: ${socket.id}`);
|
| 2098 |
-
});
|
| 2099 |
-
});
|
| 2100 |
-
httpServer.listen(PORT, HOST, () => {
|
| 2101 |
-
console.log(`Server running on ${HOST}:${PORT}`);
|
| 2102 |
-
console.log(`Health check: http://${HOST}:${PORT}/health`);
|
| 2103 |
-
console.log(`Environment: ${process.env.NODE_ENV || "development"}`);
|
| 2104 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/backend/dist/server.js
DELETED
|
@@ -1,2104 +0,0 @@
|
|
| 1 |
-
// backend/src/server.ts
|
| 2 |
-
import express from "express";
|
| 3 |
-
import { createServer } from "http";
|
| 4 |
-
import { Server } from "socket.io";
|
| 5 |
-
import cors from "cors";
|
| 6 |
-
import dotenv from "dotenv";
|
| 7 |
-
import path from "path";
|
| 8 |
-
import fs from "fs";
|
| 9 |
-
import { fileURLToPath } from "url";
|
| 10 |
-
|
| 11 |
-
// backend/src/services/gameManager.ts
|
| 12 |
-
import { v4 as uuidv4 } from "uuid";
|
| 13 |
-
|
| 14 |
-
// inc/trigo/gameUtils.ts
|
| 15 |
-
var StoneType = {
|
| 16 |
-
EMPTY: 0,
|
| 17 |
-
BLACK: 1,
|
| 18 |
-
WHITE: 2
|
| 19 |
-
};
|
| 20 |
-
function getEnemyColor(color) {
|
| 21 |
-
if (color === StoneType.BLACK) return StoneType.WHITE;
|
| 22 |
-
if (color === StoneType.WHITE) return StoneType.BLACK;
|
| 23 |
-
return StoneType.EMPTY;
|
| 24 |
-
}
|
| 25 |
-
function isInBounds(pos, shape) {
|
| 26 |
-
return pos.x >= 0 && pos.x < shape.x && pos.y >= 0 && pos.y < shape.y && pos.z >= 0 && pos.z < shape.z;
|
| 27 |
-
}
|
| 28 |
-
function getNeighbors(pos, shape) {
|
| 29 |
-
const neighbors = [];
|
| 30 |
-
const directions = [
|
| 31 |
-
{ x: 1, y: 0, z: 0 },
|
| 32 |
-
{ x: -1, y: 0, z: 0 },
|
| 33 |
-
{ x: 0, y: 1, z: 0 },
|
| 34 |
-
{ x: 0, y: -1, z: 0 },
|
| 35 |
-
{ x: 0, y: 0, z: 1 },
|
| 36 |
-
{ x: 0, y: 0, z: -1 }
|
| 37 |
-
];
|
| 38 |
-
for (const dir of directions) {
|
| 39 |
-
const neighbor = {
|
| 40 |
-
x: pos.x + dir.x,
|
| 41 |
-
y: pos.y + dir.y,
|
| 42 |
-
z: pos.z + dir.z
|
| 43 |
-
};
|
| 44 |
-
if (isInBounds(neighbor, shape)) {
|
| 45 |
-
neighbors.push(neighbor);
|
| 46 |
-
}
|
| 47 |
-
}
|
| 48 |
-
return neighbors;
|
| 49 |
-
}
|
| 50 |
-
function positionsEqual(p1, p2) {
|
| 51 |
-
return p1.x === p2.x && p1.y === p2.y && p1.z === p2.z;
|
| 52 |
-
}
|
| 53 |
-
var CoordSet = class {
|
| 54 |
-
constructor() {
|
| 55 |
-
this.positions = [];
|
| 56 |
-
}
|
| 57 |
-
has(pos) {
|
| 58 |
-
return this.positions.some((p) => positionsEqual(p, pos));
|
| 59 |
-
}
|
| 60 |
-
insert(pos) {
|
| 61 |
-
if (!this.has(pos)) {
|
| 62 |
-
this.positions.push(pos);
|
| 63 |
-
return true;
|
| 64 |
-
}
|
| 65 |
-
return false;
|
| 66 |
-
}
|
| 67 |
-
remove(pos) {
|
| 68 |
-
this.positions = this.positions.filter((p) => !positionsEqual(p, pos));
|
| 69 |
-
}
|
| 70 |
-
size() {
|
| 71 |
-
return this.positions.length;
|
| 72 |
-
}
|
| 73 |
-
empty() {
|
| 74 |
-
return this.positions.length === 0;
|
| 75 |
-
}
|
| 76 |
-
forEach(callback) {
|
| 77 |
-
this.positions.forEach(callback);
|
| 78 |
-
}
|
| 79 |
-
toArray() {
|
| 80 |
-
return [...this.positions];
|
| 81 |
-
}
|
| 82 |
-
clear() {
|
| 83 |
-
this.positions = [];
|
| 84 |
-
}
|
| 85 |
-
};
|
| 86 |
-
var Patch = class {
|
| 87 |
-
constructor(color = StoneType.EMPTY) {
|
| 88 |
-
this.positions = new CoordSet();
|
| 89 |
-
this.color = StoneType.EMPTY;
|
| 90 |
-
this.color = color;
|
| 91 |
-
}
|
| 92 |
-
addStone(pos) {
|
| 93 |
-
this.positions.insert(pos);
|
| 94 |
-
}
|
| 95 |
-
size() {
|
| 96 |
-
return this.positions.size();
|
| 97 |
-
}
|
| 98 |
-
/**
|
| 99 |
-
* Get all liberties (empty adjacent positions) for this group
|
| 100 |
-
*
|
| 101 |
-
* Equivalent to StoneArray.patchAir() in prototype
|
| 102 |
-
* Returns a CoordSet of empty positions adjacent to this patch
|
| 103 |
-
*/
|
| 104 |
-
getLiberties(board, shape) {
|
| 105 |
-
const liberties = new CoordSet();
|
| 106 |
-
this.positions.forEach((stonePos) => {
|
| 107 |
-
const neighbors = getNeighbors(stonePos, shape);
|
| 108 |
-
for (const neighbor of neighbors) {
|
| 109 |
-
if (board[neighbor.x][neighbor.y][neighbor.z] === StoneType.EMPTY) {
|
| 110 |
-
liberties.insert(neighbor);
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
});
|
| 114 |
-
return liberties;
|
| 115 |
-
}
|
| 116 |
-
};
|
| 117 |
-
function findGroup(pos, board, shape) {
|
| 118 |
-
const color = board[pos.x][pos.y][pos.z];
|
| 119 |
-
const group = new Patch(color);
|
| 120 |
-
if (color === StoneType.EMPTY) {
|
| 121 |
-
return group;
|
| 122 |
-
}
|
| 123 |
-
const visited = new CoordSet();
|
| 124 |
-
const stack = [pos];
|
| 125 |
-
while (stack.length > 0) {
|
| 126 |
-
const current = stack.pop();
|
| 127 |
-
if (visited.has(current)) {
|
| 128 |
-
continue;
|
| 129 |
-
}
|
| 130 |
-
visited.insert(current);
|
| 131 |
-
if (board[current.x][current.y][current.z] === color) {
|
| 132 |
-
group.addStone(current);
|
| 133 |
-
const neighbors = getNeighbors(current, shape);
|
| 134 |
-
for (const neighbor of neighbors) {
|
| 135 |
-
if (!visited.has(neighbor)) {
|
| 136 |
-
stack.push(neighbor);
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
}
|
| 140 |
-
}
|
| 141 |
-
return group;
|
| 142 |
-
}
|
| 143 |
-
function getNeighborGroups(pos, board, shape, excludeEmpty = false) {
|
| 144 |
-
const neighbors = getNeighbors(pos, shape);
|
| 145 |
-
const groups = [];
|
| 146 |
-
const processedPositions = new CoordSet();
|
| 147 |
-
for (const neighbor of neighbors) {
|
| 148 |
-
if (processedPositions.has(neighbor)) {
|
| 149 |
-
continue;
|
| 150 |
-
}
|
| 151 |
-
const stone = board[neighbor.x][neighbor.y][neighbor.z];
|
| 152 |
-
if (excludeEmpty && stone === StoneType.EMPTY) {
|
| 153 |
-
continue;
|
| 154 |
-
}
|
| 155 |
-
if (stone !== StoneType.EMPTY) {
|
| 156 |
-
const group = findGroup(neighbor, board, shape);
|
| 157 |
-
group.positions.forEach((p) => processedPositions.insert(p));
|
| 158 |
-
groups.push(group);
|
| 159 |
-
}
|
| 160 |
-
}
|
| 161 |
-
return groups;
|
| 162 |
-
}
|
| 163 |
-
function isGroupCaptured(group, board, shape) {
|
| 164 |
-
const liberties = group.getLiberties(board, shape);
|
| 165 |
-
return liberties.size() === 0;
|
| 166 |
-
}
|
| 167 |
-
function findCapturedGroups(pos, playerColor, board, shape) {
|
| 168 |
-
const enemyColor = getEnemyColor(playerColor);
|
| 169 |
-
const captured = [];
|
| 170 |
-
const tempBoard = board.map((plane) => plane.map((row) => [...row]));
|
| 171 |
-
tempBoard[pos.x][pos.y][pos.z] = playerColor;
|
| 172 |
-
const neighborGroups = getNeighborGroups(pos, tempBoard, shape, true);
|
| 173 |
-
for (const group of neighborGroups) {
|
| 174 |
-
if (group.color === enemyColor) {
|
| 175 |
-
if (isGroupCaptured(group, tempBoard, shape)) {
|
| 176 |
-
captured.push(group);
|
| 177 |
-
}
|
| 178 |
-
}
|
| 179 |
-
}
|
| 180 |
-
return captured;
|
| 181 |
-
}
|
| 182 |
-
function isSuicideMove(pos, playerColor, board, shape) {
|
| 183 |
-
const tempBoard = board.map((plane) => plane.map((row) => [...row]));
|
| 184 |
-
tempBoard[pos.x][pos.y][pos.z] = playerColor;
|
| 185 |
-
const capturedGroups = findCapturedGroups(pos, playerColor, board, shape);
|
| 186 |
-
if (capturedGroups.length > 0) {
|
| 187 |
-
return false;
|
| 188 |
-
}
|
| 189 |
-
const placedGroup = findGroup(pos, tempBoard, shape);
|
| 190 |
-
const liberties = placedGroup.getLiberties(tempBoard, shape);
|
| 191 |
-
return liberties.size() === 0;
|
| 192 |
-
}
|
| 193 |
-
function isKoViolation(pos, playerColor, board, shape, lastCapturedPositions) {
|
| 194 |
-
if (!lastCapturedPositions || lastCapturedPositions.length !== 1) {
|
| 195 |
-
return false;
|
| 196 |
-
}
|
| 197 |
-
const capturedGroups = findCapturedGroups(pos, playerColor, board, shape);
|
| 198 |
-
if (capturedGroups.length !== 1 || capturedGroups[0].size() !== 1) {
|
| 199 |
-
return false;
|
| 200 |
-
}
|
| 201 |
-
const previouslyCaptured = lastCapturedPositions[0];
|
| 202 |
-
if (positionsEqual(pos, previouslyCaptured)) {
|
| 203 |
-
return true;
|
| 204 |
-
}
|
| 205 |
-
return false;
|
| 206 |
-
}
|
| 207 |
-
function executeCaptures(capturedGroups, board) {
|
| 208 |
-
const capturedPositions = [];
|
| 209 |
-
for (const group of capturedGroups) {
|
| 210 |
-
group.positions.forEach((pos) => {
|
| 211 |
-
board[pos.x][pos.y][pos.z] = StoneType.EMPTY;
|
| 212 |
-
capturedPositions.push(pos);
|
| 213 |
-
});
|
| 214 |
-
}
|
| 215 |
-
return capturedPositions;
|
| 216 |
-
}
|
| 217 |
-
function findEmptyRegion(startPos, board, shape, visited) {
|
| 218 |
-
const region = new CoordSet();
|
| 219 |
-
const stack = [startPos];
|
| 220 |
-
while (stack.length > 0) {
|
| 221 |
-
const pos = stack.pop();
|
| 222 |
-
if (visited.has(pos)) {
|
| 223 |
-
continue;
|
| 224 |
-
}
|
| 225 |
-
visited.insert(pos);
|
| 226 |
-
if (board[pos.x][pos.y][pos.z] === StoneType.EMPTY) {
|
| 227 |
-
region.insert(pos);
|
| 228 |
-
const neighbors = getNeighbors(pos, shape);
|
| 229 |
-
for (const neighbor of neighbors) {
|
| 230 |
-
if (!visited.has(neighbor)) {
|
| 231 |
-
stack.push(neighbor);
|
| 232 |
-
}
|
| 233 |
-
}
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
return region;
|
| 237 |
-
}
|
| 238 |
-
function determineRegionOwner(region, board, shape) {
|
| 239 |
-
let owner = StoneType.EMPTY;
|
| 240 |
-
let solved = false;
|
| 241 |
-
region.forEach((pos) => {
|
| 242 |
-
if (solved) return;
|
| 243 |
-
const neighbors = getNeighbors(pos, shape);
|
| 244 |
-
for (const neighbor of neighbors) {
|
| 245 |
-
if (solved) break;
|
| 246 |
-
const stone = board[neighbor.x][neighbor.y][neighbor.z];
|
| 247 |
-
if (stone !== StoneType.EMPTY) {
|
| 248 |
-
if (owner === StoneType.EMPTY) {
|
| 249 |
-
owner = stone;
|
| 250 |
-
} else if (owner !== stone) {
|
| 251 |
-
owner = StoneType.EMPTY;
|
| 252 |
-
solved = true;
|
| 253 |
-
}
|
| 254 |
-
}
|
| 255 |
-
}
|
| 256 |
-
});
|
| 257 |
-
return owner;
|
| 258 |
-
}
|
| 259 |
-
function calculateTerritory(board, shape) {
|
| 260 |
-
const result = {
|
| 261 |
-
black: 0,
|
| 262 |
-
white: 0,
|
| 263 |
-
neutral: 0,
|
| 264 |
-
blackTerritory: [],
|
| 265 |
-
whiteTerritory: [],
|
| 266 |
-
neutralTerritory: []
|
| 267 |
-
};
|
| 268 |
-
const visited = new CoordSet();
|
| 269 |
-
const emptyRegions = [];
|
| 270 |
-
for (let x = 0; x < shape.x; x++) {
|
| 271 |
-
for (let y = 0; y < shape.y; y++) {
|
| 272 |
-
for (let z = 0; z < shape.z; z++) {
|
| 273 |
-
const pos = { x, y, z };
|
| 274 |
-
const stone = board[x][y][z];
|
| 275 |
-
if (stone === StoneType.BLACK) {
|
| 276 |
-
result.black++;
|
| 277 |
-
result.blackTerritory.push(pos);
|
| 278 |
-
} else if (stone === StoneType.WHITE) {
|
| 279 |
-
result.white++;
|
| 280 |
-
result.whiteTerritory.push(pos);
|
| 281 |
-
} else if (!visited.has(pos)) {
|
| 282 |
-
const region = findEmptyRegion(pos, board, shape, visited);
|
| 283 |
-
emptyRegions.push(region);
|
| 284 |
-
}
|
| 285 |
-
}
|
| 286 |
-
}
|
| 287 |
-
}
|
| 288 |
-
for (const region of emptyRegions) {
|
| 289 |
-
const owner = determineRegionOwner(region, board, shape);
|
| 290 |
-
const regionArray = region.toArray();
|
| 291 |
-
if (owner === StoneType.BLACK) {
|
| 292 |
-
result.black += region.size();
|
| 293 |
-
result.blackTerritory.push(...regionArray);
|
| 294 |
-
} else if (owner === StoneType.WHITE) {
|
| 295 |
-
result.white += region.size();
|
| 296 |
-
result.whiteTerritory.push(...regionArray);
|
| 297 |
-
} else {
|
| 298 |
-
result.neutral += region.size();
|
| 299 |
-
result.neutralTerritory.push(...regionArray);
|
| 300 |
-
}
|
| 301 |
-
}
|
| 302 |
-
return result;
|
| 303 |
-
}
|
| 304 |
-
function validateMove(pos, playerColor, board, shape, lastCapturedPositions = null) {
|
| 305 |
-
if (!isInBounds(pos, shape)) {
|
| 306 |
-
return { valid: false, reason: "Position out of bounds" };
|
| 307 |
-
}
|
| 308 |
-
if (board[pos.x][pos.y][pos.z] !== StoneType.EMPTY) {
|
| 309 |
-
return { valid: false, reason: "Position already occupied" };
|
| 310 |
-
}
|
| 311 |
-
if (isKoViolation(pos, playerColor, board, shape, lastCapturedPositions)) {
|
| 312 |
-
return { valid: false, reason: "Ko rule violation" };
|
| 313 |
-
}
|
| 314 |
-
if (isSuicideMove(pos, playerColor, board, shape)) {
|
| 315 |
-
return { valid: false, reason: "suicide move not allowed" };
|
| 316 |
-
}
|
| 317 |
-
return { valid: true };
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
// inc/trigo/ab0yz.ts
|
| 321 |
-
var compactShape = (shape) => shape[shape.length - 1] === 1 ? compactShape(shape.slice(0, shape.length - 1)) : shape;
|
| 322 |
-
var encodeAb0yz = (pos, boardShape) => {
|
| 323 |
-
const compactedShape = compactShape(boardShape);
|
| 324 |
-
const result = [];
|
| 325 |
-
for (let i = 0; i < compactedShape.length; i++) {
|
| 326 |
-
const size = compactedShape[i];
|
| 327 |
-
const center = (size - 1) / 2;
|
| 328 |
-
const index = pos[i];
|
| 329 |
-
if (index === center) {
|
| 330 |
-
result.push("0");
|
| 331 |
-
} else if (index < center) {
|
| 332 |
-
result.push(String.fromCharCode(97 + index));
|
| 333 |
-
} else {
|
| 334 |
-
const offset = size - 1 - index;
|
| 335 |
-
result.push(String.fromCharCode(122 - offset));
|
| 336 |
-
}
|
| 337 |
-
}
|
| 338 |
-
return result.join("");
|
| 339 |
-
};
|
| 340 |
-
var decodeAb0yz = (code, boardShape) => {
|
| 341 |
-
const compactedShape = compactShape(boardShape);
|
| 342 |
-
if (code.length !== compactedShape.length) {
|
| 343 |
-
throw new Error(
|
| 344 |
-
`Invalid TGN coordinate: "${code}" (must be ${compactedShape.length} characters for board shape ${boardShape.join("x")})`
|
| 345 |
-
);
|
| 346 |
-
}
|
| 347 |
-
const result = [];
|
| 348 |
-
for (let i = 0; i < compactedShape.length; i++) {
|
| 349 |
-
const char = code[i];
|
| 350 |
-
const size = compactedShape[i];
|
| 351 |
-
const center = (size - 1) / 2;
|
| 352 |
-
if (char === "0") {
|
| 353 |
-
console.assert(Number.isInteger(center));
|
| 354 |
-
result.push(center);
|
| 355 |
-
} else {
|
| 356 |
-
const charCode = char.charCodeAt(0);
|
| 357 |
-
if (charCode >= 97 && charCode <= 122) {
|
| 358 |
-
const distFromA = charCode - 97;
|
| 359 |
-
const distFromZ = 122 - charCode;
|
| 360 |
-
if (distFromA < distFromZ) {
|
| 361 |
-
const index = distFromA;
|
| 362 |
-
if (index >= center) {
|
| 363 |
-
throw new Error(
|
| 364 |
-
`Invalid TGN coordinate: "${code}" (position ${index} >= center ${center} on axis ${i})`
|
| 365 |
-
);
|
| 366 |
-
}
|
| 367 |
-
result.push(index);
|
| 368 |
-
} else {
|
| 369 |
-
const index = size - 1 - distFromZ;
|
| 370 |
-
if (index <= center) {
|
| 371 |
-
throw new Error(
|
| 372 |
-
`Invalid TGN coordinate: "${code}" (position ${index} <= center ${center} on axis ${i})`
|
| 373 |
-
);
|
| 374 |
-
}
|
| 375 |
-
result.push(index);
|
| 376 |
-
}
|
| 377 |
-
} else {
|
| 378 |
-
throw new Error(
|
| 379 |
-
`Invalid TGN coordinate: "${code}" (character '${char}' at position ${i} must be '0' or a-z)`
|
| 380 |
-
);
|
| 381 |
-
}
|
| 382 |
-
}
|
| 383 |
-
}
|
| 384 |
-
while (result.length < boardShape.length) {
|
| 385 |
-
result.push(0);
|
| 386 |
-
}
|
| 387 |
-
return result;
|
| 388 |
-
};
|
| 389 |
-
|
| 390 |
-
// inc/tgn/tgnParser.ts
|
| 391 |
-
var TGNParseError = class extends Error {
|
| 392 |
-
constructor(message, line, column, hash) {
|
| 393 |
-
super(message);
|
| 394 |
-
this.line = line;
|
| 395 |
-
this.column = column;
|
| 396 |
-
this.hash = hash;
|
| 397 |
-
this.name = "TGNParseError";
|
| 398 |
-
}
|
| 399 |
-
};
|
| 400 |
-
var parserModule = null;
|
| 401 |
-
function getParser() {
|
| 402 |
-
if (!parserModule) {
|
| 403 |
-
throw new Error(
|
| 404 |
-
"TGN parser not loaded. Please ensure the parser has been built.\nRun: npm run build:parsers"
|
| 405 |
-
);
|
| 406 |
-
}
|
| 407 |
-
return parserModule;
|
| 408 |
-
}
|
| 409 |
-
function parseTGN(tgnString) {
|
| 410 |
-
const parser = getParser();
|
| 411 |
-
if (!parser.parse) {
|
| 412 |
-
throw new Error("TGN parser parse method not available");
|
| 413 |
-
}
|
| 414 |
-
try {
|
| 415 |
-
const result = parser.parse(tgnString);
|
| 416 |
-
return result;
|
| 417 |
-
} catch (error) {
|
| 418 |
-
throw new TGNParseError(
|
| 419 |
-
error.message || "Unknown parse error",
|
| 420 |
-
error.hash?.line,
|
| 421 |
-
error.hash?.loc?.first_column,
|
| 422 |
-
error.hash
|
| 423 |
-
);
|
| 424 |
-
}
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
// inc/trigo/game.ts
|
| 428 |
-
var TrigoGame = class _TrigoGame {
|
| 429 |
-
/**
|
| 430 |
-
* Constructor
|
| 431 |
-
* Equivalent to trigo.Game constructor (lines 75-85)
|
| 432 |
-
*/
|
| 433 |
-
constructor(shape = { x: 5, y: 5, z: 5 }, callbacks = {}) {
|
| 434 |
-
// Last captured stones for Ko rule detection
|
| 435 |
-
this.lastCapturedPositions = null;
|
| 436 |
-
// Static analysis cache (territory, capturing moves)
|
| 437 |
-
// Invalidated on any board state change
|
| 438 |
-
this.cachedTerritory = null;
|
| 439 |
-
this.cachedCapturingMove = /* @__PURE__ */ new Map();
|
| 440 |
-
this.shape = shape;
|
| 441 |
-
this.callbacks = callbacks;
|
| 442 |
-
this.board = this.createEmptyBoard();
|
| 443 |
-
this.currentPlayer = StoneType.BLACK;
|
| 444 |
-
this.stepHistory = [];
|
| 445 |
-
this.currentStepIndex = 0;
|
| 446 |
-
this.gameStatus = "idle";
|
| 447 |
-
this.gameResult = void 0;
|
| 448 |
-
this.passCount = 0;
|
| 449 |
-
}
|
| 450 |
-
/**
|
| 451 |
-
* Create an empty board
|
| 452 |
-
*/
|
| 453 |
-
createEmptyBoard() {
|
| 454 |
-
const board = [];
|
| 455 |
-
for (let x = 0; x < this.shape.x; x++) {
|
| 456 |
-
board[x] = [];
|
| 457 |
-
for (let y = 0; y < this.shape.y; y++) {
|
| 458 |
-
board[x][y] = [];
|
| 459 |
-
for (let z = 0; z < this.shape.z; z++) {
|
| 460 |
-
board[x][y][z] = StoneType.EMPTY;
|
| 461 |
-
}
|
| 462 |
-
}
|
| 463 |
-
}
|
| 464 |
-
return board;
|
| 465 |
-
}
|
| 466 |
-
/**
|
| 467 |
-
* Reset the game to initial state
|
| 468 |
-
* Equivalent to Game.reset() (lines 153-163)
|
| 469 |
-
*/
|
| 470 |
-
reset() {
|
| 471 |
-
this.board = this.createEmptyBoard();
|
| 472 |
-
this.currentPlayer = StoneType.BLACK;
|
| 473 |
-
this.stepHistory = [];
|
| 474 |
-
this.currentStepIndex = 0;
|
| 475 |
-
this.lastCapturedPositions = null;
|
| 476 |
-
this.invalidateAnalysisCache();
|
| 477 |
-
this.gameStatus = "idle";
|
| 478 |
-
this.gameResult = void 0;
|
| 479 |
-
this.passCount = 0;
|
| 480 |
-
}
|
| 481 |
-
/**
|
| 482 |
-
* Invalidate all static analysis caches
|
| 483 |
-
* Called when board state changes
|
| 484 |
-
*/
|
| 485 |
-
invalidateAnalysisCache() {
|
| 486 |
-
this.cachedTerritory = null;
|
| 487 |
-
this.cachedCapturingMove.clear();
|
| 488 |
-
}
|
| 489 |
-
/**
|
| 490 |
-
* Clone the game state (deep copy)
|
| 491 |
-
* Creates an independent copy with all state preserved
|
| 492 |
-
*/
|
| 493 |
-
clone() {
|
| 494 |
-
const cloned = new _TrigoGame(this.shape, {});
|
| 495 |
-
cloned.board = this.board.map((plane) => plane.map((row) => [...row]));
|
| 496 |
-
cloned.currentPlayer = this.currentPlayer;
|
| 497 |
-
cloned.currentStepIndex = this.currentStepIndex;
|
| 498 |
-
cloned.gameStatus = this.gameStatus;
|
| 499 |
-
cloned.passCount = this.passCount;
|
| 500 |
-
cloned.stepHistory = this.stepHistory.map((step) => ({
|
| 501 |
-
...step,
|
| 502 |
-
position: step.position ? { ...step.position } : void 0,
|
| 503 |
-
capturedPositions: step.capturedPositions ? step.capturedPositions.map((pos) => ({ ...pos })) : []
|
| 504 |
-
}));
|
| 505 |
-
cloned.lastCapturedPositions = this.lastCapturedPositions ? this.lastCapturedPositions.map((pos) => ({ ...pos })) : null;
|
| 506 |
-
if (this.gameResult) {
|
| 507 |
-
cloned.gameResult = {
|
| 508 |
-
...this.gameResult,
|
| 509 |
-
score: this.gameResult.score ? { ...this.gameResult.score } : void 0
|
| 510 |
-
};
|
| 511 |
-
}
|
| 512 |
-
cloned.invalidateAnalysisCache();
|
| 513 |
-
return cloned;
|
| 514 |
-
}
|
| 515 |
-
/**
|
| 516 |
-
* Get current board state (read-only)
|
| 517 |
-
*/
|
| 518 |
-
getBoard() {
|
| 519 |
-
return this.board.map((plane) => plane.map((row) => [...row]));
|
| 520 |
-
}
|
| 521 |
-
/**
|
| 522 |
-
* Get stone at specific position
|
| 523 |
-
* Equivalent to Game.stone() (lines 95-97)
|
| 524 |
-
*/
|
| 525 |
-
getStone(pos) {
|
| 526 |
-
return this.board[pos.x][pos.y][pos.z];
|
| 527 |
-
}
|
| 528 |
-
/**
|
| 529 |
-
* Get current player
|
| 530 |
-
*/
|
| 531 |
-
getCurrentPlayer() {
|
| 532 |
-
return this.currentPlayer;
|
| 533 |
-
}
|
| 534 |
-
/**
|
| 535 |
-
* Get current step number
|
| 536 |
-
* Equivalent to Game.currentStep() (lines 99-101)
|
| 537 |
-
*/
|
| 538 |
-
getCurrentStep() {
|
| 539 |
-
return this.currentStepIndex;
|
| 540 |
-
}
|
| 541 |
-
/**
|
| 542 |
-
* Get move history
|
| 543 |
-
* Equivalent to Game.routine() (lines 103-105)
|
| 544 |
-
*/
|
| 545 |
-
getHistory() {
|
| 546 |
-
return [...this.stepHistory];
|
| 547 |
-
}
|
| 548 |
-
/**
|
| 549 |
-
* Get last move
|
| 550 |
-
* Equivalent to Game.lastStep() (lines 107-110)
|
| 551 |
-
*/
|
| 552 |
-
getLastStep() {
|
| 553 |
-
if (this.currentStepIndex > 0) {
|
| 554 |
-
return this.stepHistory[this.currentStepIndex - 1];
|
| 555 |
-
}
|
| 556 |
-
return null;
|
| 557 |
-
}
|
| 558 |
-
/**
|
| 559 |
-
* Get board shape
|
| 560 |
-
* Equivalent to Game.shape() (lines 87-89)
|
| 561 |
-
*/
|
| 562 |
-
getShape() {
|
| 563 |
-
return { ...this.shape };
|
| 564 |
-
}
|
| 565 |
-
/**
|
| 566 |
-
* Get game status
|
| 567 |
-
*/
|
| 568 |
-
getGameStatus() {
|
| 569 |
-
return this.gameStatus;
|
| 570 |
-
}
|
| 571 |
-
/**
|
| 572 |
-
* Set game status
|
| 573 |
-
*/
|
| 574 |
-
setGameStatus(status) {
|
| 575 |
-
this.gameStatus = status;
|
| 576 |
-
}
|
| 577 |
-
/**
|
| 578 |
-
* Get game result
|
| 579 |
-
*/
|
| 580 |
-
getGameResult() {
|
| 581 |
-
return this.gameResult;
|
| 582 |
-
}
|
| 583 |
-
/**
|
| 584 |
-
* Get consecutive pass count
|
| 585 |
-
*/
|
| 586 |
-
getPassCount() {
|
| 587 |
-
return this.passCount;
|
| 588 |
-
}
|
| 589 |
-
/**
|
| 590 |
-
* Recalculate consecutive pass count based on current history
|
| 591 |
-
* Counts consecutive PASS steps from the end of current history
|
| 592 |
-
*/
|
| 593 |
-
recalculatePassCount() {
|
| 594 |
-
this.passCount = 0;
|
| 595 |
-
for (let i = this.currentStepIndex - 1; i >= 0; i--) {
|
| 596 |
-
if (this.stepHistory[i].type === 1 /* PASS */) {
|
| 597 |
-
this.passCount++;
|
| 598 |
-
} else {
|
| 599 |
-
break;
|
| 600 |
-
}
|
| 601 |
-
}
|
| 602 |
-
}
|
| 603 |
-
/**
|
| 604 |
-
* Start the game
|
| 605 |
-
*/
|
| 606 |
-
startGame() {
|
| 607 |
-
if (this.gameStatus === "idle") {
|
| 608 |
-
this.gameStatus = "playing";
|
| 609 |
-
}
|
| 610 |
-
}
|
| 611 |
-
/**
|
| 612 |
-
* Check if game is active
|
| 613 |
-
*/
|
| 614 |
-
isGameActive() {
|
| 615 |
-
return this.gameStatus === "playing";
|
| 616 |
-
}
|
| 617 |
-
/**
|
| 618 |
-
* Check if a move is valid
|
| 619 |
-
* Equivalent to Game.isDropable() and Game.isValidStep() (lines 112-151)
|
| 620 |
-
*/
|
| 621 |
-
isValidMove(pos, player) {
|
| 622 |
-
const playerColor = player || this.currentPlayer;
|
| 623 |
-
return validateMove(pos, playerColor, this.board, this.shape, this.lastCapturedPositions);
|
| 624 |
-
}
|
| 625 |
-
/**
|
| 626 |
-
* Get all valid move positions for current player (efficient batch query)
|
| 627 |
-
*
|
| 628 |
-
* This method is optimized to avoid repeated validation checks by:
|
| 629 |
-
* 1. Only checking empty positions
|
| 630 |
-
* 2. Skipping bounds checking (iterator is already within bounds)
|
| 631 |
-
* 3. Using low-level validation functions directly
|
| 632 |
-
* 4. Batching board state access
|
| 633 |
-
*
|
| 634 |
-
* @param player - Optional player color (defaults to current player)
|
| 635 |
-
* @returns Array of all valid move positions
|
| 636 |
-
*/
|
| 637 |
-
validMovePositions(player) {
|
| 638 |
-
const playerColor = player || this.currentPlayer;
|
| 639 |
-
const validPositions = [];
|
| 640 |
-
for (let x = 0; x < this.shape.x; x++) {
|
| 641 |
-
for (let y = 0; y < this.shape.y; y++) {
|
| 642 |
-
for (let z = 0; z < this.shape.z; z++) {
|
| 643 |
-
if (this.board[x][y][z] !== StoneType.EMPTY) {
|
| 644 |
-
continue;
|
| 645 |
-
}
|
| 646 |
-
const pos = { x, y, z };
|
| 647 |
-
if (isKoViolation(
|
| 648 |
-
pos,
|
| 649 |
-
playerColor,
|
| 650 |
-
this.board,
|
| 651 |
-
this.shape,
|
| 652 |
-
this.lastCapturedPositions
|
| 653 |
-
)) {
|
| 654 |
-
continue;
|
| 655 |
-
}
|
| 656 |
-
if (isSuicideMove(pos, playerColor, this.board, this.shape)) {
|
| 657 |
-
continue;
|
| 658 |
-
}
|
| 659 |
-
validPositions.push(pos);
|
| 660 |
-
}
|
| 661 |
-
}
|
| 662 |
-
}
|
| 663 |
-
return validPositions;
|
| 664 |
-
}
|
| 665 |
-
/**
|
| 666 |
-
* Check if any valid move can capture enemy stones
|
| 667 |
-
* Used by MCTS to determine if a position is truly terminal
|
| 668 |
-
*
|
| 669 |
-
* Results are cached and invalidated when board state changes.
|
| 670 |
-
*
|
| 671 |
-
* @param player - Optional player color (defaults to current player)
|
| 672 |
-
* @returns true if at least one valid move would capture stones
|
| 673 |
-
*/
|
| 674 |
-
hasCapturingMove(player) {
|
| 675 |
-
const playerColor = player ?? this.currentPlayer;
|
| 676 |
-
if (this.cachedCapturingMove.has(playerColor)) {
|
| 677 |
-
return this.cachedCapturingMove.get(playerColor);
|
| 678 |
-
}
|
| 679 |
-
const result = this.computeHasCapturingMove(playerColor);
|
| 680 |
-
this.cachedCapturingMove.set(playerColor, result);
|
| 681 |
-
return result;
|
| 682 |
-
}
|
| 683 |
-
/**
|
| 684 |
-
* Internal: Compute whether a player has any capturing move
|
| 685 |
-
*/
|
| 686 |
-
computeHasCapturingMove(playerColor) {
|
| 687 |
-
for (let x = 0; x < this.shape.x; x++) {
|
| 688 |
-
for (let y = 0; y < this.shape.y; y++) {
|
| 689 |
-
for (let z = 0; z < this.shape.z; z++) {
|
| 690 |
-
if (this.board[x][y][z] !== StoneType.EMPTY) {
|
| 691 |
-
continue;
|
| 692 |
-
}
|
| 693 |
-
const pos = { x, y, z };
|
| 694 |
-
if (isKoViolation(
|
| 695 |
-
pos,
|
| 696 |
-
playerColor,
|
| 697 |
-
this.board,
|
| 698 |
-
this.shape,
|
| 699 |
-
this.lastCapturedPositions
|
| 700 |
-
)) {
|
| 701 |
-
continue;
|
| 702 |
-
}
|
| 703 |
-
if (isSuicideMove(pos, playerColor, this.board, this.shape)) {
|
| 704 |
-
continue;
|
| 705 |
-
}
|
| 706 |
-
const capturedGroups = findCapturedGroups(pos, playerColor, this.board, this.shape);
|
| 707 |
-
if (capturedGroups.length > 0) {
|
| 708 |
-
return true;
|
| 709 |
-
}
|
| 710 |
-
}
|
| 711 |
-
}
|
| 712 |
-
}
|
| 713 |
-
return false;
|
| 714 |
-
}
|
| 715 |
-
/**
|
| 716 |
-
* Check if the game has reached a natural terminal state
|
| 717 |
-
*
|
| 718 |
-
* A game is naturally terminal when:
|
| 719 |
-
* - All territory is claimed (neutral === 0)
|
| 720 |
-
* - AND neither player has any capturing moves available
|
| 721 |
-
*
|
| 722 |
-
* This is different from the "finished" status which requires double pass.
|
| 723 |
-
* Natural termination means the game state is completely settled and
|
| 724 |
-
* no further moves can meaningfully change the outcome.
|
| 725 |
-
*
|
| 726 |
-
* NOTE: This method is expensive due to territory calculation and capture move checking.
|
| 727 |
-
* Use coverage ratio check as a pre-filter when calling frequently.
|
| 728 |
-
*
|
| 729 |
-
* Used by:
|
| 730 |
-
* - MCTS agent (terminal detection for tree search)
|
| 731 |
-
* - Model battle (early stopping when settled)
|
| 732 |
-
* - Self-play games (early stopping when settled)
|
| 733 |
-
*
|
| 734 |
-
* @returns true if the game is naturally terminal, false otherwise
|
| 735 |
-
*/
|
| 736 |
-
isNaturallyTerminal() {
|
| 737 |
-
const territory = this.getTerritory();
|
| 738 |
-
if (territory.neutral !== 0) {
|
| 739 |
-
return false;
|
| 740 |
-
}
|
| 741 |
-
const blackCanCapture = this.hasCapturingMove(StoneType.BLACK);
|
| 742 |
-
const whiteCanCapture = this.hasCapturingMove(StoneType.WHITE);
|
| 743 |
-
return !blackCanCapture && !whiteCanCapture;
|
| 744 |
-
}
|
| 745 |
-
/**
|
| 746 |
-
* Reset pass count (called when a stone is placed)
|
| 747 |
-
*/
|
| 748 |
-
resetPassCount() {
|
| 749 |
-
this.passCount = 0;
|
| 750 |
-
}
|
| 751 |
-
/**
|
| 752 |
-
* Place a stone (drop move)
|
| 753 |
-
* Equivalent to Game.drop() and Game.appendStone() (lines 181-273)
|
| 754 |
-
*
|
| 755 |
-
* @returns true if move was successful, false otherwise
|
| 756 |
-
*/
|
| 757 |
-
drop(pos) {
|
| 758 |
-
const validation = this.isValidMove(pos);
|
| 759 |
-
if (!validation.valid) {
|
| 760 |
-
console.warn(`Invalid move at (${pos.x}, ${pos.y}, ${pos.z}): ${validation.reason}`);
|
| 761 |
-
return false;
|
| 762 |
-
}
|
| 763 |
-
const capturedGroups = findCapturedGroups(pos, this.currentPlayer, this.board, this.shape);
|
| 764 |
-
this.board[pos.x][pos.y][pos.z] = this.currentPlayer;
|
| 765 |
-
const capturedPositions = executeCaptures(capturedGroups, this.board);
|
| 766 |
-
this.lastCapturedPositions = capturedPositions.length > 0 ? capturedPositions : null;
|
| 767 |
-
this.invalidateAnalysisCache();
|
| 768 |
-
this.resetPassCount();
|
| 769 |
-
const step = {
|
| 770 |
-
type: 0 /* DROP */,
|
| 771 |
-
position: pos,
|
| 772 |
-
player: this.currentPlayer,
|
| 773 |
-
capturedPositions: capturedPositions.length > 0 ? capturedPositions : void 0,
|
| 774 |
-
timestamp: Date.now()
|
| 775 |
-
};
|
| 776 |
-
this.advanceStep(step);
|
| 777 |
-
if (capturedPositions.length > 0 && this.callbacks.onCapture) {
|
| 778 |
-
this.callbacks.onCapture(capturedPositions);
|
| 779 |
-
}
|
| 780 |
-
if (this.callbacks.onTerritoryChange) {
|
| 781 |
-
this.callbacks.onTerritoryChange(this.getTerritory());
|
| 782 |
-
}
|
| 783 |
-
return true;
|
| 784 |
-
}
|
| 785 |
-
/**
|
| 786 |
-
* Pass turn
|
| 787 |
-
* Equivalent to PASS step type in prototype
|
| 788 |
-
*/
|
| 789 |
-
pass() {
|
| 790 |
-
const step = {
|
| 791 |
-
type: 1 /* PASS */,
|
| 792 |
-
player: this.currentPlayer,
|
| 793 |
-
timestamp: Date.now()
|
| 794 |
-
};
|
| 795 |
-
this.lastCapturedPositions = null;
|
| 796 |
-
this.passCount++;
|
| 797 |
-
this.advanceStep(step);
|
| 798 |
-
if (this.passCount >= 2) {
|
| 799 |
-
const territory = this.getTerritory();
|
| 800 |
-
const capturedCounts = this.getCapturedCounts();
|
| 801 |
-
const blackTotal = territory.black + capturedCounts.white;
|
| 802 |
-
const whiteTotal = territory.white + capturedCounts.black;
|
| 803 |
-
let winner;
|
| 804 |
-
if (blackTotal > whiteTotal) {
|
| 805 |
-
winner = "black";
|
| 806 |
-
} else if (whiteTotal > blackTotal) {
|
| 807 |
-
winner = "white";
|
| 808 |
-
} else {
|
| 809 |
-
winner = "draw";
|
| 810 |
-
}
|
| 811 |
-
this.gameResult = {
|
| 812 |
-
winner,
|
| 813 |
-
reason: "double-pass",
|
| 814 |
-
score: territory
|
| 815 |
-
};
|
| 816 |
-
this.gameStatus = "finished";
|
| 817 |
-
if (this.callbacks.onWin) {
|
| 818 |
-
const winnerStone = winner === "black" ? StoneType.BLACK : winner === "white" ? StoneType.WHITE : StoneType.EMPTY;
|
| 819 |
-
this.callbacks.onWin(winnerStone);
|
| 820 |
-
}
|
| 821 |
-
}
|
| 822 |
-
return true;
|
| 823 |
-
}
|
| 824 |
-
/**
|
| 825 |
-
* Surrender/resign
|
| 826 |
-
* Equivalent to Game.step() with SURRENDER type (lines 176-178)
|
| 827 |
-
*/
|
| 828 |
-
surrender() {
|
| 829 |
-
const surrenderingPlayer = this.currentPlayer;
|
| 830 |
-
const step = {
|
| 831 |
-
type: 2 /* SURRENDER */,
|
| 832 |
-
player: this.currentPlayer,
|
| 833 |
-
timestamp: Date.now()
|
| 834 |
-
};
|
| 835 |
-
this.advanceStep(step);
|
| 836 |
-
const winner = surrenderingPlayer === StoneType.BLACK ? "white" : "black";
|
| 837 |
-
this.gameResult = {
|
| 838 |
-
winner,
|
| 839 |
-
reason: "resignation"
|
| 840 |
-
};
|
| 841 |
-
this.gameStatus = "finished";
|
| 842 |
-
const winnerStone = getEnemyColor(surrenderingPlayer);
|
| 843 |
-
if (this.callbacks.onWin) {
|
| 844 |
-
this.callbacks.onWin(winnerStone);
|
| 845 |
-
}
|
| 846 |
-
return true;
|
| 847 |
-
}
|
| 848 |
-
/**
|
| 849 |
-
* Undo last move
|
| 850 |
-
* Equivalent to Game.repent() (lines 197-230)
|
| 851 |
-
*
|
| 852 |
-
* @returns true if undo was successful, false if no moves to undo
|
| 853 |
-
*/
|
| 854 |
-
undo() {
|
| 855 |
-
if (this.currentStepIndex === 0 || this.stepHistory.length === 0) {
|
| 856 |
-
return false;
|
| 857 |
-
}
|
| 858 |
-
const lastStep = this.stepHistory[this.currentStepIndex - 1];
|
| 859 |
-
if (lastStep.type === 0 /* DROP */ && lastStep.position) {
|
| 860 |
-
this.board[lastStep.position.x][lastStep.position.y][lastStep.position.z] = StoneType.EMPTY;
|
| 861 |
-
if (lastStep.capturedPositions) {
|
| 862 |
-
const enemyColor = getEnemyColor(lastStep.player);
|
| 863 |
-
for (const pos of lastStep.capturedPositions) {
|
| 864 |
-
this.board[pos.x][pos.y][pos.z] = enemyColor;
|
| 865 |
-
}
|
| 866 |
-
}
|
| 867 |
-
}
|
| 868 |
-
this.currentStepIndex--;
|
| 869 |
-
this.currentPlayer = lastStep.player;
|
| 870 |
-
this.recalculatePassCount();
|
| 871 |
-
if (this.currentStepIndex > 0) {
|
| 872 |
-
const previousStep = this.stepHistory[this.currentStepIndex - 1];
|
| 873 |
-
this.lastCapturedPositions = previousStep.capturedPositions || null;
|
| 874 |
-
} else {
|
| 875 |
-
this.lastCapturedPositions = null;
|
| 876 |
-
}
|
| 877 |
-
this.invalidateAnalysisCache();
|
| 878 |
-
if (this.callbacks.onStepBack) {
|
| 879 |
-
this.callbacks.onStepBack(lastStep, this.stepHistory.slice(0, this.currentStepIndex));
|
| 880 |
-
}
|
| 881 |
-
return true;
|
| 882 |
-
}
|
| 883 |
-
/**
|
| 884 |
-
* Redo next move (after undo)
|
| 885 |
-
*
|
| 886 |
-
* @returns true if redo was successful, false if no moves to redo
|
| 887 |
-
*/
|
| 888 |
-
redo() {
|
| 889 |
-
if (this.currentStepIndex >= this.stepHistory.length) {
|
| 890 |
-
return false;
|
| 891 |
-
}
|
| 892 |
-
const nextStep = this.stepHistory[this.currentStepIndex];
|
| 893 |
-
if (nextStep.type === 0 /* DROP */ && nextStep.position) {
|
| 894 |
-
this.board[nextStep.position.x][nextStep.position.y][nextStep.position.z] = nextStep.player;
|
| 895 |
-
if (nextStep.capturedPositions) {
|
| 896 |
-
for (const pos of nextStep.capturedPositions) {
|
| 897 |
-
this.board[pos.x][pos.y][pos.z] = StoneType.EMPTY;
|
| 898 |
-
}
|
| 899 |
-
}
|
| 900 |
-
this.lastCapturedPositions = nextStep.capturedPositions || null;
|
| 901 |
-
} else if (nextStep.type === 1 /* PASS */) {
|
| 902 |
-
this.lastCapturedPositions = null;
|
| 903 |
-
}
|
| 904 |
-
this.currentStepIndex++;
|
| 905 |
-
this.currentPlayer = getEnemyColor(nextStep.player);
|
| 906 |
-
this.invalidateAnalysisCache();
|
| 907 |
-
if (this.callbacks.onStepAdvance) {
|
| 908 |
-
this.callbacks.onStepAdvance(
|
| 909 |
-
nextStep,
|
| 910 |
-
this.stepHistory.slice(0, this.currentStepIndex)
|
| 911 |
-
);
|
| 912 |
-
}
|
| 913 |
-
return true;
|
| 914 |
-
}
|
| 915 |
-
/**
|
| 916 |
-
* Check if redo is available
|
| 917 |
-
*/
|
| 918 |
-
canRedo() {
|
| 919 |
-
return this.currentStepIndex < this.stepHistory.length;
|
| 920 |
-
}
|
| 921 |
-
/**
|
| 922 |
-
* Jump to specific step in history
|
| 923 |
-
* Rebuilds board state after applying the first 'index' moves
|
| 924 |
-
*
|
| 925 |
-
* @param index Number of moves to apply from history (0 for initial state, 1 for after first move, etc.)
|
| 926 |
-
* @returns true if jump was successful
|
| 927 |
-
*/
|
| 928 |
-
jumpToStep(index) {
|
| 929 |
-
if (index < 0 || index > this.stepHistory.length) {
|
| 930 |
-
return false;
|
| 931 |
-
}
|
| 932 |
-
if (index === this.currentStepIndex) {
|
| 933 |
-
return false;
|
| 934 |
-
}
|
| 935 |
-
this.board = this.createEmptyBoard();
|
| 936 |
-
this.lastCapturedPositions = null;
|
| 937 |
-
for (let i = 0; i < index; i++) {
|
| 938 |
-
const step = this.stepHistory[i];
|
| 939 |
-
if (step.type === 0 /* DROP */ && step.position) {
|
| 940 |
-
const pos = step.position;
|
| 941 |
-
this.board[pos.x][pos.y][pos.z] = step.player;
|
| 942 |
-
if (step.capturedPositions) {
|
| 943 |
-
for (const capturedPos of step.capturedPositions) {
|
| 944 |
-
this.board[capturedPos.x][capturedPos.y][capturedPos.z] = StoneType.EMPTY;
|
| 945 |
-
}
|
| 946 |
-
}
|
| 947 |
-
}
|
| 948 |
-
}
|
| 949 |
-
if (index > 0) {
|
| 950 |
-
const lastAppliedStep = this.stepHistory[index - 1];
|
| 951 |
-
if (lastAppliedStep.type === 0 /* DROP */) {
|
| 952 |
-
this.lastCapturedPositions = lastAppliedStep.capturedPositions || null;
|
| 953 |
-
} else if (lastAppliedStep.type === 1 /* PASS */) {
|
| 954 |
-
this.lastCapturedPositions = null;
|
| 955 |
-
}
|
| 956 |
-
} else {
|
| 957 |
-
this.lastCapturedPositions = null;
|
| 958 |
-
}
|
| 959 |
-
const oldStepIndex = this.currentStepIndex;
|
| 960 |
-
this.currentStepIndex = index;
|
| 961 |
-
const movesPlayed = index;
|
| 962 |
-
this.currentPlayer = movesPlayed % 2 === 0 ? StoneType.BLACK : StoneType.WHITE;
|
| 963 |
-
this.recalculatePassCount();
|
| 964 |
-
this.invalidateAnalysisCache();
|
| 965 |
-
if (index < oldStepIndex && this.callbacks.onStepBack) {
|
| 966 |
-
const currentStep = this.stepHistory[index];
|
| 967 |
-
this.callbacks.onStepBack(currentStep, this.stepHistory.slice(0, index + 1));
|
| 968 |
-
} else if (index > oldStepIndex && this.callbacks.onStepAdvance) {
|
| 969 |
-
const currentStep = this.stepHistory[index];
|
| 970 |
-
this.callbacks.onStepAdvance(currentStep, this.stepHistory.slice(0, index + 1));
|
| 971 |
-
}
|
| 972 |
-
return true;
|
| 973 |
-
}
|
| 974 |
-
/**
|
| 975 |
-
* Advance to next step
|
| 976 |
-
* Equivalent to Game.stepAdvance() (lines 279-287)
|
| 977 |
-
*/
|
| 978 |
-
advanceStep(step) {
|
| 979 |
-
if (this.currentStepIndex < this.stepHistory.length) {
|
| 980 |
-
this.stepHistory = this.stepHistory.slice(0, this.currentStepIndex);
|
| 981 |
-
}
|
| 982 |
-
this.stepHistory.push(step);
|
| 983 |
-
this.currentStepIndex++;
|
| 984 |
-
this.currentPlayer = getEnemyColor(this.currentPlayer);
|
| 985 |
-
if (this.callbacks.onStepAdvance) {
|
| 986 |
-
this.callbacks.onStepAdvance(step, this.stepHistory);
|
| 987 |
-
}
|
| 988 |
-
}
|
| 989 |
-
/**
|
| 990 |
-
* Get territory calculation
|
| 991 |
-
* Equivalent to Game.blackDomain() and Game.whiteDomain() (lines 232-244)
|
| 992 |
-
*
|
| 993 |
-
* Returns cached result if territory hasn't changed
|
| 994 |
-
*/
|
| 995 |
-
getTerritory() {
|
| 996 |
-
if (!this.cachedTerritory)
|
| 997 |
-
this.cachedTerritory = calculateTerritory(this.board, this.shape);
|
| 998 |
-
return this.cachedTerritory;
|
| 999 |
-
}
|
| 1000 |
-
/**
|
| 1001 |
-
* Get captured stone counts up to current position in history
|
| 1002 |
-
* Only counts captures that have been played (up to currentStepIndex)
|
| 1003 |
-
*/
|
| 1004 |
-
getCapturedCounts() {
|
| 1005 |
-
const counts = { black: 0, white: 0 };
|
| 1006 |
-
for (let i = 0; i < this.currentStepIndex; i++) {
|
| 1007 |
-
const step = this.stepHistory[i];
|
| 1008 |
-
if (step.capturedPositions && step.capturedPositions.length > 0) {
|
| 1009 |
-
const enemyColor = getEnemyColor(step.player);
|
| 1010 |
-
if (enemyColor === StoneType.BLACK) {
|
| 1011 |
-
counts.black += step.capturedPositions.length;
|
| 1012 |
-
} else if (enemyColor === StoneType.WHITE) {
|
| 1013 |
-
counts.white += step.capturedPositions.length;
|
| 1014 |
-
}
|
| 1015 |
-
}
|
| 1016 |
-
}
|
| 1017 |
-
return counts;
|
| 1018 |
-
}
|
| 1019 |
-
/**
|
| 1020 |
-
* Serialize game state to JSON
|
| 1021 |
-
* Equivalent to Game.serialize() (lines 250-252)
|
| 1022 |
-
*/
|
| 1023 |
-
toJSON() {
|
| 1024 |
-
return {
|
| 1025 |
-
shape: this.shape,
|
| 1026 |
-
currentPlayer: this.currentPlayer,
|
| 1027 |
-
currentStepIndex: this.currentStepIndex,
|
| 1028 |
-
history: this.stepHistory,
|
| 1029 |
-
board: this.board,
|
| 1030 |
-
gameStatus: this.gameStatus,
|
| 1031 |
-
gameResult: this.gameResult,
|
| 1032 |
-
passCount: this.passCount
|
| 1033 |
-
};
|
| 1034 |
-
}
|
| 1035 |
-
/**
|
| 1036 |
-
* Load game state from JSON
|
| 1037 |
-
*/
|
| 1038 |
-
fromJSON(data) {
|
| 1039 |
-
try {
|
| 1040 |
-
if (!data || typeof data !== "object") {
|
| 1041 |
-
return false;
|
| 1042 |
-
}
|
| 1043 |
-
if (!data.shape || !data.board || !Array.isArray(data.history)) {
|
| 1044 |
-
return false;
|
| 1045 |
-
}
|
| 1046 |
-
this.shape = data.shape;
|
| 1047 |
-
this.currentPlayer = data.currentPlayer;
|
| 1048 |
-
this.currentStepIndex = data.currentStepIndex;
|
| 1049 |
-
this.stepHistory = data.history || [];
|
| 1050 |
-
this.board = data.board;
|
| 1051 |
-
this.gameStatus = data.gameStatus || "idle";
|
| 1052 |
-
this.gameResult = data.gameResult;
|
| 1053 |
-
this.passCount = data.passCount || 0;
|
| 1054 |
-
if (this.currentStepIndex > 0) {
|
| 1055 |
-
const lastStep = this.stepHistory[this.currentStepIndex - 1];
|
| 1056 |
-
this.lastCapturedPositions = lastStep.capturedPositions || null;
|
| 1057 |
-
} else {
|
| 1058 |
-
this.lastCapturedPositions = null;
|
| 1059 |
-
}
|
| 1060 |
-
this.invalidateAnalysisCache();
|
| 1061 |
-
return true;
|
| 1062 |
-
} catch (error) {
|
| 1063 |
-
console.error("Failed to load game state:", error);
|
| 1064 |
-
return false;
|
| 1065 |
-
}
|
| 1066 |
-
}
|
| 1067 |
-
/**
|
| 1068 |
-
* Get game statistics
|
| 1069 |
-
*/
|
| 1070 |
-
getStats() {
|
| 1071 |
-
const captured = this.getCapturedCounts();
|
| 1072 |
-
const territory = this.getTerritory();
|
| 1073 |
-
let blackMoves = 0;
|
| 1074 |
-
let whiteMoves = 0;
|
| 1075 |
-
for (const step of this.stepHistory.slice(0, this.currentStepIndex)) {
|
| 1076 |
-
if (step.type === 0 /* DROP */) {
|
| 1077 |
-
if (step.player === StoneType.BLACK) {
|
| 1078 |
-
blackMoves++;
|
| 1079 |
-
} else if (step.player === StoneType.WHITE) {
|
| 1080 |
-
whiteMoves++;
|
| 1081 |
-
}
|
| 1082 |
-
}
|
| 1083 |
-
}
|
| 1084 |
-
return {
|
| 1085 |
-
totalMoves: this.currentStepIndex,
|
| 1086 |
-
blackMoves,
|
| 1087 |
-
whiteMoves,
|
| 1088 |
-
capturedByBlack: captured.white,
|
| 1089 |
-
// Black captures white stones
|
| 1090 |
-
capturedByWhite: captured.black,
|
| 1091 |
-
// White captures black stones
|
| 1092 |
-
territory
|
| 1093 |
-
};
|
| 1094 |
-
}
|
| 1095 |
-
/**
|
| 1096 |
-
* Save game state to sessionStorage
|
| 1097 |
-
*
|
| 1098 |
-
* @param key Storage key (default: "trigoGameState")
|
| 1099 |
-
* @returns true if save was successful
|
| 1100 |
-
*/
|
| 1101 |
-
saveToSessionStorage(key = "trigoGameState") {
|
| 1102 |
-
if (typeof globalThis !== "undefined" && globalThis.sessionStorage) {
|
| 1103 |
-
try {
|
| 1104 |
-
const gameState = this.toJSON();
|
| 1105 |
-
globalThis.sessionStorage.setItem(key, JSON.stringify(gameState));
|
| 1106 |
-
return true;
|
| 1107 |
-
} catch (error) {
|
| 1108 |
-
console.error("Failed to save game state to sessionStorage:", error);
|
| 1109 |
-
return false;
|
| 1110 |
-
}
|
| 1111 |
-
}
|
| 1112 |
-
console.warn("sessionStorage is not available");
|
| 1113 |
-
return false;
|
| 1114 |
-
}
|
| 1115 |
-
/**
|
| 1116 |
-
* Load game state from sessionStorage
|
| 1117 |
-
*
|
| 1118 |
-
* @param key Storage key (default: "trigoGameState")
|
| 1119 |
-
* @returns true if load was successful
|
| 1120 |
-
*/
|
| 1121 |
-
loadFromSessionStorage(key = "trigoGameState") {
|
| 1122 |
-
if (typeof globalThis !== "undefined" && globalThis.sessionStorage) {
|
| 1123 |
-
try {
|
| 1124 |
-
const savedState = globalThis.sessionStorage.getItem(key);
|
| 1125 |
-
if (!savedState) {
|
| 1126 |
-
console.log("No saved game state found");
|
| 1127 |
-
return false;
|
| 1128 |
-
}
|
| 1129 |
-
const data = JSON.parse(savedState);
|
| 1130 |
-
return this.fromJSON(data);
|
| 1131 |
-
} catch (error) {
|
| 1132 |
-
console.error("Failed to load game state from sessionStorage:", error);
|
| 1133 |
-
return false;
|
| 1134 |
-
}
|
| 1135 |
-
}
|
| 1136 |
-
console.warn("sessionStorage is not available");
|
| 1137 |
-
return false;
|
| 1138 |
-
}
|
| 1139 |
-
/**
|
| 1140 |
-
* Clear saved game state from sessionStorage
|
| 1141 |
-
*
|
| 1142 |
-
* @param key Storage key (default: "trigoGameState")
|
| 1143 |
-
*/
|
| 1144 |
-
clearSessionStorage(key = "trigoGameState") {
|
| 1145 |
-
if (typeof globalThis !== "undefined" && globalThis.sessionStorage) {
|
| 1146 |
-
try {
|
| 1147 |
-
globalThis.sessionStorage.removeItem(key);
|
| 1148 |
-
} catch (error) {
|
| 1149 |
-
console.error("Failed to clear sessionStorage:", error);
|
| 1150 |
-
}
|
| 1151 |
-
} else {
|
| 1152 |
-
console.warn("sessionStorage is not available");
|
| 1153 |
-
}
|
| 1154 |
-
}
|
| 1155 |
-
/**
|
| 1156 |
-
* Export game to TGN (Trigo Game Notation) format
|
| 1157 |
-
*
|
| 1158 |
-
* TGN format is similar to PGN (Portable Game Notation) for chess.
|
| 1159 |
-
* It includes metadata tags and move sequence using ab0yz coordinate notation.
|
| 1160 |
-
*
|
| 1161 |
-
* @param metadata Optional metadata for the game (Event, Site, Date, Players, etc.)
|
| 1162 |
-
* @returns TGN-formatted string
|
| 1163 |
-
*
|
| 1164 |
-
* @example
|
| 1165 |
-
* const tgn = game.toTGN({
|
| 1166 |
-
* event: "World Championship",
|
| 1167 |
-
* site: "Tokyo",
|
| 1168 |
-
* date: "2025.10.31",
|
| 1169 |
-
* black: "Alice",
|
| 1170 |
-
* white: "Bob"
|
| 1171 |
-
* });
|
| 1172 |
-
*/
|
| 1173 |
-
toTGN(metadata, { markResult } = {}) {
|
| 1174 |
-
const lines = [];
|
| 1175 |
-
if (metadata) {
|
| 1176 |
-
if (metadata.event) lines.push(`[Event "${metadata.event}"]`);
|
| 1177 |
-
if (metadata.site) lines.push(`[Site "${metadata.site}"]`);
|
| 1178 |
-
if (metadata.date) lines.push(`[Date "${metadata.date}"]`);
|
| 1179 |
-
if (metadata.round) lines.push(`[Round "${metadata.round}"]`);
|
| 1180 |
-
if (metadata.black) lines.push(`[Black "${metadata.black}"]`);
|
| 1181 |
-
if (metadata.white) lines.push(`[White "${metadata.white}"]`);
|
| 1182 |
-
}
|
| 1183 |
-
if (this.gameStatus === "finished" && this.gameResult) {
|
| 1184 |
-
let resultStr = "";
|
| 1185 |
-
if (this.gameResult.winner === "black") {
|
| 1186 |
-
resultStr = "B+";
|
| 1187 |
-
} else if (this.gameResult.winner === "white") {
|
| 1188 |
-
resultStr = "W+";
|
| 1189 |
-
} else {
|
| 1190 |
-
resultStr = "=";
|
| 1191 |
-
}
|
| 1192 |
-
if (this.gameResult.score) {
|
| 1193 |
-
const { black, white } = this.gameResult.score;
|
| 1194 |
-
const diff = Math.abs(black - white);
|
| 1195 |
-
resultStr += `${diff}points`;
|
| 1196 |
-
} else if (this.gameResult.reason === "resignation") {
|
| 1197 |
-
resultStr += "Resign";
|
| 1198 |
-
}
|
| 1199 |
-
}
|
| 1200 |
-
const boardStr = this.shape.z === 1 ? `${this.shape.x}x${this.shape.y}` : `${this.shape.x}x${this.shape.y}x${this.shape.z}`;
|
| 1201 |
-
lines.push(`[Board ${boardStr}]`);
|
| 1202 |
-
if (metadata) {
|
| 1203 |
-
if (metadata.rules) lines.push(`[Rules "${metadata.rules}"]`);
|
| 1204 |
-
if (metadata.timeControl) lines.push(`[TimeControl "${metadata.timeControl}"]`);
|
| 1205 |
-
if (metadata.application) lines.push(`[Application "${metadata.application}"]`);
|
| 1206 |
-
}
|
| 1207 |
-
lines.push("");
|
| 1208 |
-
const moves = [];
|
| 1209 |
-
let moveNumber = 1;
|
| 1210 |
-
for (let i = 0; i < this.stepHistory.length; i++) {
|
| 1211 |
-
const step = this.stepHistory[i];
|
| 1212 |
-
let moveStr = "";
|
| 1213 |
-
if (step.player === StoneType.BLACK) {
|
| 1214 |
-
moveStr = `${moveNumber}. `;
|
| 1215 |
-
}
|
| 1216 |
-
if (step.type === 0 /* DROP */ && step.position) {
|
| 1217 |
-
const pos = [step.position.x, step.position.y, step.position.z];
|
| 1218 |
-
const boardShape = [this.shape.x, this.shape.y, this.shape.z];
|
| 1219 |
-
const coord = encodeAb0yz(pos, boardShape);
|
| 1220 |
-
moveStr += coord;
|
| 1221 |
-
} else if (step.type === 1 /* PASS */) {
|
| 1222 |
-
moveStr += "Pass";
|
| 1223 |
-
} else if (step.type === 2 /* SURRENDER */) {
|
| 1224 |
-
moveStr += "Resign";
|
| 1225 |
-
}
|
| 1226 |
-
moves.push(moveStr);
|
| 1227 |
-
if (step.player === StoneType.WHITE) {
|
| 1228 |
-
moveNumber++;
|
| 1229 |
-
}
|
| 1230 |
-
}
|
| 1231 |
-
let currentLine = "";
|
| 1232 |
-
for (let i = 0; i < moves.length; i++) {
|
| 1233 |
-
const move = moves[i];
|
| 1234 |
-
if (move.match(/^\d+\./)) {
|
| 1235 |
-
if (currentLine) {
|
| 1236 |
-
lines.push(currentLine);
|
| 1237 |
-
}
|
| 1238 |
-
currentLine = move;
|
| 1239 |
-
} else {
|
| 1240 |
-
currentLine += " " + move;
|
| 1241 |
-
}
|
| 1242 |
-
}
|
| 1243 |
-
if (currentLine) {
|
| 1244 |
-
lines.push(currentLine);
|
| 1245 |
-
}
|
| 1246 |
-
if (markResult) {
|
| 1247 |
-
const territory = this.getTerritory();
|
| 1248 |
-
const scoreDiff = territory.black - territory.white;
|
| 1249 |
-
lines.push(`; ${scoreDiff > 0 ? "-" : scoreDiff < 0 ? "+" : ""}${Math.abs(scoreDiff)}`);
|
| 1250 |
-
}
|
| 1251 |
-
lines.push("");
|
| 1252 |
-
return lines.join("\n");
|
| 1253 |
-
}
|
| 1254 |
-
/**
|
| 1255 |
-
* Import game from TGN (Trigo Game Notation) format
|
| 1256 |
-
*
|
| 1257 |
-
* Static factory method that parses a TGN string and creates a new TrigoGame instance
|
| 1258 |
-
* with the board configuration and moves from the TGN file.
|
| 1259 |
-
*
|
| 1260 |
-
* Synchronous operation - requires parser to be loaded via setParserModule()
|
| 1261 |
-
*
|
| 1262 |
-
* @param tgnString TGN-formatted game notation string
|
| 1263 |
-
* @param callbacks Optional game callbacks
|
| 1264 |
-
* @returns New TrigoGame instance with the imported game state
|
| 1265 |
-
* @throws TGNParseError if the TGN string is invalid
|
| 1266 |
-
*
|
| 1267 |
-
* @example
|
| 1268 |
-
* const tgnString = `
|
| 1269 |
-
* [Event "World Championship"]
|
| 1270 |
-
* [Board "5x5x5"]
|
| 1271 |
-
* [Black "Alice"]
|
| 1272 |
-
* [White "Bob"]
|
| 1273 |
-
*
|
| 1274 |
-
* 1. 000 y00
|
| 1275 |
-
* 2. 0y0 pass
|
| 1276 |
-
* `;
|
| 1277 |
-
* const game = TrigoGame.fromTGN(tgnString);
|
| 1278 |
-
*/
|
| 1279 |
-
static fromTGN(tgnString, callbacks) {
|
| 1280 |
-
const parsed = parseTGN(tgnString);
|
| 1281 |
-
let boardShape;
|
| 1282 |
-
if (parsed.tags.Board && Array.isArray(parsed.tags.Board)) {
|
| 1283 |
-
const shape = parsed.tags.Board;
|
| 1284 |
-
boardShape = {
|
| 1285 |
-
x: shape[0] || 5,
|
| 1286 |
-
y: shape[1] || 5,
|
| 1287 |
-
z: shape[2] || 1
|
| 1288 |
-
};
|
| 1289 |
-
} else {
|
| 1290 |
-
boardShape = { x: 5, y: 5, z: 5 };
|
| 1291 |
-
}
|
| 1292 |
-
const game = new this(boardShape, callbacks);
|
| 1293 |
-
game.startGame();
|
| 1294 |
-
if (parsed.moves && parsed.moves.length > 0) {
|
| 1295 |
-
for (const round of parsed.moves) {
|
| 1296 |
-
if (round.action_black) {
|
| 1297 |
-
game._applyParsedMove(round.action_black, boardShape);
|
| 1298 |
-
}
|
| 1299 |
-
if (round.action_white) {
|
| 1300 |
-
game._applyParsedMove(round.action_white, boardShape);
|
| 1301 |
-
}
|
| 1302 |
-
}
|
| 1303 |
-
}
|
| 1304 |
-
return game;
|
| 1305 |
-
}
|
| 1306 |
-
/**
|
| 1307 |
-
* Apply a parsed move action to the game
|
| 1308 |
-
* Private helper method for fromTGN
|
| 1309 |
-
*
|
| 1310 |
-
* @param action Parsed move action from TGN parser
|
| 1311 |
-
* @param boardShape Board dimensions for coordinate decoding
|
| 1312 |
-
*/
|
| 1313 |
-
_applyParsedMove(action, boardShape) {
|
| 1314 |
-
if (action.type === "pass") {
|
| 1315 |
-
this.pass();
|
| 1316 |
-
} else if (action.type === "resign") {
|
| 1317 |
-
this.surrender();
|
| 1318 |
-
} else if (action.type === "move" && action.position) {
|
| 1319 |
-
const coords = decodeAb0yz(action.position, [boardShape.x, boardShape.y, boardShape.z]);
|
| 1320 |
-
const position = {
|
| 1321 |
-
x: coords[0],
|
| 1322 |
-
y: coords[1],
|
| 1323 |
-
z: coords[2]
|
| 1324 |
-
};
|
| 1325 |
-
this.drop(position);
|
| 1326 |
-
}
|
| 1327 |
-
}
|
| 1328 |
-
};
|
| 1329 |
-
|
| 1330 |
-
// backend/src/services/gameManager.ts
|
| 1331 |
-
var GameManager = class {
|
| 1332 |
-
// Default 5x5x5 board
|
| 1333 |
-
constructor() {
|
| 1334 |
-
this.rooms = /* @__PURE__ */ new Map();
|
| 1335 |
-
this.playerRoomMap = /* @__PURE__ */ new Map();
|
| 1336 |
-
this.defaultBoardShape = { x: 5, y: 5, z: 5 };
|
| 1337 |
-
console.log("GameManager initialized");
|
| 1338 |
-
}
|
| 1339 |
-
createRoom(playerId, nickname, boardShape, preferredColor) {
|
| 1340 |
-
const roomId = this.generateRoomId();
|
| 1341 |
-
const shape = boardShape || this.defaultBoardShape;
|
| 1342 |
-
const playerColor = preferredColor || "black";
|
| 1343 |
-
const room = {
|
| 1344 |
-
id: roomId,
|
| 1345 |
-
adminId: playerId,
|
| 1346 |
-
// Room creator is admin
|
| 1347 |
-
players: {
|
| 1348 |
-
[playerId]: {
|
| 1349 |
-
id: playerId,
|
| 1350 |
-
nickname,
|
| 1351 |
-
color: playerColor,
|
| 1352 |
-
connected: true
|
| 1353 |
-
}
|
| 1354 |
-
},
|
| 1355 |
-
game: new TrigoGame(shape, {
|
| 1356 |
-
onStepAdvance: (_step, history) => {
|
| 1357 |
-
console.log(`Step ${history.length}: Player made move`);
|
| 1358 |
-
},
|
| 1359 |
-
onCapture: (captured) => {
|
| 1360 |
-
console.log(`Captured ${captured.length} stones`);
|
| 1361 |
-
},
|
| 1362 |
-
onWin: (winner) => {
|
| 1363 |
-
console.log(`Game won by ${winner}`);
|
| 1364 |
-
}
|
| 1365 |
-
}),
|
| 1366 |
-
gameState: {
|
| 1367 |
-
gameStatus: "waiting",
|
| 1368 |
-
winner: null
|
| 1369 |
-
},
|
| 1370 |
-
createdAt: /* @__PURE__ */ new Date(),
|
| 1371 |
-
startedAt: null
|
| 1372 |
-
};
|
| 1373 |
-
this.rooms.set(roomId, room);
|
| 1374 |
-
this.playerRoomMap.set(playerId, roomId);
|
| 1375 |
-
console.log(`Room ${roomId} created by ${playerId}`);
|
| 1376 |
-
return room;
|
| 1377 |
-
}
|
| 1378 |
-
joinRoom(roomId, playerId, nickname, preferredColor) {
|
| 1379 |
-
const room = this.rooms.get(roomId);
|
| 1380 |
-
if (!room) {
|
| 1381 |
-
return null;
|
| 1382 |
-
}
|
| 1383 |
-
const playerCount = Object.keys(room.players).length;
|
| 1384 |
-
if (playerCount >= 2) {
|
| 1385 |
-
return null;
|
| 1386 |
-
}
|
| 1387 |
-
const firstPlayer = Object.values(room.players)[0];
|
| 1388 |
-
let assignedColor;
|
| 1389 |
-
if (preferredColor && preferredColor !== firstPlayer.color) {
|
| 1390 |
-
assignedColor = preferredColor;
|
| 1391 |
-
} else {
|
| 1392 |
-
assignedColor = firstPlayer.color === "black" ? "white" : "black";
|
| 1393 |
-
}
|
| 1394 |
-
room.players[playerId] = {
|
| 1395 |
-
id: playerId,
|
| 1396 |
-
nickname,
|
| 1397 |
-
color: assignedColor,
|
| 1398 |
-
connected: true
|
| 1399 |
-
};
|
| 1400 |
-
this.playerRoomMap.set(playerId, roomId);
|
| 1401 |
-
if (playerCount === 1) {
|
| 1402 |
-
room.gameState.gameStatus = "playing";
|
| 1403 |
-
room.startedAt = /* @__PURE__ */ new Date();
|
| 1404 |
-
}
|
| 1405 |
-
return room;
|
| 1406 |
-
}
|
| 1407 |
-
leaveRoom(roomId, playerId) {
|
| 1408 |
-
const room = this.rooms.get(roomId);
|
| 1409 |
-
if (!room) return;
|
| 1410 |
-
if (room.players[playerId]) {
|
| 1411 |
-
room.players[playerId].connected = false;
|
| 1412 |
-
}
|
| 1413 |
-
this.playerRoomMap.delete(playerId);
|
| 1414 |
-
const connectedPlayers = Object.values(room.players).filter((p) => p.connected);
|
| 1415 |
-
if (connectedPlayers.length === 0) {
|
| 1416 |
-
this.rooms.delete(roomId);
|
| 1417 |
-
console.log(`Room ${roomId} deleted - no players remaining`);
|
| 1418 |
-
}
|
| 1419 |
-
}
|
| 1420 |
-
makeMove(roomId, playerId, move) {
|
| 1421 |
-
const room = this.rooms.get(roomId);
|
| 1422 |
-
if (!room) return false;
|
| 1423 |
-
const player = room.players[playerId];
|
| 1424 |
-
if (!player) return false;
|
| 1425 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1426 |
-
return false;
|
| 1427 |
-
}
|
| 1428 |
-
const expectedPlayer = player.color === "black" ? StoneType.BLACK : StoneType.WHITE;
|
| 1429 |
-
const currentPlayer = room.game.getCurrentPlayer();
|
| 1430 |
-
if (currentPlayer !== expectedPlayer) {
|
| 1431 |
-
return false;
|
| 1432 |
-
}
|
| 1433 |
-
const position = { x: move.x, y: move.y, z: move.z };
|
| 1434 |
-
const success = room.game.drop(position);
|
| 1435 |
-
return success;
|
| 1436 |
-
}
|
| 1437 |
-
passTurn(roomId, playerId) {
|
| 1438 |
-
const room = this.rooms.get(roomId);
|
| 1439 |
-
if (!room) return false;
|
| 1440 |
-
const player = room.players[playerId];
|
| 1441 |
-
if (!player) return false;
|
| 1442 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1443 |
-
return false;
|
| 1444 |
-
}
|
| 1445 |
-
const expectedPlayer = player.color === "black" ? StoneType.BLACK : StoneType.WHITE;
|
| 1446 |
-
const currentPlayer = room.game.getCurrentPlayer();
|
| 1447 |
-
if (currentPlayer !== expectedPlayer) {
|
| 1448 |
-
return false;
|
| 1449 |
-
}
|
| 1450 |
-
return room.game.pass();
|
| 1451 |
-
}
|
| 1452 |
-
resign(roomId, playerId) {
|
| 1453 |
-
const room = this.rooms.get(roomId);
|
| 1454 |
-
if (!room) return false;
|
| 1455 |
-
const player = room.players[playerId];
|
| 1456 |
-
if (!player) return false;
|
| 1457 |
-
room.game.surrender();
|
| 1458 |
-
room.gameState.gameStatus = "finished";
|
| 1459 |
-
room.gameState.winner = player.color === "black" ? "white" : "black";
|
| 1460 |
-
return true;
|
| 1461 |
-
}
|
| 1462 |
-
/**
|
| 1463 |
-
* Undo the last move (悔棋)
|
| 1464 |
-
*/
|
| 1465 |
-
undoMove(roomId, playerId) {
|
| 1466 |
-
const room = this.rooms.get(roomId);
|
| 1467 |
-
if (!room) return false;
|
| 1468 |
-
const player = room.players[playerId];
|
| 1469 |
-
if (!player) return false;
|
| 1470 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1471 |
-
return false;
|
| 1472 |
-
}
|
| 1473 |
-
return room.game.undo();
|
| 1474 |
-
}
|
| 1475 |
-
/**
|
| 1476 |
-
* Redo the last undone move (forward in history)
|
| 1477 |
-
*/
|
| 1478 |
-
redoMove(roomId, playerId) {
|
| 1479 |
-
const room = this.rooms.get(roomId);
|
| 1480 |
-
if (!room) return false;
|
| 1481 |
-
const player = room.players[playerId];
|
| 1482 |
-
if (!player) return false;
|
| 1483 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1484 |
-
return false;
|
| 1485 |
-
}
|
| 1486 |
-
return room.game.redo();
|
| 1487 |
-
}
|
| 1488 |
-
/**
|
| 1489 |
-
* Reset the game to initial state (new game in same room)
|
| 1490 |
-
* Only admin can reset the game
|
| 1491 |
-
*/
|
| 1492 |
-
resetGame(roomId, adminId, options) {
|
| 1493 |
-
const room = this.rooms.get(roomId);
|
| 1494 |
-
if (!room) return { success: false, error: "Room not found" };
|
| 1495 |
-
if (room.adminId !== adminId) {
|
| 1496 |
-
return { success: false, error: "Only room admin can reset the game" };
|
| 1497 |
-
}
|
| 1498 |
-
const boardShape = options?.boardShape;
|
| 1499 |
-
const playerColors = options?.playerColors;
|
| 1500 |
-
if (playerColors) {
|
| 1501 |
-
const playerIds = Object.keys(room.players);
|
| 1502 |
-
for (const playerId of playerIds) {
|
| 1503 |
-
if (playerColors[playerId]) {
|
| 1504 |
-
room.players[playerId].color = playerColors[playerId];
|
| 1505 |
-
}
|
| 1506 |
-
}
|
| 1507 |
-
console.log(`Player colors assigned:`, playerColors);
|
| 1508 |
-
}
|
| 1509 |
-
if (boardShape) {
|
| 1510 |
-
const currentShape = room.game.getShape();
|
| 1511 |
-
if (boardShape.x !== currentShape.x || boardShape.y !== currentShape.y || boardShape.z !== currentShape.z) {
|
| 1512 |
-
room.game = new TrigoGame(boardShape, {
|
| 1513 |
-
onStepAdvance: (_step, history) => {
|
| 1514 |
-
console.log(`Step ${history.length}: Player made move`);
|
| 1515 |
-
},
|
| 1516 |
-
onCapture: (captured) => {
|
| 1517 |
-
console.log(`Captured ${captured.length} stones`);
|
| 1518 |
-
},
|
| 1519 |
-
onWin: (winner) => {
|
| 1520 |
-
console.log(`Game won by ${winner}`);
|
| 1521 |
-
}
|
| 1522 |
-
});
|
| 1523 |
-
console.log(`Game ${roomId} reset with new board shape: ${boardShape.x}x${boardShape.y}x${boardShape.z}`);
|
| 1524 |
-
} else {
|
| 1525 |
-
room.game.reset();
|
| 1526 |
-
console.log(`Game ${roomId} reset to initial state`);
|
| 1527 |
-
}
|
| 1528 |
-
} else {
|
| 1529 |
-
room.game.reset();
|
| 1530 |
-
console.log(`Game ${roomId} reset to initial state`);
|
| 1531 |
-
}
|
| 1532 |
-
room.gameState.gameStatus = "playing";
|
| 1533 |
-
room.gameState.winner = null;
|
| 1534 |
-
room.startedAt = /* @__PURE__ */ new Date();
|
| 1535 |
-
return { success: true };
|
| 1536 |
-
}
|
| 1537 |
-
/**
|
| 1538 |
-
* Get game board state for a room
|
| 1539 |
-
*/
|
| 1540 |
-
getGameBoard(roomId) {
|
| 1541 |
-
const room = this.rooms.get(roomId);
|
| 1542 |
-
if (!room) return null;
|
| 1543 |
-
return room.game.getBoard();
|
| 1544 |
-
}
|
| 1545 |
-
/**
|
| 1546 |
-
* Get game statistics for a room
|
| 1547 |
-
*/
|
| 1548 |
-
getGameStats(roomId) {
|
| 1549 |
-
const room = this.rooms.get(roomId);
|
| 1550 |
-
if (!room) return null;
|
| 1551 |
-
return room.game.getStats();
|
| 1552 |
-
}
|
| 1553 |
-
/**
|
| 1554 |
-
* Get current player for a room
|
| 1555 |
-
*/
|
| 1556 |
-
getCurrentPlayer(roomId) {
|
| 1557 |
-
const room = this.rooms.get(roomId);
|
| 1558 |
-
if (!room) return null;
|
| 1559 |
-
const currentStone = room.game.getCurrentPlayer();
|
| 1560 |
-
return currentStone === StoneType.BLACK ? "black" : "white";
|
| 1561 |
-
}
|
| 1562 |
-
/**
|
| 1563 |
-
* Calculate and get territory for a room
|
| 1564 |
-
*/
|
| 1565 |
-
getTerritory(roomId) {
|
| 1566 |
-
const room = this.rooms.get(roomId);
|
| 1567 |
-
if (!room) return null;
|
| 1568 |
-
return room.game.getTerritory();
|
| 1569 |
-
}
|
| 1570 |
-
/**
|
| 1571 |
-
* End the game and determine winner based on territory
|
| 1572 |
-
*/
|
| 1573 |
-
endGameByTerritory(roomId) {
|
| 1574 |
-
const room = this.rooms.get(roomId);
|
| 1575 |
-
if (!room) return false;
|
| 1576 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1577 |
-
return false;
|
| 1578 |
-
}
|
| 1579 |
-
const territory = room.game.getTerritory();
|
| 1580 |
-
if (territory.black > territory.white) {
|
| 1581 |
-
room.gameState.winner = "black";
|
| 1582 |
-
} else if (territory.white > territory.black) {
|
| 1583 |
-
room.gameState.winner = "white";
|
| 1584 |
-
} else {
|
| 1585 |
-
room.gameState.winner = null;
|
| 1586 |
-
}
|
| 1587 |
-
room.gameState.gameStatus = "finished";
|
| 1588 |
-
console.log(
|
| 1589 |
-
`Game ${roomId} ended. Black: ${territory.black}, White: ${territory.white}, Winner: ${room.gameState.winner}`
|
| 1590 |
-
);
|
| 1591 |
-
return true;
|
| 1592 |
-
}
|
| 1593 |
-
/**
|
| 1594 |
-
* Check if both players passed consecutively (game should end)
|
| 1595 |
-
* Returns true if game was ended
|
| 1596 |
-
*/
|
| 1597 |
-
checkConsecutivePasses(roomId) {
|
| 1598 |
-
const room = this.rooms.get(roomId);
|
| 1599 |
-
if (!room) return false;
|
| 1600 |
-
const history = room.game.getHistory();
|
| 1601 |
-
if (history.length < 2) return false;
|
| 1602 |
-
const lastMove = history[history.length - 1];
|
| 1603 |
-
const secondLastMove = history[history.length - 2];
|
| 1604 |
-
if (lastMove.type === 1 /* PASS */ && secondLastMove.type === 1 /* PASS */) {
|
| 1605 |
-
this.endGameByTerritory(roomId);
|
| 1606 |
-
return true;
|
| 1607 |
-
}
|
| 1608 |
-
return false;
|
| 1609 |
-
}
|
| 1610 |
-
getRoom(roomId) {
|
| 1611 |
-
return this.rooms.get(roomId);
|
| 1612 |
-
}
|
| 1613 |
-
getPlayerRoom(playerId) {
|
| 1614 |
-
const roomId = this.playerRoomMap.get(playerId);
|
| 1615 |
-
if (!roomId) return void 0;
|
| 1616 |
-
return this.rooms.get(roomId);
|
| 1617 |
-
}
|
| 1618 |
-
getActiveRooms() {
|
| 1619 |
-
return Array.from(this.rooms.values()).filter(
|
| 1620 |
-
(room) => room.gameState.gameStatus !== "finished"
|
| 1621 |
-
);
|
| 1622 |
-
}
|
| 1623 |
-
generateRoomId() {
|
| 1624 |
-
return uuidv4().substring(0, 8).toUpperCase();
|
| 1625 |
-
}
|
| 1626 |
-
};
|
| 1627 |
-
|
| 1628 |
-
// backend/src/sockets/gameSocket.ts
|
| 1629 |
-
function getRoomSummary(room) {
|
| 1630 |
-
const connectedPlayers = Object.values(room.players).filter((p) => p.connected);
|
| 1631 |
-
return {
|
| 1632 |
-
id: room.id,
|
| 1633 |
-
playerCount: connectedPlayers.length,
|
| 1634 |
-
maxPlayers: 2,
|
| 1635 |
-
status: room.gameState.gameStatus,
|
| 1636 |
-
isFull: connectedPlayers.length >= 2,
|
| 1637 |
-
createdAt: room.createdAt.toISOString()
|
| 1638 |
-
};
|
| 1639 |
-
}
|
| 1640 |
-
function setupSocketHandlers(io2, socket, gameManager2) {
|
| 1641 |
-
console.log(`Setting up socket handlers for ${socket.id}`);
|
| 1642 |
-
socket.on("listRooms", (callback) => {
|
| 1643 |
-
const rooms = gameManager2.getActiveRooms();
|
| 1644 |
-
const roomList = rooms.map((room) => getRoomSummary(room));
|
| 1645 |
-
if (callback) {
|
| 1646 |
-
callback({ success: true, rooms: roomList });
|
| 1647 |
-
}
|
| 1648 |
-
});
|
| 1649 |
-
socket.on(
|
| 1650 |
-
"joinRoom",
|
| 1651 |
-
(data, callback) => {
|
| 1652 |
-
console.log("[gameSocket] joinRoom event received:", {
|
| 1653 |
-
roomId: data.roomId,
|
| 1654 |
-
nickname: data.nickname,
|
| 1655 |
-
preferredColor: data.preferredColor,
|
| 1656 |
-
hasCallback: !!callback,
|
| 1657 |
-
socketId: socket.id
|
| 1658 |
-
});
|
| 1659 |
-
const { roomId, nickname, preferredColor } = data;
|
| 1660 |
-
try {
|
| 1661 |
-
let room;
|
| 1662 |
-
if (roomId) {
|
| 1663 |
-
const existingRoom = gameManager2.getRoom(roomId);
|
| 1664 |
-
if (!existingRoom) {
|
| 1665 |
-
if (callback) {
|
| 1666 |
-
callback({
|
| 1667 |
-
success: false,
|
| 1668 |
-
error: "Room not found",
|
| 1669 |
-
errorCode: "ROOM_NOT_FOUND"
|
| 1670 |
-
});
|
| 1671 |
-
}
|
| 1672 |
-
return;
|
| 1673 |
-
}
|
| 1674 |
-
const playerCount = Object.keys(existingRoom.players).length;
|
| 1675 |
-
if (playerCount >= 2) {
|
| 1676 |
-
if (callback) {
|
| 1677 |
-
callback({
|
| 1678 |
-
success: false,
|
| 1679 |
-
error: "Room is full",
|
| 1680 |
-
errorCode: "ROOM_FULL"
|
| 1681 |
-
});
|
| 1682 |
-
}
|
| 1683 |
-
return;
|
| 1684 |
-
}
|
| 1685 |
-
room = gameManager2.joinRoom(roomId, socket.id, nickname, preferredColor);
|
| 1686 |
-
} else {
|
| 1687 |
-
room = gameManager2.createRoom(socket.id, nickname, void 0, preferredColor);
|
| 1688 |
-
}
|
| 1689 |
-
if (room) {
|
| 1690 |
-
socket.join(room.id);
|
| 1691 |
-
const roomSockets = io2.sockets.adapter.rooms.get(room.id);
|
| 1692 |
-
console.log(`[gameSocket] Socket ${socket.id} joined room ${room.id}`);
|
| 1693 |
-
console.log(`[gameSocket] Room ${room.id} now has sockets:`, roomSockets ? Array.from(roomSockets) : []);
|
| 1694 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1695 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1696 |
-
const tgn = room.game.toTGN();
|
| 1697 |
-
const players = {};
|
| 1698 |
-
for (const [pid, player] of Object.entries(room.players)) {
|
| 1699 |
-
players[pid] = {
|
| 1700 |
-
nickname: player.nickname,
|
| 1701 |
-
color: player.color
|
| 1702 |
-
};
|
| 1703 |
-
}
|
| 1704 |
-
const response = {
|
| 1705 |
-
success: true,
|
| 1706 |
-
roomId: room.id,
|
| 1707 |
-
playerId: socket.id,
|
| 1708 |
-
playerColor: room.players[socket.id]?.color,
|
| 1709 |
-
isAdmin: room.adminId === socket.id,
|
| 1710 |
-
adminId: room.adminId,
|
| 1711 |
-
players,
|
| 1712 |
-
// Include all players in room
|
| 1713 |
-
gameState: {
|
| 1714 |
-
boardShape: room.game.getShape(),
|
| 1715 |
-
currentPlayer,
|
| 1716 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1717 |
-
capturedStones: {
|
| 1718 |
-
black: stats?.capturedByBlack || 0,
|
| 1719 |
-
white: stats?.capturedByWhite || 0
|
| 1720 |
-
},
|
| 1721 |
-
gameStatus: room.gameState.gameStatus,
|
| 1722 |
-
winner: room.gameState.winner,
|
| 1723 |
-
tgn
|
| 1724 |
-
}
|
| 1725 |
-
};
|
| 1726 |
-
if (callback) {
|
| 1727 |
-
console.log("[gameSocket] Sending response via callback:", {
|
| 1728 |
-
roomId: response.roomId,
|
| 1729 |
-
playerColor: response.playerColor
|
| 1730 |
-
});
|
| 1731 |
-
callback(response);
|
| 1732 |
-
} else {
|
| 1733 |
-
console.log("[gameSocket] No callback, using roomJoined emit");
|
| 1734 |
-
socket.emit("roomJoined", response);
|
| 1735 |
-
}
|
| 1736 |
-
console.log(`[gameSocket] Broadcasting playerJoined to room ${room.id} (excluding ${socket.id})`);
|
| 1737 |
-
socket.to(room.id).emit("playerJoined", {
|
| 1738 |
-
playerId: socket.id,
|
| 1739 |
-
nickname
|
| 1740 |
-
});
|
| 1741 |
-
console.log(`[gameSocket] playerJoined broadcast sent`);
|
| 1742 |
-
const roomSummary = getRoomSummary(room);
|
| 1743 |
-
if (roomId) {
|
| 1744 |
-
io2.emit("roomUpdated", roomSummary);
|
| 1745 |
-
} else {
|
| 1746 |
-
io2.emit("roomCreated", roomSummary);
|
| 1747 |
-
}
|
| 1748 |
-
console.log(
|
| 1749 |
-
`Player ${socket.id} ${roomId ? "joined" : "created"} room ${room.id}`
|
| 1750 |
-
);
|
| 1751 |
-
} else {
|
| 1752 |
-
if (callback) {
|
| 1753 |
-
callback({
|
| 1754 |
-
success: false,
|
| 1755 |
-
error: "Failed to join or create room",
|
| 1756 |
-
errorCode: "UNKNOWN_ERROR"
|
| 1757 |
-
});
|
| 1758 |
-
}
|
| 1759 |
-
}
|
| 1760 |
-
} catch (error) {
|
| 1761 |
-
console.error(`Error in joinRoom handler:`, error);
|
| 1762 |
-
if (callback) {
|
| 1763 |
-
callback({
|
| 1764 |
-
success: false,
|
| 1765 |
-
error: "Server error",
|
| 1766 |
-
errorCode: "SERVER_ERROR"
|
| 1767 |
-
});
|
| 1768 |
-
}
|
| 1769 |
-
}
|
| 1770 |
-
}
|
| 1771 |
-
);
|
| 1772 |
-
socket.on("leaveRoom", () => {
|
| 1773 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1774 |
-
if (room) {
|
| 1775 |
-
const roomId = room.id;
|
| 1776 |
-
socket.leave(room.id);
|
| 1777 |
-
gameManager2.leaveRoom(room.id, socket.id);
|
| 1778 |
-
socket.to(roomId).emit("playerLeft", {
|
| 1779 |
-
playerId: socket.id
|
| 1780 |
-
});
|
| 1781 |
-
const updatedRoom = gameManager2.getRoom(roomId);
|
| 1782 |
-
if (updatedRoom) {
|
| 1783 |
-
io2.emit("roomUpdated", getRoomSummary(updatedRoom));
|
| 1784 |
-
} else {
|
| 1785 |
-
io2.emit("roomDeleted", { roomId });
|
| 1786 |
-
}
|
| 1787 |
-
}
|
| 1788 |
-
});
|
| 1789 |
-
socket.on("makeMove", (data) => {
|
| 1790 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1791 |
-
if (room && gameManager2.makeMove(room.id, socket.id, data)) {
|
| 1792 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1793 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1794 |
-
const lastStep = room.game.getLastStep();
|
| 1795 |
-
const tgn = room.game.toTGN();
|
| 1796 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1797 |
-
currentPlayer,
|
| 1798 |
-
action: "move",
|
| 1799 |
-
lastMove: data,
|
| 1800 |
-
capturedStones: {
|
| 1801 |
-
black: stats?.capturedByBlack || 0,
|
| 1802 |
-
white: stats?.capturedByWhite || 0
|
| 1803 |
-
},
|
| 1804 |
-
capturedPositions: lastStep?.capturedPositions,
|
| 1805 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1806 |
-
tgn
|
| 1807 |
-
});
|
| 1808 |
-
} else {
|
| 1809 |
-
socket.emit("error", { message: "Invalid move" });
|
| 1810 |
-
}
|
| 1811 |
-
});
|
| 1812 |
-
socket.on("pass", () => {
|
| 1813 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1814 |
-
if (room && gameManager2.passTurn(room.id, socket.id)) {
|
| 1815 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1816 |
-
const tgn = room.game.toTGN();
|
| 1817 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1818 |
-
currentPlayer,
|
| 1819 |
-
action: "pass",
|
| 1820 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1821 |
-
tgn
|
| 1822 |
-
});
|
| 1823 |
-
if (gameManager2.checkConsecutivePasses(room.id)) {
|
| 1824 |
-
const territory = gameManager2.getTerritory(room.id);
|
| 1825 |
-
io2.to(room.id).emit("gameEnded", {
|
| 1826 |
-
winner: room.gameState.winner,
|
| 1827 |
-
reason: "double-pass",
|
| 1828 |
-
territory
|
| 1829 |
-
});
|
| 1830 |
-
}
|
| 1831 |
-
}
|
| 1832 |
-
});
|
| 1833 |
-
socket.on("resign", () => {
|
| 1834 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1835 |
-
if (room && gameManager2.resign(room.id, socket.id)) {
|
| 1836 |
-
io2.to(room.id).emit("gameEnded", {
|
| 1837 |
-
winner: room.gameState.winner,
|
| 1838 |
-
reason: "resignation"
|
| 1839 |
-
});
|
| 1840 |
-
}
|
| 1841 |
-
});
|
| 1842 |
-
socket.on("undoMove", (callback) => {
|
| 1843 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1844 |
-
if (!room) {
|
| 1845 |
-
if (callback) callback({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 1846 |
-
return;
|
| 1847 |
-
}
|
| 1848 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1849 |
-
if (callback) callback({ success: false, error: "Game not active", errorCode: "GAME_NOT_ACTIVE" });
|
| 1850 |
-
return;
|
| 1851 |
-
}
|
| 1852 |
-
const success = gameManager2.undoMove(room.id, socket.id);
|
| 1853 |
-
if (success) {
|
| 1854 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1855 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1856 |
-
const tgn = room.game.toTGN();
|
| 1857 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1858 |
-
currentPlayer,
|
| 1859 |
-
action: "undo",
|
| 1860 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1861 |
-
capturedStones: {
|
| 1862 |
-
black: stats?.capturedByBlack || 0,
|
| 1863 |
-
white: stats?.capturedByWhite || 0
|
| 1864 |
-
},
|
| 1865 |
-
tgn
|
| 1866 |
-
});
|
| 1867 |
-
if (callback) callback({ success: true });
|
| 1868 |
-
} else {
|
| 1869 |
-
if (callback) callback({ success: false, error: "Cannot undo", errorCode: "UNDO_FAILED" });
|
| 1870 |
-
}
|
| 1871 |
-
});
|
| 1872 |
-
socket.on("redoMove", (callback) => {
|
| 1873 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1874 |
-
if (!room) {
|
| 1875 |
-
if (callback) callback({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 1876 |
-
return;
|
| 1877 |
-
}
|
| 1878 |
-
if (room.gameState.gameStatus !== "playing") {
|
| 1879 |
-
if (callback) callback({ success: false, error: "Game not active", errorCode: "GAME_NOT_ACTIVE" });
|
| 1880 |
-
return;
|
| 1881 |
-
}
|
| 1882 |
-
if (!room.game.canRedo()) {
|
| 1883 |
-
if (callback) callback({ success: false, error: "Nothing to redo", errorCode: "NOTHING_TO_REDO" });
|
| 1884 |
-
return;
|
| 1885 |
-
}
|
| 1886 |
-
const success = gameManager2.redoMove(room.id, socket.id);
|
| 1887 |
-
if (success) {
|
| 1888 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1889 |
-
const stats = gameManager2.getGameStats(room.id);
|
| 1890 |
-
const lastStep = room.game.getLastStep();
|
| 1891 |
-
const tgn = room.game.toTGN();
|
| 1892 |
-
io2.to(room.id).emit("gameUpdate", {
|
| 1893 |
-
currentPlayer,
|
| 1894 |
-
action: "redo",
|
| 1895 |
-
lastMove: lastStep?.position,
|
| 1896 |
-
capturedStones: {
|
| 1897 |
-
black: stats?.capturedByBlack || 0,
|
| 1898 |
-
white: stats?.capturedByWhite || 0
|
| 1899 |
-
},
|
| 1900 |
-
capturedPositions: lastStep?.capturedPositions,
|
| 1901 |
-
currentMoveIndex: room.game.getCurrentStep(),
|
| 1902 |
-
tgn
|
| 1903 |
-
});
|
| 1904 |
-
if (callback) callback({ success: true });
|
| 1905 |
-
} else {
|
| 1906 |
-
if (callback) callback({ success: false, error: "Redo failed", errorCode: "REDO_FAILED" });
|
| 1907 |
-
}
|
| 1908 |
-
});
|
| 1909 |
-
socket.on("resetGame", (data, callback) => {
|
| 1910 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1911 |
-
if (!room) {
|
| 1912 |
-
const cb = typeof data === "function" ? data : callback;
|
| 1913 |
-
if (cb) cb({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 1914 |
-
return;
|
| 1915 |
-
}
|
| 1916 |
-
const options = typeof data === "object" && data !== null ? {
|
| 1917 |
-
boardShape: data.boardShape,
|
| 1918 |
-
playerColors: data.playerColors
|
| 1919 |
-
} : void 0;
|
| 1920 |
-
const responseCb = typeof data === "function" ? data : callback;
|
| 1921 |
-
const result = gameManager2.resetGame(room.id, socket.id, options);
|
| 1922 |
-
if (result.success) {
|
| 1923 |
-
const currentPlayer = gameManager2.getCurrentPlayer(room.id);
|
| 1924 |
-
const tgn = room.game.toTGN();
|
| 1925 |
-
const players = {};
|
| 1926 |
-
for (const [pid, player] of Object.entries(room.players)) {
|
| 1927 |
-
players[pid] = {
|
| 1928 |
-
nickname: player.nickname,
|
| 1929 |
-
color: player.color
|
| 1930 |
-
};
|
| 1931 |
-
}
|
| 1932 |
-
io2.to(room.id).emit("gameReset", {
|
| 1933 |
-
currentPlayer,
|
| 1934 |
-
boardShape: room.game.getShape(),
|
| 1935 |
-
currentMoveIndex: 0,
|
| 1936 |
-
capturedStones: { black: 0, white: 0 },
|
| 1937 |
-
players,
|
| 1938 |
-
tgn
|
| 1939 |
-
});
|
| 1940 |
-
if (responseCb) responseCb({ success: true });
|
| 1941 |
-
} else {
|
| 1942 |
-
if (responseCb) responseCb({
|
| 1943 |
-
success: false,
|
| 1944 |
-
error: result.error || "Reset failed",
|
| 1945 |
-
errorCode: result.error === "Only room admin can reset the game" ? "NOT_ADMIN" : "RESET_FAILED"
|
| 1946 |
-
});
|
| 1947 |
-
}
|
| 1948 |
-
});
|
| 1949 |
-
socket.on("chatMessage", (data) => {
|
| 1950 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1951 |
-
if (room) {
|
| 1952 |
-
const player = room.players[socket.id];
|
| 1953 |
-
io2.to(room.id).emit("chatMessage", {
|
| 1954 |
-
author: player?.nickname || "Unknown",
|
| 1955 |
-
content: data.content,
|
| 1956 |
-
playerId: socket.id
|
| 1957 |
-
});
|
| 1958 |
-
}
|
| 1959 |
-
});
|
| 1960 |
-
socket.on(
|
| 1961 |
-
"changeNickname",
|
| 1962 |
-
(data, callback) => {
|
| 1963 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1964 |
-
if (!room) {
|
| 1965 |
-
const error = { success: false, error: "Not in a room" };
|
| 1966 |
-
if (callback) callback(error);
|
| 1967 |
-
return;
|
| 1968 |
-
}
|
| 1969 |
-
const validation = validateNickname(data.nickname);
|
| 1970 |
-
if (!validation.valid) {
|
| 1971 |
-
const error = { success: false, error: validation.error };
|
| 1972 |
-
if (callback) callback(error);
|
| 1973 |
-
return;
|
| 1974 |
-
}
|
| 1975 |
-
const player = room.players[socket.id];
|
| 1976 |
-
if (player) {
|
| 1977 |
-
const oldNickname = player.nickname;
|
| 1978 |
-
player.nickname = data.nickname;
|
| 1979 |
-
io2.to(room.id).emit("nicknameChanged", {
|
| 1980 |
-
playerId: socket.id,
|
| 1981 |
-
nickname: data.nickname,
|
| 1982 |
-
oldNickname
|
| 1983 |
-
});
|
| 1984 |
-
console.log(`Player ${socket.id} changed nickname: ${oldNickname} -> ${data.nickname}`);
|
| 1985 |
-
if (callback) {
|
| 1986 |
-
callback({ success: true, nickname: data.nickname });
|
| 1987 |
-
}
|
| 1988 |
-
}
|
| 1989 |
-
}
|
| 1990 |
-
);
|
| 1991 |
-
socket.on("disconnect", () => {
|
| 1992 |
-
console.log(`Client disconnected: ${socket.id}`);
|
| 1993 |
-
const room = gameManager2.getPlayerRoom(socket.id);
|
| 1994 |
-
if (room) {
|
| 1995 |
-
const roomId = room.id;
|
| 1996 |
-
gameManager2.leaveRoom(room.id, socket.id);
|
| 1997 |
-
socket.to(room.id).emit("playerDisconnected", {
|
| 1998 |
-
playerId: socket.id
|
| 1999 |
-
});
|
| 2000 |
-
const updatedRoom = gameManager2.getRoom(roomId);
|
| 2001 |
-
if (updatedRoom) {
|
| 2002 |
-
io2.emit("roomUpdated", getRoomSummary(updatedRoom));
|
| 2003 |
-
} else {
|
| 2004 |
-
io2.emit("roomDeleted", { roomId });
|
| 2005 |
-
}
|
| 2006 |
-
}
|
| 2007 |
-
});
|
| 2008 |
-
}
|
| 2009 |
-
function validateNickname(nickname) {
|
| 2010 |
-
if (!nickname || typeof nickname !== "string") {
|
| 2011 |
-
return { valid: false, error: "Invalid nickname" };
|
| 2012 |
-
}
|
| 2013 |
-
const trimmed = nickname.trim();
|
| 2014 |
-
if (trimmed.length < 3) {
|
| 2015 |
-
return { valid: false, error: "Nickname must be at least 3 characters" };
|
| 2016 |
-
}
|
| 2017 |
-
if (trimmed.length > 20) {
|
| 2018 |
-
return { valid: false, error: "Nickname must be 20 characters or less" };
|
| 2019 |
-
}
|
| 2020 |
-
if (!/^[a-zA-Z0-9 ]+$/.test(trimmed)) {
|
| 2021 |
-
return { valid: false, error: "Only letters, numbers, and spaces allowed" };
|
| 2022 |
-
}
|
| 2023 |
-
if (trimmed !== nickname) {
|
| 2024 |
-
return { valid: false, error: "No leading or trailing spaces allowed" };
|
| 2025 |
-
}
|
| 2026 |
-
return { valid: true };
|
| 2027 |
-
}
|
| 2028 |
-
|
| 2029 |
-
// backend/src/server.ts
|
| 2030 |
-
var __filename = fileURLToPath(import.meta.url);
|
| 2031 |
-
var __dirname = path.dirname(__filename);
|
| 2032 |
-
var isDev = __dirname.includes("/src") && !__dirname.includes("/dist");
|
| 2033 |
-
var levelsUp = isDev ? "../" : "../../../";
|
| 2034 |
-
var envPath = path.join(__dirname, levelsUp, ".env");
|
| 2035 |
-
var envLocalPath = path.join(__dirname, levelsUp, ".env.local");
|
| 2036 |
-
if (fs.existsSync(envPath)) {
|
| 2037 |
-
dotenv.config({ path: envPath });
|
| 2038 |
-
console.log("[Config] Loaded .env");
|
| 2039 |
-
} else {
|
| 2040 |
-
console.log(`[Config] .env not found at: ${envPath}`);
|
| 2041 |
-
}
|
| 2042 |
-
if (fs.existsSync(envLocalPath)) {
|
| 2043 |
-
dotenv.config({ path: envLocalPath, override: true });
|
| 2044 |
-
console.log("[Config] Loaded .env.local (overriding .env)");
|
| 2045 |
-
} else {
|
| 2046 |
-
console.log(`[Config] .env.local not found at: ${envLocalPath}`);
|
| 2047 |
-
}
|
| 2048 |
-
var app = express();
|
| 2049 |
-
var httpServer = createServer(app);
|
| 2050 |
-
var io = new Server(httpServer, {
|
| 2051 |
-
cors: {
|
| 2052 |
-
origin: process.env.NODE_ENV === "production" ? process.env.CLIENT_URL || "http://localhost:5173" : true,
|
| 2053 |
-
// Allow all origins in development
|
| 2054 |
-
methods: ["GET", "POST"],
|
| 2055 |
-
credentials: true
|
| 2056 |
-
}
|
| 2057 |
-
});
|
| 2058 |
-
var gameManager = new GameManager();
|
| 2059 |
-
var PORT = parseInt(process.env.PORT || "3000", 10);
|
| 2060 |
-
var HOST = process.env.HOST || "0.0.0.0";
|
| 2061 |
-
console.log(`[Config] Server Configuration:`);
|
| 2062 |
-
console.log(`[Config] PORT: ${PORT}`);
|
| 2063 |
-
console.log(`[Config] HOST: ${HOST}`);
|
| 2064 |
-
console.log(`[Config] NODE_ENV: ${process.env.NODE_ENV || "development"}`);
|
| 2065 |
-
console.log(`[Config] CLIENT_URL: ${process.env.CLIENT_URL || "not set"}`);
|
| 2066 |
-
app.use(cors());
|
| 2067 |
-
app.use(express.json());
|
| 2068 |
-
if (process.env.NODE_ENV === "production") {
|
| 2069 |
-
const frontendPath = path.join(__dirname, "../../../../app/dist");
|
| 2070 |
-
app.use(express.static(frontendPath));
|
| 2071 |
-
app.get("*", (req, res, next) => {
|
| 2072 |
-
if (req.path.startsWith("/health") || req.path.startsWith("/socket.io")) {
|
| 2073 |
-
return next();
|
| 2074 |
-
}
|
| 2075 |
-
res.sendFile(path.join(frontendPath, "index.html"));
|
| 2076 |
-
});
|
| 2077 |
-
}
|
| 2078 |
-
app.get("/health", (_req, res) => {
|
| 2079 |
-
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
| 2080 |
-
});
|
| 2081 |
-
io.on("connection", (socket) => {
|
| 2082 |
-
console.log(`New client connected: ${socket.id}`);
|
| 2083 |
-
setupSocketHandlers(io, socket, gameManager);
|
| 2084 |
-
socket.on("echo", (data, callback) => {
|
| 2085 |
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
| 2086 |
-
const responseMessage = `Hello from server! Received: "${data.message}" at ${timestamp}`;
|
| 2087 |
-
console.log(`[Echo] Client ${socket.id}: ${data.message}`);
|
| 2088 |
-
if (callback && typeof callback === "function") {
|
| 2089 |
-
callback({
|
| 2090 |
-
message: responseMessage,
|
| 2091 |
-
serverTime: timestamp,
|
| 2092 |
-
clientTime: data.timestamp
|
| 2093 |
-
});
|
| 2094 |
-
}
|
| 2095 |
-
});
|
| 2096 |
-
socket.on("disconnect", () => {
|
| 2097 |
-
console.log(`Client disconnected: ${socket.id}`);
|
| 2098 |
-
});
|
| 2099 |
-
});
|
| 2100 |
-
httpServer.listen(PORT, HOST, () => {
|
| 2101 |
-
console.log(`Server running on ${HOST}:${PORT}`);
|
| 2102 |
-
console.log(`Health check: http://${HOST}:${PORT}/health`);
|
| 2103 |
-
console.log(`Environment: ${process.env.NODE_ENV || "development"}`);
|
| 2104 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/backend/package.json
CHANGED
|
@@ -3,11 +3,11 @@
|
|
| 3 |
"version": "1.0.0",
|
| 4 |
"type": "module",
|
| 5 |
"description": "Backend server for Trigo game",
|
| 6 |
-
"main": "dist/
|
| 7 |
"scripts": {
|
| 8 |
"dev": "nodemon --watch src --exec tsx src/server.ts",
|
| 9 |
"build": "tsc",
|
| 10 |
-
"start": "node dist/
|
| 11 |
"test": "echo \"Error: no test specified\" && exit 1"
|
| 12 |
},
|
| 13 |
"keywords": [
|
|
|
|
| 3 |
"version": "1.0.0",
|
| 4 |
"type": "module",
|
| 5 |
"description": "Backend server for Trigo game",
|
| 6 |
+
"main": "dist/server.js",
|
| 7 |
"scripts": {
|
| 8 |
"dev": "nodemon --watch src --exec tsx src/server.ts",
|
| 9 |
"build": "tsc",
|
| 10 |
+
"start": "node dist/server.js",
|
| 11 |
"test": "echo \"Error: no test specified\" && exit 1"
|
| 12 |
},
|
| 13 |
"keywords": [
|
trigo-web/backend/src/server.ts
CHANGED
|
@@ -69,7 +69,7 @@ app.use(express.json());
|
|
| 69 |
|
| 70 |
// Serve static files from frontend build (for production)
|
| 71 |
if (process.env.NODE_ENV === "production") {
|
| 72 |
-
const frontendPath = path.join(__dirname, "
|
| 73 |
app.use(express.static(frontendPath));
|
| 74 |
|
| 75 |
// Serve index.html for all routes (SPA support)
|
|
|
|
| 69 |
|
| 70 |
// Serve static files from frontend build (for production)
|
| 71 |
if (process.env.NODE_ENV === "production") {
|
| 72 |
+
const frontendPath = path.join(__dirname, "../../app/dist");
|
| 73 |
app.use(express.static(frontendPath));
|
| 74 |
|
| 75 |
// Serve index.html for all routes (SPA support)
|
trigo-web/backend/src/services/gameManager.ts
CHANGED
|
@@ -83,16 +83,20 @@ export class GameManager {
|
|
| 83 |
return null;
|
| 84 |
}
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
|
|
|
| 88 |
return null; // Room is full
|
| 89 |
}
|
| 90 |
|
| 91 |
// Try to assign preferred color if specified
|
| 92 |
-
const firstPlayer =
|
| 93 |
let assignedColor: "black" | "white";
|
| 94 |
|
| 95 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 96 |
// Preferred color is available
|
| 97 |
assignedColor = preferredColor;
|
| 98 |
} else {
|
|
@@ -100,6 +104,13 @@ export class GameManager {
|
|
| 100 |
assignedColor = firstPlayer.color === "black" ? "white" : "black";
|
| 101 |
}
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
room.players[playerId] = {
|
| 104 |
id: playerId,
|
| 105 |
nickname,
|
|
@@ -110,7 +121,7 @@ export class GameManager {
|
|
| 110 |
this.playerRoomMap.set(playerId, roomId);
|
| 111 |
|
| 112 |
// Start the game when second player joins
|
| 113 |
-
if (
|
| 114 |
room.gameState.gameStatus = "playing";
|
| 115 |
room.startedAt = new Date();
|
| 116 |
}
|
|
|
|
| 83 |
return null;
|
| 84 |
}
|
| 85 |
|
| 86 |
+
// Count only connected players
|
| 87 |
+
const connectedPlayers = Object.values(room.players).filter(p => p.connected);
|
| 88 |
+
if (connectedPlayers.length >= 2) {
|
| 89 |
return null; // Room is full
|
| 90 |
}
|
| 91 |
|
| 92 |
// Try to assign preferred color if specified
|
| 93 |
+
const firstPlayer = connectedPlayers[0];
|
| 94 |
let assignedColor: "black" | "white";
|
| 95 |
|
| 96 |
+
if (!firstPlayer) {
|
| 97 |
+
// No connected players, assign preferred or default to black
|
| 98 |
+
assignedColor = preferredColor || "black";
|
| 99 |
+
} else if (preferredColor && preferredColor !== firstPlayer.color) {
|
| 100 |
// Preferred color is available
|
| 101 |
assignedColor = preferredColor;
|
| 102 |
} else {
|
|
|
|
| 104 |
assignedColor = firstPlayer.color === "black" ? "white" : "black";
|
| 105 |
}
|
| 106 |
|
| 107 |
+
// Clean up old disconnected player entries with the same color
|
| 108 |
+
for (const [pid, player] of Object.entries(room.players)) {
|
| 109 |
+
if (!player.connected && player.color === assignedColor) {
|
| 110 |
+
delete room.players[pid];
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
room.players[playerId] = {
|
| 115 |
id: playerId,
|
| 116 |
nickname,
|
|
|
|
| 121 |
this.playerRoomMap.set(playerId, roomId);
|
| 122 |
|
| 123 |
// Start the game when second player joins
|
| 124 |
+
if (connectedPlayers.length === 1) {
|
| 125 |
room.gameState.gameStatus = "playing";
|
| 126 |
room.startedAt = new Date();
|
| 127 |
}
|
trigo-web/backend/src/sockets/gameSocket.ts
CHANGED
|
@@ -19,8 +19,19 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 19 |
|
| 20 |
// List available rooms
|
| 21 |
socket.on("listRooms", (callback?: (response: any) => void) => {
|
|
|
|
| 22 |
const rooms = gameManager.getActiveRooms();
|
| 23 |
const roomList = rooms.map(room => getRoomSummary(room));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
if (callback) {
|
| 26 |
callback({ success: true, rooms: roomList });
|
|
@@ -61,8 +72,8 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 61 |
return;
|
| 62 |
}
|
| 63 |
|
| 64 |
-
const
|
| 65 |
-
if (
|
| 66 |
// Room is full
|
| 67 |
if (callback) {
|
| 68 |
callback({
|
|
@@ -83,10 +94,15 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 83 |
if (room) {
|
| 84 |
socket.join(room.id);
|
| 85 |
|
| 86 |
-
// Debug: Log socket.io room membership
|
| 87 |
const roomSockets = io.sockets.adapter.rooms.get(room.id);
|
| 88 |
console.log(`[gameSocket] Socket ${socket.id} joined room ${room.id}`);
|
| 89 |
console.log(`[gameSocket] Room ${room.id} now has sockets:`, roomSockets ? Array.from(roomSockets) : []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
// Get complete game data for frontend
|
| 92 |
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
|
@@ -472,20 +488,32 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 472 |
socket.on("disconnect", () => {
|
| 473 |
console.log(`Client disconnected: ${socket.id}`);
|
| 474 |
const room = gameManager.getPlayerRoom(socket.id);
|
|
|
|
| 475 |
if (room) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
const roomId = room.id;
|
| 477 |
gameManager.leaveRoom(room.id, socket.id);
|
| 478 |
-
socket.to(room.id).emit("playerDisconnected", {
|
| 479 |
-
playerId: socket.id
|
| 480 |
-
});
|
| 481 |
|
| 482 |
-
// Broadcast room update or deletion to all sockets
|
| 483 |
const updatedRoom = gameManager.getRoom(roomId);
|
| 484 |
if (updatedRoom) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
io.emit("roomUpdated", getRoomSummary(updatedRoom));
|
| 486 |
} else {
|
|
|
|
| 487 |
io.emit("roomDeleted", { roomId });
|
| 488 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
}
|
| 490 |
});
|
| 491 |
}
|
|
|
|
| 19 |
|
| 20 |
// List available rooms
|
| 21 |
socket.on("listRooms", (callback?: (response: any) => void) => {
|
| 22 |
+
console.log("[gameSocket] listRooms request from:", socket.id);
|
| 23 |
const rooms = gameManager.getActiveRooms();
|
| 24 |
const roomList = rooms.map(room => getRoomSummary(room));
|
| 25 |
+
console.log("[gameSocket] Returning", roomList.length, "rooms:", roomList.map(r => `${r.id}(${r.playerCount}p)`));
|
| 26 |
+
|
| 27 |
+
// Debug: show detailed player state for each room
|
| 28 |
+
rooms.forEach(room => {
|
| 29 |
+
console.log(`[gameSocket] Room ${room.id} detailed state:`,
|
| 30 |
+
Object.entries(room.players).map(([id, p]: [string, any]) =>
|
| 31 |
+
`${id.slice(-6)}:${p.color}:${p.connected}`
|
| 32 |
+
)
|
| 33 |
+
);
|
| 34 |
+
});
|
| 35 |
|
| 36 |
if (callback) {
|
| 37 |
callback({ success: true, rooms: roomList });
|
|
|
|
| 72 |
return;
|
| 73 |
}
|
| 74 |
|
| 75 |
+
const connectedPlayers = Object.values(existingRoom.players).filter((p: any) => p.connected);
|
| 76 |
+
if (connectedPlayers.length >= 2) {
|
| 77 |
// Room is full
|
| 78 |
if (callback) {
|
| 79 |
callback({
|
|
|
|
| 94 |
if (room) {
|
| 95 |
socket.join(room.id);
|
| 96 |
|
| 97 |
+
// Debug: Log socket.io room membership and player state
|
| 98 |
const roomSockets = io.sockets.adapter.rooms.get(room.id);
|
| 99 |
console.log(`[gameSocket] Socket ${socket.id} joined room ${room.id}`);
|
| 100 |
console.log(`[gameSocket] Room ${room.id} now has sockets:`, roomSockets ? Array.from(roomSockets) : []);
|
| 101 |
+
console.log(`[gameSocket] Room ${room.id} players:`,
|
| 102 |
+
Object.entries(room.players).map(([id, p]: [string, any]) =>
|
| 103 |
+
`${id.slice(-6)}:${p.color}:${p.connected}`
|
| 104 |
+
)
|
| 105 |
+
);
|
| 106 |
|
| 107 |
// Get complete game data for frontend
|
| 108 |
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
|
|
|
| 488 |
socket.on("disconnect", () => {
|
| 489 |
console.log(`Client disconnected: ${socket.id}`);
|
| 490 |
const room = gameManager.getPlayerRoom(socket.id);
|
| 491 |
+
console.log(`[disconnect] Player ${socket.id} room:`, room ? room.id : 'none');
|
| 492 |
if (room) {
|
| 493 |
+
console.log(`[disconnect] Room ${room.id} players BEFORE leave:`,
|
| 494 |
+
Object.entries(room.players).map(([id, p]: [string, any]) =>
|
| 495 |
+
`${id.slice(-6)}:${p.color}:${p.connected}`
|
| 496 |
+
)
|
| 497 |
+
);
|
| 498 |
const roomId = room.id;
|
| 499 |
gameManager.leaveRoom(room.id, socket.id);
|
|
|
|
|
|
|
|
|
|
| 500 |
|
|
|
|
| 501 |
const updatedRoom = gameManager.getRoom(roomId);
|
| 502 |
if (updatedRoom) {
|
| 503 |
+
console.log(`[disconnect] Room ${roomId} players AFTER leave:`,
|
| 504 |
+
Object.entries(updatedRoom.players).map(([id, p]: [string, any]) =>
|
| 505 |
+
`${id.slice(-6)}:${p.color}:${p.connected}`
|
| 506 |
+
)
|
| 507 |
+
);
|
| 508 |
io.emit("roomUpdated", getRoomSummary(updatedRoom));
|
| 509 |
} else {
|
| 510 |
+
console.log(`[disconnect] Room ${roomId} deleted`);
|
| 511 |
io.emit("roomDeleted", { roomId });
|
| 512 |
}
|
| 513 |
+
|
| 514 |
+
socket.to(room.id).emit("playerDisconnected", {
|
| 515 |
+
playerId: socket.id
|
| 516 |
+
});
|
| 517 |
}
|
| 518 |
});
|
| 519 |
}
|
trigo-web/package.json
CHANGED
|
@@ -1,65 +1,64 @@
|
|
| 1 |
{
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
}
|
| 65 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "trigo-web",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"description": "3D Go board game with Vue3 and Node.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:app\"",
|
| 8 |
+
"dev:app": "cd app && npm run dev",
|
| 9 |
+
"dev:backend": "cd backend && npm run dev",
|
| 10 |
+
"build": "npm run build:app && npm run build:backend",
|
| 11 |
+
"build:app": "cd app && npm run build",
|
| 12 |
+
"build:backend": "cd backend && npm run build",
|
| 13 |
+
"build:parsers": "npm run build:parser:tgn",
|
| 14 |
+
"build:parser:tgn": "tsx tools/buildJisonParser.ts",
|
| 15 |
+
"install:all": "npm install && cd app && npm install && cd ../backend && npm install",
|
| 16 |
+
"start:prod": "cd backend && npm start",
|
| 17 |
+
"format": "prettier --write \"**/*.{js,ts,vue,json,md,scss,css}\"",
|
| 18 |
+
"format:check": "prettier --check \"**/*.{js,ts,vue,json,md,scss,css}\"",
|
| 19 |
+
"test": "vitest",
|
| 20 |
+
"test:ui": "vitest --ui",
|
| 21 |
+
"test:run": "vitest run",
|
| 22 |
+
"generate:games": "tsx tools/generateRandomGames.ts",
|
| 23 |
+
"migrate:tgn": "tsx tools/migrateTGN.ts"
|
| 24 |
+
},
|
| 25 |
+
"keywords": [
|
| 26 |
+
"game",
|
| 27 |
+
"go",
|
| 28 |
+
"3d",
|
| 29 |
+
"vue",
|
| 30 |
+
"nodejs",
|
| 31 |
+
"websocket"
|
| 32 |
+
],
|
| 33 |
+
"author": "",
|
| 34 |
+
"license": "MIT",
|
| 35 |
+
"lint-staged": {
|
| 36 |
+
"**/*.{js,ts,vue,json,md,scss,css}": []
|
| 37 |
+
},
|
| 38 |
+
"devDependencies": {
|
| 39 |
+
"@types/node": "^24.10.0",
|
| 40 |
+
"@types/yargs": "^17.0.34",
|
| 41 |
+
"@vitejs/plugin-vue": "^5.2.4",
|
| 42 |
+
"@vitest/ui": "^4.0.6",
|
| 43 |
+
"concurrently": "^7.6.0",
|
| 44 |
+
"eslint-config-prettier": "^10.1.8",
|
| 45 |
+
"eslint-plugin-prettier": "^5.5.4",
|
| 46 |
+
"husky": "^9.1.7",
|
| 47 |
+
"jison": "^0.4.18",
|
| 48 |
+
"jsdom": "^27.1.0",
|
| 49 |
+
"lint-staged": "^16.2.7",
|
| 50 |
+
"onnxruntime-node": "^1.23.2",
|
| 51 |
+
"onnxruntime-web": "1.23.2",
|
| 52 |
+
"prettier": "^3.6.2",
|
| 53 |
+
"tsx": "^4.20.6",
|
| 54 |
+
"typescript": "^5.2.2",
|
| 55 |
+
"vite": "^5.4.21",
|
| 56 |
+
"vitest": "^4.0.6",
|
| 57 |
+
"vue": "^3.3.4",
|
| 58 |
+
"vue-tsc": "^3.1.3",
|
| 59 |
+
"yargs": "^18.0.0"
|
| 60 |
+
},
|
| 61 |
+
"dependencies": {
|
| 62 |
+
"dotenv": "^17.2.3"
|
| 63 |
+
}
|
|
|
|
| 64 |
}
|
trigo-web/tests/game/debug_capture.test.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
import { describe, it } from "vitest";
|
| 2 |
-
import { TrigoGame, StoneType } from "@inc/trigo/game";
|
| 3 |
-
|
| 4 |
-
describe("Debug Capture", () => {
|
| 5 |
-
it("debug 2D capture scenario", () => {
|
| 6 |
-
const game = new TrigoGame({ x: 5, y: 5, z: 1 });
|
| 7 |
-
game.startGame();
|
| 8 |
-
|
| 9 |
-
console.log("\n=== Testing 2D Capture ===");
|
| 10 |
-
|
| 11 |
-
// Check White stone at (3,2,0) neighbors
|
| 12 |
-
game.drop({ x: 2, y: 2, z: 0 }); // Black
|
| 13 |
-
game.drop({ x: 3, y: 2, z: 0 }); // White (target)
|
| 14 |
-
console.log("White placed at (3,2,0)");
|
| 15 |
-
|
| 16 |
-
game.drop({ x: 4, y: 2, z: 0 }); // Black (right of white)
|
| 17 |
-
console.log("After Black at (4,2,0) - right of white");
|
| 18 |
-
|
| 19 |
-
game.drop({ x: 3, y: 1, z: 0 }); // White elsewhere
|
| 20 |
-
game.drop({ x: 3, y: 3, z: 0 }); // Black (bottom of white)
|
| 21 |
-
console.log("After Black at (3,3,0) - bottom of white");
|
| 22 |
-
|
| 23 |
-
game.drop({ x: 1, y: 1, z: 0 }); // White elsewhere
|
| 24 |
-
|
| 25 |
-
console.log("\nBefore final move:");
|
| 26 |
-
console.log(" board[2][2][0] (left):", game.getBoard()[2][2][0], "BLACK=1");
|
| 27 |
-
console.log(" board[3][2][0] (white):", game.getBoard()[3][2][0], "WHITE=2");
|
| 28 |
-
console.log(" board[4][2][0] (right):", game.getBoard()[4][2][0], "BLACK=1");
|
| 29 |
-
console.log(" board[3][1][0] (top):", game.getBoard()[3][1][0], "WHITE=2");
|
| 30 |
-
console.log(" board[3][3][0] (bottom):", game.getBoard()[3][3][0], "BLACK=1");
|
| 31 |
-
console.log(" board[2][1][0] (top-left):", game.getBoard()[2][1][0], "EMPTY=0");
|
| 32 |
-
|
| 33 |
-
game.drop({ x: 2, y: 1, z: 0 }); // Black at (2,1,0) - top of white?
|
| 34 |
-
|
| 35 |
-
console.log("\nAfter final Black move at (2,1,0):");
|
| 36 |
-
console.log(
|
| 37 |
-
" board[3][2][0]:",
|
| 38 |
-
game.getBoard()[3][2][0],
|
| 39 |
-
"(should be EMPTY=0 if captured)"
|
| 40 |
-
);
|
| 41 |
-
console.log(" Last step capturedPositions:", game.getLastStep()?.capturedPositions);
|
| 42 |
-
console.log(" Captured counts:", game.getCapturedCounts());
|
| 43 |
-
});
|
| 44 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/debug_redo.test.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
import { describe, it } from "vitest";
|
| 2 |
-
import { TrigoGame, StoneType } from "@inc/trigo/game";
|
| 3 |
-
|
| 4 |
-
describe("Debug Redo After Jump", () => {
|
| 5 |
-
it("trace redo behavior", () => {
|
| 6 |
-
const game = new TrigoGame({ x: 5, y: 5, z: 1 });
|
| 7 |
-
game.startGame();
|
| 8 |
-
|
| 9 |
-
console.log("\n=== Initial state ===");
|
| 10 |
-
console.log(`getCurrentStep: ${game.getCurrentStep()}`);
|
| 11 |
-
console.log(`history length: ${game.getHistory().length}`);
|
| 12 |
-
console.log(`Expected: step 0 means NO moves applied`);
|
| 13 |
-
|
| 14 |
-
console.log("\n=== After first drop (2,2,0) ===");
|
| 15 |
-
game.drop({ x: 2, y: 2, z: 0 });
|
| 16 |
-
console.log(`getCurrentStep: ${game.getCurrentStep()}`);
|
| 17 |
-
console.log(`history length: ${game.getHistory().length}`);
|
| 18 |
-
console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]} (BLACK=1)`);
|
| 19 |
-
|
| 20 |
-
console.log("\n=== After second drop (3,2,0) ===");
|
| 21 |
-
game.drop({ x: 3, y: 2, z: 0 });
|
| 22 |
-
console.log(`getCurrentStep: ${game.getCurrentStep()}`);
|
| 23 |
-
console.log(`history length: ${game.getHistory().length}`);
|
| 24 |
-
console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]}`);
|
| 25 |
-
console.log(`board[3][2][0]: ${game.getBoard()[3][2][0]} (WHITE=2)`);
|
| 26 |
-
|
| 27 |
-
console.log("\n=== After jumpToStep(0) ===");
|
| 28 |
-
game.jumpToStep(0);
|
| 29 |
-
console.log(`getCurrentStep: ${game.getCurrentStep()}`);
|
| 30 |
-
console.log(`history length: ${game.getHistory().length}`);
|
| 31 |
-
console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]}`);
|
| 32 |
-
console.log(`board[3][2][0]: ${game.getBoard()[3][2][0]} (should be 0=EMPTY)`);
|
| 33 |
-
console.log(`canRedo: ${game.canRedo()}`);
|
| 34 |
-
|
| 35 |
-
console.log("\n=== After redo() ===");
|
| 36 |
-
const redoResult = game.redo();
|
| 37 |
-
console.log(`redo returned: ${redoResult}`);
|
| 38 |
-
console.log(`getCurrentStep: ${game.getCurrentStep()}`);
|
| 39 |
-
console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]}`);
|
| 40 |
-
console.log(`board[3][2][0]: ${game.getBoard()[3][2][0]} (expected WHITE=2)`);
|
| 41 |
-
});
|
| 42 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/trigoGame.core.test.ts
DELETED
|
@@ -1,300 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* TrigoGame Core Functionality Tests
|
| 3 |
-
*
|
| 4 |
-
* Tests basic game operations: initialization, moves, pass, surrender
|
| 5 |
-
*/
|
| 6 |
-
|
| 7 |
-
import { describe, it, expect, beforeEach } from "vitest";
|
| 8 |
-
import { TrigoGame, StoneType } from "@inc/trigo/game";
|
| 9 |
-
import type { BoardShape } from "@inc/trigo/types";
|
| 10 |
-
|
| 11 |
-
describe("TrigoGame - Core Functionality", () => {
|
| 12 |
-
let game: TrigoGame;
|
| 13 |
-
const defaultShape: BoardShape = { x: 5, y: 5, z: 5 };
|
| 14 |
-
|
| 15 |
-
beforeEach(() => {
|
| 16 |
-
game = new TrigoGame(defaultShape);
|
| 17 |
-
});
|
| 18 |
-
|
| 19 |
-
describe("Initialization", () => {
|
| 20 |
-
it("should initialize with empty board", () => {
|
| 21 |
-
const board = game.getBoard();
|
| 22 |
-
expect(board).toBeDefined();
|
| 23 |
-
expect(board.length).toBe(5);
|
| 24 |
-
expect(board[0].length).toBe(5);
|
| 25 |
-
expect(board[0][0].length).toBe(5);
|
| 26 |
-
});
|
| 27 |
-
|
| 28 |
-
it("should start with black player", () => {
|
| 29 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
|
| 30 |
-
});
|
| 31 |
-
|
| 32 |
-
it("should have initial game status as idle", () => {
|
| 33 |
-
expect(game.getGameStatus()).toBe("idle");
|
| 34 |
-
});
|
| 35 |
-
|
| 36 |
-
it("should have no moves initially", () => {
|
| 37 |
-
expect(game.getCurrentStep()).toBe(0);
|
| 38 |
-
expect(game.getHistory().length).toBe(0);
|
| 39 |
-
});
|
| 40 |
-
|
| 41 |
-
it("should initialize with correct board shape", () => {
|
| 42 |
-
const shape = game.getShape();
|
| 43 |
-
expect(shape).toEqual(defaultShape);
|
| 44 |
-
});
|
| 45 |
-
});
|
| 46 |
-
|
| 47 |
-
describe("Start Game", () => {
|
| 48 |
-
it("should change status from idle to playing", () => {
|
| 49 |
-
game.startGame();
|
| 50 |
-
expect(game.getGameStatus()).toBe("playing");
|
| 51 |
-
});
|
| 52 |
-
|
| 53 |
-
it("should not start if already playing", () => {
|
| 54 |
-
game.startGame();
|
| 55 |
-
expect(game.getGameStatus()).toBe("playing");
|
| 56 |
-
game.startGame(); // Try starting again
|
| 57 |
-
expect(game.getGameStatus()).toBe("playing");
|
| 58 |
-
});
|
| 59 |
-
});
|
| 60 |
-
|
| 61 |
-
describe("Drop Stone", () => {
|
| 62 |
-
beforeEach(() => {
|
| 63 |
-
game.startGame();
|
| 64 |
-
});
|
| 65 |
-
|
| 66 |
-
it("should place a black stone at position", () => {
|
| 67 |
-
const success = game.drop({ x: 2, y: 2, z: 2 });
|
| 68 |
-
expect(success).toBe(true);
|
| 69 |
-
|
| 70 |
-
const board = game.getBoard();
|
| 71 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK);
|
| 72 |
-
});
|
| 73 |
-
|
| 74 |
-
it("should switch to white player after black moves", () => {
|
| 75 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 76 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE);
|
| 77 |
-
});
|
| 78 |
-
|
| 79 |
-
it("should increment step count", () => {
|
| 80 |
-
expect(game.getCurrentStep()).toBe(0);
|
| 81 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 82 |
-
expect(game.getCurrentStep()).toBe(1);
|
| 83 |
-
});
|
| 84 |
-
|
| 85 |
-
it("should add move to history", () => {
|
| 86 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 87 |
-
const history = game.getHistory();
|
| 88 |
-
expect(history.length).toBe(1);
|
| 89 |
-
expect(history[0].position).toEqual({ x: 2, y: 2, z: 2 });
|
| 90 |
-
expect(history[0].player).toBe(StoneType.BLACK);
|
| 91 |
-
});
|
| 92 |
-
|
| 93 |
-
it("should not place stone on occupied position", () => {
|
| 94 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 95 |
-
const success = game.drop({ x: 2, y: 2, z: 2 });
|
| 96 |
-
expect(success).toBe(false);
|
| 97 |
-
});
|
| 98 |
-
|
| 99 |
-
it("should not place stone out of bounds", () => {
|
| 100 |
-
const success = game.drop({ x: 10, y: 10, z: 10 });
|
| 101 |
-
expect(success).toBe(false);
|
| 102 |
-
});
|
| 103 |
-
|
| 104 |
-
it("should allow alternating moves", () => {
|
| 105 |
-
game.drop({ x: 2, y: 2, z: 2 }); // Black
|
| 106 |
-
game.drop({ x: 3, y: 2, z: 2 }); // White
|
| 107 |
-
game.drop({ x: 2, y: 3, z: 2 }); // Black
|
| 108 |
-
|
| 109 |
-
const board = game.getBoard();
|
| 110 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK);
|
| 111 |
-
expect(board[3][2][2]).toBe(StoneType.WHITE);
|
| 112 |
-
expect(board[2][3][2]).toBe(StoneType.BLACK);
|
| 113 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE);
|
| 114 |
-
});
|
| 115 |
-
|
| 116 |
-
it("should reset pass count when stone is placed", () => {
|
| 117 |
-
game.pass(); // Black passes
|
| 118 |
-
expect(game.getPassCount()).toBe(1);
|
| 119 |
-
game.drop({ x: 2, y: 2, z: 2 }); // White places stone
|
| 120 |
-
expect(game.getPassCount()).toBe(0);
|
| 121 |
-
});
|
| 122 |
-
});
|
| 123 |
-
|
| 124 |
-
describe("Pass", () => {
|
| 125 |
-
beforeEach(() => {
|
| 126 |
-
game.startGame();
|
| 127 |
-
});
|
| 128 |
-
|
| 129 |
-
it("should allow player to pass", () => {
|
| 130 |
-
const success = game.pass();
|
| 131 |
-
expect(success).toBe(true);
|
| 132 |
-
});
|
| 133 |
-
|
| 134 |
-
it("should switch player after pass", () => {
|
| 135 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
|
| 136 |
-
game.pass();
|
| 137 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE);
|
| 138 |
-
});
|
| 139 |
-
|
| 140 |
-
it("should increment pass count", () => {
|
| 141 |
-
expect(game.getPassCount()).toBe(0);
|
| 142 |
-
game.pass();
|
| 143 |
-
expect(game.getPassCount()).toBe(1);
|
| 144 |
-
});
|
| 145 |
-
|
| 146 |
-
it("should add pass to history", () => {
|
| 147 |
-
game.pass();
|
| 148 |
-
const history = game.getHistory();
|
| 149 |
-
expect(history.length).toBe(1);
|
| 150 |
-
expect(history[0].type).toBe(1); // StepType.PASS
|
| 151 |
-
});
|
| 152 |
-
|
| 153 |
-
it("should end game after two consecutive passes", () => {
|
| 154 |
-
game.pass(); // Black passes
|
| 155 |
-
game.pass(); // White passes
|
| 156 |
-
|
| 157 |
-
expect(game.getGameStatus()).toBe("finished");
|
| 158 |
-
expect(game.getGameResult()).toBeDefined();
|
| 159 |
-
expect(game.getGameResult()?.reason).toBe("double-pass");
|
| 160 |
-
});
|
| 161 |
-
|
| 162 |
-
it("should not end game if pass not consecutive", () => {
|
| 163 |
-
game.pass(); // Black passes
|
| 164 |
-
game.drop({ x: 2, y: 2, z: 2 }); // White places stone
|
| 165 |
-
game.pass(); // Black passes again
|
| 166 |
-
|
| 167 |
-
expect(game.getGameStatus()).toBe("playing");
|
| 168 |
-
});
|
| 169 |
-
});
|
| 170 |
-
|
| 171 |
-
describe("Surrender", () => {
|
| 172 |
-
beforeEach(() => {
|
| 173 |
-
game.startGame();
|
| 174 |
-
});
|
| 175 |
-
|
| 176 |
-
it("should allow player to surrender", () => {
|
| 177 |
-
const success = game.surrender();
|
| 178 |
-
expect(success).toBe(true);
|
| 179 |
-
});
|
| 180 |
-
|
| 181 |
-
it("should end the game", () => {
|
| 182 |
-
game.surrender();
|
| 183 |
-
expect(game.getGameStatus()).toBe("finished");
|
| 184 |
-
});
|
| 185 |
-
|
| 186 |
-
it("should set winner as opponent", () => {
|
| 187 |
-
// Black surrenders
|
| 188 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
|
| 189 |
-
game.surrender();
|
| 190 |
-
|
| 191 |
-
const result = game.getGameResult();
|
| 192 |
-
expect(result).toBeDefined();
|
| 193 |
-
expect(result?.winner).toBe("white");
|
| 194 |
-
expect(result?.reason).toBe("resignation");
|
| 195 |
-
});
|
| 196 |
-
|
| 197 |
-
it("should add surrender to history", () => {
|
| 198 |
-
game.surrender();
|
| 199 |
-
const history = game.getHistory();
|
| 200 |
-
expect(history.length).toBe(1);
|
| 201 |
-
expect(history[0].type).toBe(2); // StepType.SURRENDER
|
| 202 |
-
});
|
| 203 |
-
});
|
| 204 |
-
|
| 205 |
-
describe("Reset", () => {
|
| 206 |
-
it("should reset game to initial state", () => {
|
| 207 |
-
game.startGame();
|
| 208 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 209 |
-
game.drop({ x: 3, y: 2, z: 2 });
|
| 210 |
-
game.pass();
|
| 211 |
-
|
| 212 |
-
game.reset();
|
| 213 |
-
|
| 214 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
|
| 215 |
-
expect(game.getCurrentStep()).toBe(0);
|
| 216 |
-
expect(game.getHistory().length).toBe(0);
|
| 217 |
-
expect(game.getGameStatus()).toBe("idle");
|
| 218 |
-
expect(game.getPassCount()).toBe(0);
|
| 219 |
-
|
| 220 |
-
// Board should be empty
|
| 221 |
-
const board = game.getBoard();
|
| 222 |
-
for (let x = 0; x < 5; x++) {
|
| 223 |
-
for (let y = 0; y < 5; y++) {
|
| 224 |
-
for (let z = 0; z < 5; z++) {
|
| 225 |
-
expect(board[x][y][z]).toBe(StoneType.EMPTY);
|
| 226 |
-
}
|
| 227 |
-
}
|
| 228 |
-
}
|
| 229 |
-
});
|
| 230 |
-
});
|
| 231 |
-
|
| 232 |
-
describe("Getters", () => {
|
| 233 |
-
beforeEach(() => {
|
| 234 |
-
game.startGame();
|
| 235 |
-
});
|
| 236 |
-
|
| 237 |
-
it("should get stone at position", () => {
|
| 238 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 239 |
-
const stone = game.getStone({ x: 2, y: 2, z: 2 });
|
| 240 |
-
expect(stone).toBe(StoneType.BLACK);
|
| 241 |
-
});
|
| 242 |
-
|
| 243 |
-
it("should return EMPTY for empty position", () => {
|
| 244 |
-
const stone = game.getStone({ x: 2, y: 2, z: 2 });
|
| 245 |
-
expect(stone).toBe(StoneType.EMPTY);
|
| 246 |
-
});
|
| 247 |
-
|
| 248 |
-
it("should get last step", () => {
|
| 249 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 250 |
-
const lastStep = game.getLastStep();
|
| 251 |
-
expect(lastStep).toBeDefined();
|
| 252 |
-
expect(lastStep?.position).toEqual({ x: 2, y: 2, z: 2 });
|
| 253 |
-
});
|
| 254 |
-
|
| 255 |
-
it("should return null for last step when no moves", () => {
|
| 256 |
-
const lastStep = game.getLastStep();
|
| 257 |
-
expect(lastStep).toBeNull();
|
| 258 |
-
});
|
| 259 |
-
|
| 260 |
-
it("should get captured counts", () => {
|
| 261 |
-
const counts = game.getCapturedCounts();
|
| 262 |
-
expect(counts).toEqual({ black: 0, white: 0 });
|
| 263 |
-
});
|
| 264 |
-
|
| 265 |
-
it("should get game stats", () => {
|
| 266 |
-
game.drop({ x: 2, y: 2, z: 2 }); // Black
|
| 267 |
-
game.drop({ x: 3, y: 2, z: 2 }); // White
|
| 268 |
-
|
| 269 |
-
const stats = game.getStats();
|
| 270 |
-
expect(stats.totalMoves).toBe(2);
|
| 271 |
-
expect(stats.blackMoves).toBe(1);
|
| 272 |
-
expect(stats.whiteMoves).toBe(1);
|
| 273 |
-
expect(stats.territory).toBeDefined();
|
| 274 |
-
});
|
| 275 |
-
});
|
| 276 |
-
|
| 277 |
-
describe("Validation", () => {
|
| 278 |
-
beforeEach(() => {
|
| 279 |
-
game.startGame();
|
| 280 |
-
});
|
| 281 |
-
|
| 282 |
-
it("should validate valid move", () => {
|
| 283 |
-
const result = game.isValidMove({ x: 2, y: 2, z: 2 });
|
| 284 |
-
expect(result.valid).toBe(true);
|
| 285 |
-
});
|
| 286 |
-
|
| 287 |
-
it("should invalidate out of bounds move", () => {
|
| 288 |
-
const result = game.isValidMove({ x: 10, y: 10, z: 10 });
|
| 289 |
-
expect(result.valid).toBe(false);
|
| 290 |
-
expect(result.reason).toContain("out of bounds");
|
| 291 |
-
});
|
| 292 |
-
|
| 293 |
-
it("should invalidate occupied position", () => {
|
| 294 |
-
game.drop({ x: 2, y: 2, z: 2 });
|
| 295 |
-
const result = game.isValidMove({ x: 2, y: 2, z: 2 });
|
| 296 |
-
expect(result.valid).toBe(false);
|
| 297 |
-
expect(result.reason).toContain("occupied");
|
| 298 |
-
});
|
| 299 |
-
});
|
| 300 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/trigoGame.fromTGN.test.ts
DELETED
|
@@ -1,319 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Tests for TrigoGame.fromTGN() - TGN Import Functionality
|
| 3 |
-
*/
|
| 4 |
-
|
| 5 |
-
import { describe, it, expect, beforeAll } from "vitest";
|
| 6 |
-
import { TrigoGame, StoneType, validateTGN, TGNParseError } from "@inc/trigo/game";
|
| 7 |
-
import { initializeParsers } from "@inc/trigo/parserInit";
|
| 8 |
-
|
| 9 |
-
describe("TrigoGame.fromTGN() - TGN Import", () => {
|
| 10 |
-
// Initialize parsers before running tests
|
| 11 |
-
beforeAll(async () => {
|
| 12 |
-
await initializeParsers();
|
| 13 |
-
});
|
| 14 |
-
|
| 15 |
-
describe("Basic TGN Parsing", () => {
|
| 16 |
-
it("should parse empty TGN with default board", () => {
|
| 17 |
-
const tgn = ``;
|
| 18 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 19 |
-
|
| 20 |
-
expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
|
| 21 |
-
expect(game.getGameStatus()).toBe("playing");
|
| 22 |
-
expect(game.getHistory()).toHaveLength(0);
|
| 23 |
-
});
|
| 24 |
-
|
| 25 |
-
it("should parse TGN with only metadata", () => {
|
| 26 |
-
const tgn = `
|
| 27 |
-
[Event "Test Game"]
|
| 28 |
-
[Black "Alice"]
|
| 29 |
-
[White "Bob"]
|
| 30 |
-
[Board 5x5x5]
|
| 31 |
-
`;
|
| 32 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 33 |
-
|
| 34 |
-
expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
|
| 35 |
-
expect(game.getGameStatus()).toBe("playing");
|
| 36 |
-
});
|
| 37 |
-
|
| 38 |
-
it("should parse TGN with 3x3x3 board", () => {
|
| 39 |
-
const tgn = `[Board 3x3x3]`;
|
| 40 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 41 |
-
|
| 42 |
-
expect(game.getShape()).toEqual({ x: 3, y: 3, z: 3 });
|
| 43 |
-
});
|
| 44 |
-
|
| 45 |
-
it("should parse TGN with 2D board (19x19)", () => {
|
| 46 |
-
const tgn = `[Board 19x19]`;
|
| 47 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 48 |
-
|
| 49 |
-
expect(game.getShape()).toEqual({ x: 19, y: 19, z: 1 });
|
| 50 |
-
});
|
| 51 |
-
});
|
| 52 |
-
|
| 53 |
-
describe("Move Parsing", () => {
|
| 54 |
-
it("should parse and replay simple move sequence", () => {
|
| 55 |
-
const tgn = `
|
| 56 |
-
[Board 5x5x5]
|
| 57 |
-
|
| 58 |
-
1. 000 y00
|
| 59 |
-
`;
|
| 60 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 61 |
-
|
| 62 |
-
expect(game.getHistory()).toHaveLength(2);
|
| 63 |
-
|
| 64 |
-
// Check first move (black at center)
|
| 65 |
-
const board = game.getBoard();
|
| 66 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK); // 000
|
| 67 |
-
|
| 68 |
-
// Check second move (white at y=3, 0=2, 0=2)
|
| 69 |
-
expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
|
| 70 |
-
});
|
| 71 |
-
|
| 72 |
-
it("should parse multiple rounds", () => {
|
| 73 |
-
const tgn = `
|
| 74 |
-
[Board 5x5x5]
|
| 75 |
-
|
| 76 |
-
1. 000 y00
|
| 77 |
-
2. 0aa zyy
|
| 78 |
-
`;
|
| 79 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 80 |
-
|
| 81 |
-
expect(game.getHistory()).toHaveLength(4);
|
| 82 |
-
|
| 83 |
-
const board = game.getBoard();
|
| 84 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK); // 000 = [2,2,2]
|
| 85 |
-
expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
|
| 86 |
-
expect(board[2][0][0]).toBe(StoneType.BLACK); // 0aa = [2,0,0]
|
| 87 |
-
expect(board[4][3][3]).toBe(StoneType.WHITE); // zyy = [4,3,3]
|
| 88 |
-
});
|
| 89 |
-
|
| 90 |
-
it("should parse pass move", () => {
|
| 91 |
-
const tgn = `
|
| 92 |
-
[Board 5x5x5]
|
| 93 |
-
|
| 94 |
-
1. 000 Pass
|
| 95 |
-
`;
|
| 96 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 97 |
-
|
| 98 |
-
expect(game.getHistory()).toHaveLength(2);
|
| 99 |
-
|
| 100 |
-
const history = game.getHistory();
|
| 101 |
-
expect(history[0].position).toEqual({ x: 2, y: 2, z: 2 });
|
| 102 |
-
expect(history[1].position).toBeUndefined();
|
| 103 |
-
});
|
| 104 |
-
|
| 105 |
-
it("should parse resign move", () => {
|
| 106 |
-
const tgn = `
|
| 107 |
-
[Board 5x5x5]
|
| 108 |
-
|
| 109 |
-
1. 000 Resign
|
| 110 |
-
`;
|
| 111 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 112 |
-
|
| 113 |
-
expect(game.getHistory()).toHaveLength(2);
|
| 114 |
-
expect(game.getGameStatus()).toBe("finished");
|
| 115 |
-
});
|
| 116 |
-
|
| 117 |
-
it("should handle incomplete round (black only)", () => {
|
| 118 |
-
const tgn = `
|
| 119 |
-
[Board 5x5x5]
|
| 120 |
-
|
| 121 |
-
1. 000 y00
|
| 122 |
-
2. 0aa
|
| 123 |
-
`;
|
| 124 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 125 |
-
|
| 126 |
-
expect(game.getHistory()).toHaveLength(3);
|
| 127 |
-
|
| 128 |
-
const board = game.getBoard();
|
| 129 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK); // 000 = [2,2,2]
|
| 130 |
-
expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
|
| 131 |
-
expect(board[2][0][0]).toBe(StoneType.BLACK); // 0aa = [2,0,0]
|
| 132 |
-
});
|
| 133 |
-
});
|
| 134 |
-
|
| 135 |
-
describe("2D Board Games", () => {
|
| 136 |
-
it("should parse 2D game correctly", () => {
|
| 137 |
-
const tgn = `
|
| 138 |
-
[Board 9x9]
|
| 139 |
-
|
| 140 |
-
1. 00 y0
|
| 141 |
-
2. 0a zy
|
| 142 |
-
`;
|
| 143 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 144 |
-
|
| 145 |
-
expect(game.getShape()).toEqual({ x: 9, y: 9, z: 1 });
|
| 146 |
-
expect(game.getHistory()).toHaveLength(4);
|
| 147 |
-
|
| 148 |
-
const board = game.getBoard();
|
| 149 |
-
expect(board[4][4][0]).toBe(StoneType.BLACK); // 00 = [4,4,0] (center of 9x9)
|
| 150 |
-
expect(board[7][4][0]).toBe(StoneType.WHITE); // y0 = [7,4,0] (y=7 for 9x9)
|
| 151 |
-
});
|
| 152 |
-
});
|
| 153 |
-
|
| 154 |
-
describe("Complete Game Examples", () => {
|
| 155 |
-
it("should parse complete game with metadata and moves", () => {
|
| 156 |
-
const tgn = `
|
| 157 |
-
[Event "World Championship"]
|
| 158 |
-
[Site "Tokyo"]
|
| 159 |
-
[Date "2025.10.31"]
|
| 160 |
-
[Black "Alice"]
|
| 161 |
-
[White "Bob"]
|
| 162 |
-
[Board 5x5x5]
|
| 163 |
-
[Rules "Chinese"]
|
| 164 |
-
[Application "Trigo v1.0"]
|
| 165 |
-
|
| 166 |
-
1. 000 y00
|
| 167 |
-
2. 0y0 yy0
|
| 168 |
-
3. aaa Pass
|
| 169 |
-
`;
|
| 170 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 171 |
-
|
| 172 |
-
expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
|
| 173 |
-
expect(game.getHistory()).toHaveLength(6);
|
| 174 |
-
|
| 175 |
-
const board = game.getBoard();
|
| 176 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK); // 000 = [2,2,2]
|
| 177 |
-
expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
|
| 178 |
-
expect(board[2][3][2]).toBe(StoneType.BLACK); // 0y0 = [2,3,2]
|
| 179 |
-
expect(board[3][3][2]).toBe(StoneType.WHITE); // yy0 = [3,3,2]
|
| 180 |
-
expect(board[0][0][0]).toBe(StoneType.BLACK); // aaa = [0,0,0]
|
| 181 |
-
});
|
| 182 |
-
|
| 183 |
-
it("should handle game with early resignation", () => {
|
| 184 |
-
const tgn = `
|
| 185 |
-
[Board 5x5x5]
|
| 186 |
-
|
| 187 |
-
1. 000 y00
|
| 188 |
-
2. Resign
|
| 189 |
-
`;
|
| 190 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 191 |
-
|
| 192 |
-
expect(game.getHistory()).toHaveLength(3);
|
| 193 |
-
expect(game.getGameStatus()).toBe("finished");
|
| 194 |
-
|
| 195 |
-
const result = game.getGameResult();
|
| 196 |
-
expect(result?.winner).toBe("white");
|
| 197 |
-
expect(result?.reason).toBe("resignation");
|
| 198 |
-
});
|
| 199 |
-
});
|
| 200 |
-
|
| 201 |
-
describe("TGN Validation", () => {
|
| 202 |
-
it("should validate correct TGN", () => {
|
| 203 |
-
const tgn = `
|
| 204 |
-
[Board 5x5x5]
|
| 205 |
-
|
| 206 |
-
1. 000 y00
|
| 207 |
-
`;
|
| 208 |
-
const result = validateTGN(tgn);
|
| 209 |
-
|
| 210 |
-
expect(result.valid).toBe(true);
|
| 211 |
-
expect(result.error).toBeUndefined();
|
| 212 |
-
});
|
| 213 |
-
|
| 214 |
-
it("should detect invalid TGN", () => {
|
| 215 |
-
const tgn = `[Board invalid]`;
|
| 216 |
-
const result = validateTGN(tgn);
|
| 217 |
-
|
| 218 |
-
expect(result.valid).toBe(false);
|
| 219 |
-
expect(result.error).toBeDefined();
|
| 220 |
-
});
|
| 221 |
-
|
| 222 |
-
it("should throw TGNParseError on invalid input", () => {
|
| 223 |
-
const tgn = `[Invalid Tag Without Value`;
|
| 224 |
-
|
| 225 |
-
expect(() => TrigoGame.fromTGN(tgn)).toThrow();
|
| 226 |
-
});
|
| 227 |
-
});
|
| 228 |
-
|
| 229 |
-
describe("Roundtrip Testing", () => {
|
| 230 |
-
it("should roundtrip: export to TGN and reimport", () => {
|
| 231 |
-
// Create original game
|
| 232 |
-
const game1 = new TrigoGame({ x: 5, y: 5, z: 5 });
|
| 233 |
-
game1.startGame();
|
| 234 |
-
game1.drop({ x: 2, y: 2, z: 2 }); // 000
|
| 235 |
-
game1.drop({ x: 4, y: 2, z: 2 }); // y00
|
| 236 |
-
game1.drop({ x: 2, y: 4, z: 2 }); // 0y0
|
| 237 |
-
game1.pass();
|
| 238 |
-
|
| 239 |
-
// Export to TGN
|
| 240 |
-
const tgn = game1.toTGN({
|
| 241 |
-
event: "Roundtrip Test",
|
| 242 |
-
black: "Player 1",
|
| 243 |
-
white: "Player 2"
|
| 244 |
-
});
|
| 245 |
-
|
| 246 |
-
// Import from TGN
|
| 247 |
-
const game2 = TrigoGame.fromTGN(tgn);
|
| 248 |
-
|
| 249 |
-
// Verify board state matches
|
| 250 |
-
const board1 = game1.getBoard();
|
| 251 |
-
const board2 = game2.getBoard();
|
| 252 |
-
|
| 253 |
-
for (let x = 0; x < 5; x++) {
|
| 254 |
-
for (let y = 0; y < 5; y++) {
|
| 255 |
-
for (let z = 0; z < 5; z++) {
|
| 256 |
-
expect(board2[x][y][z]).toBe(board1[x][y][z]);
|
| 257 |
-
}
|
| 258 |
-
}
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
// Verify step history length
|
| 262 |
-
expect(game2.getHistory()).toHaveLength(game1.getHistory().length);
|
| 263 |
-
});
|
| 264 |
-
|
| 265 |
-
it("should roundtrip complex game", () => {
|
| 266 |
-
// Create a more complex game
|
| 267 |
-
const game1 = new TrigoGame({ x: 3, y: 3, z: 3 });
|
| 268 |
-
game1.startGame();
|
| 269 |
-
|
| 270 |
-
// Play several moves
|
| 271 |
-
game1.drop({ x: 1, y: 1, z: 1 }); // Center
|
| 272 |
-
game1.drop({ x: 0, y: 0, z: 0 });
|
| 273 |
-
game1.drop({ x: 2, y: 2, z: 2 });
|
| 274 |
-
game1.drop({ x: 0, y: 2, z: 0 });
|
| 275 |
-
game1.pass();
|
| 276 |
-
game1.drop({ x: 1, y: 0, z: 1 });
|
| 277 |
-
|
| 278 |
-
const tgn = game1.toTGN({
|
| 279 |
-
application: "Test Suite"
|
| 280 |
-
});
|
| 281 |
-
|
| 282 |
-
const game2 = TrigoGame.fromTGN(tgn);
|
| 283 |
-
|
| 284 |
-
// Boards should be identical
|
| 285 |
-
const board1 = game1.getBoard();
|
| 286 |
-
const board2 = game2.getBoard();
|
| 287 |
-
|
| 288 |
-
for (let x = 0; x < 3; x++) {
|
| 289 |
-
for (let y = 0; y < 3; y++) {
|
| 290 |
-
for (let z = 0; z < 3; z++) {
|
| 291 |
-
expect(board2[x][y][z]).toBe(board1[x][y][z]);
|
| 292 |
-
}
|
| 293 |
-
}
|
| 294 |
-
}
|
| 295 |
-
});
|
| 296 |
-
});
|
| 297 |
-
|
| 298 |
-
describe("Error Handling", () => {
|
| 299 |
-
it("should handle invalid coordinates gracefully", () => {
|
| 300 |
-
const tgn = `
|
| 301 |
-
[Board 5x5x5]
|
| 302 |
-
|
| 303 |
-
1. xyz 000
|
| 304 |
-
`;
|
| 305 |
-
// Should throw an error during parsing (now synchronous)
|
| 306 |
-
expect(() => {
|
| 307 |
-
TrigoGame.fromTGN(tgn);
|
| 308 |
-
}).toThrow();
|
| 309 |
-
});
|
| 310 |
-
|
| 311 |
-
it("should handle invalid board shape", () => {
|
| 312 |
-
const tgn = `[Board notaboard]`;
|
| 313 |
-
|
| 314 |
-
// Parser should reject this
|
| 315 |
-
const result = validateTGN(tgn);
|
| 316 |
-
expect(result.valid).toBe(false);
|
| 317 |
-
});
|
| 318 |
-
});
|
| 319 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/trigoGame.history.test.ts
DELETED
|
@@ -1,301 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* TrigoGame History Management Tests
|
| 3 |
-
*
|
| 4 |
-
* Tests undo, redo, and jump-to-step functionality
|
| 5 |
-
*/
|
| 6 |
-
|
| 7 |
-
import { describe, it, expect, beforeEach } from 'vitest'
|
| 8 |
-
import { TrigoGame, StoneType } from '@inc/trigo/game'
|
| 9 |
-
import type { BoardShape } from '@inc/trigo/types'
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
describe('TrigoGame - History Management', () => {
|
| 13 |
-
let game: TrigoGame
|
| 14 |
-
const defaultShape: BoardShape = { x: 5, y: 5, z: 1 } // Use 2D board for capture tests
|
| 15 |
-
|
| 16 |
-
beforeEach(() => {
|
| 17 |
-
game = new TrigoGame(defaultShape)
|
| 18 |
-
game.startGame()
|
| 19 |
-
})
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
describe('Undo', () => {
|
| 23 |
-
it('should undo last move', () => {
|
| 24 |
-
game.drop({ x: 2, y: 2, z: 0 })
|
| 25 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 26 |
-
|
| 27 |
-
const success = game.undo()
|
| 28 |
-
expect(success).toBe(true)
|
| 29 |
-
expect(game.getCurrentStep()).toBe(0)
|
| 30 |
-
|
| 31 |
-
const board = game.getBoard()
|
| 32 |
-
expect(board[2][2][0]).toBe(StoneType.EMPTY)
|
| 33 |
-
})
|
| 34 |
-
|
| 35 |
-
it('should restore previous player after undo', () => {
|
| 36 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black moves
|
| 37 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
|
| 38 |
-
|
| 39 |
-
game.undo()
|
| 40 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
|
| 41 |
-
})
|
| 42 |
-
|
| 43 |
-
it('should not undo when at start', () => {
|
| 44 |
-
const success = game.undo()
|
| 45 |
-
expect(success).toBe(false)
|
| 46 |
-
})
|
| 47 |
-
|
| 48 |
-
it('should undo multiple moves', () => {
|
| 49 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black
|
| 50 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White
|
| 51 |
-
game.drop({ x: 2, y: 3, z: 0 }) // Black
|
| 52 |
-
|
| 53 |
-
expect(game.getCurrentStep()).toBe(3)
|
| 54 |
-
|
| 55 |
-
game.undo() // Undo black's move
|
| 56 |
-
expect(game.getCurrentStep()).toBe(2)
|
| 57 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
|
| 58 |
-
|
| 59 |
-
game.undo() // Undo white's move
|
| 60 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 61 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
|
| 62 |
-
|
| 63 |
-
game.undo() // Undo black's move
|
| 64 |
-
expect(game.getCurrentStep()).toBe(0)
|
| 65 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
|
| 66 |
-
})
|
| 67 |
-
|
| 68 |
-
it('should undo pass move', () => {
|
| 69 |
-
game.pass()
|
| 70 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 71 |
-
expect(game.getPassCount()).toBe(1)
|
| 72 |
-
|
| 73 |
-
game.undo()
|
| 74 |
-
expect(game.getCurrentStep()).toBe(0)
|
| 75 |
-
expect(game.getPassCount()).toBe(0)
|
| 76 |
-
})
|
| 77 |
-
|
| 78 |
-
it('should restore captured stones on undo', () => {
|
| 79 |
-
// Create a capture scenario: surround white stone with black
|
| 80 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black
|
| 81 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White (target)
|
| 82 |
-
game.drop({ x: 4, y: 2, z: 0 }) // Black (surrounds white)
|
| 83 |
-
game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
|
| 84 |
-
game.drop({ x: 3, y: 3, z: 0 }) // Black surrounds
|
| 85 |
-
game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
|
| 86 |
-
game.drop({ x: 3, y: 1, z: 0 }) // Black surrounds
|
| 87 |
-
|
| 88 |
-
// White stone should be captured
|
| 89 |
-
let board = game.getBoard()
|
| 90 |
-
expect(board[3][2][0]).toBe(StoneType.EMPTY)
|
| 91 |
-
|
| 92 |
-
// Undo last move
|
| 93 |
-
game.undo()
|
| 94 |
-
|
| 95 |
-
// White stone should be restored
|
| 96 |
-
board = game.getBoard()
|
| 97 |
-
expect(board[3][2][0]).toBe(StoneType.WHITE)
|
| 98 |
-
})
|
| 99 |
-
})
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
describe('Redo', () => {
|
| 103 |
-
it('should redo undone move', () => {
|
| 104 |
-
game.drop({ x: 2, y: 2, z: 0 })
|
| 105 |
-
game.undo()
|
| 106 |
-
|
| 107 |
-
const success = game.redo()
|
| 108 |
-
expect(success).toBe(true)
|
| 109 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 110 |
-
|
| 111 |
-
const board = game.getBoard()
|
| 112 |
-
expect(board[2][2][0]).toBe(StoneType.BLACK)
|
| 113 |
-
})
|
| 114 |
-
|
| 115 |
-
it('should not redo when at end of history', () => {
|
| 116 |
-
game.drop({ x: 2, y: 2, z: 0 })
|
| 117 |
-
const success = game.redo()
|
| 118 |
-
expect(success).toBe(false)
|
| 119 |
-
})
|
| 120 |
-
|
| 121 |
-
it('should redo multiple moves', () => {
|
| 122 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black
|
| 123 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White
|
| 124 |
-
game.drop({ x: 2, y: 3, z: 0 }) // Black
|
| 125 |
-
|
| 126 |
-
game.undo()
|
| 127 |
-
game.undo()
|
| 128 |
-
game.undo()
|
| 129 |
-
|
| 130 |
-
expect(game.getCurrentStep()).toBe(0)
|
| 131 |
-
|
| 132 |
-
game.redo()
|
| 133 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 134 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
|
| 135 |
-
|
| 136 |
-
game.redo()
|
| 137 |
-
expect(game.getCurrentStep()).toBe(2)
|
| 138 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
|
| 139 |
-
|
| 140 |
-
game.redo()
|
| 141 |
-
expect(game.getCurrentStep()).toBe(3)
|
| 142 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
|
| 143 |
-
})
|
| 144 |
-
|
| 145 |
-
it('should check canRedo correctly', () => {
|
| 146 |
-
expect(game.canRedo()).toBe(false)
|
| 147 |
-
|
| 148 |
-
game.drop({ x: 2, y: 2, z: 0 })
|
| 149 |
-
expect(game.canRedo()).toBe(false)
|
| 150 |
-
|
| 151 |
-
game.undo()
|
| 152 |
-
expect(game.canRedo()).toBe(true)
|
| 153 |
-
|
| 154 |
-
game.redo()
|
| 155 |
-
expect(game.canRedo()).toBe(false)
|
| 156 |
-
})
|
| 157 |
-
|
| 158 |
-
it('should truncate history when making new move after undo', () => {
|
| 159 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Move 1
|
| 160 |
-
game.drop({ x: 3, y: 2, z: 0 }) // Move 2
|
| 161 |
-
game.drop({ x: 2, y: 3, z: 0 }) // Move 3
|
| 162 |
-
|
| 163 |
-
game.undo() // Back to move 2
|
| 164 |
-
game.undo() // Back to move 1
|
| 165 |
-
|
| 166 |
-
// Make a new move - should truncate moves 2 and 3
|
| 167 |
-
game.drop({ x: 4, y: 4, z: 0 })
|
| 168 |
-
|
| 169 |
-
expect(game.getCurrentStep()).toBe(2)
|
| 170 |
-
expect(game.canRedo()).toBe(false)
|
| 171 |
-
|
| 172 |
-
const history = game.getHistory()
|
| 173 |
-
expect(history.length).toBe(2)
|
| 174 |
-
})
|
| 175 |
-
})
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
describe('Jump to Step', () => {
|
| 179 |
-
beforeEach(() => {
|
| 180 |
-
// Set up a game with several moves
|
| 181 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Move 0
|
| 182 |
-
game.drop({ x: 3, y: 2, z: 0 }) // Move 1
|
| 183 |
-
game.drop({ x: 2, y: 3, z: 0 }) // Move 2
|
| 184 |
-
game.drop({ x: 3, y: 3, z: 0 }) // Move 3
|
| 185 |
-
})
|
| 186 |
-
|
| 187 |
-
it('should jump to specific step in history', () => {
|
| 188 |
-
expect(game.getCurrentStep()).toBe(4)
|
| 189 |
-
|
| 190 |
-
const success = game.jumpToStep(1)
|
| 191 |
-
expect(success).toBe(true)
|
| 192 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 193 |
-
})
|
| 194 |
-
|
| 195 |
-
it('should jump to initial state with index 0', () => {
|
| 196 |
-
const success = game.jumpToStep(0)
|
| 197 |
-
expect(success).toBe(true)
|
| 198 |
-
expect(game.getCurrentStep()).toBe(0)
|
| 199 |
-
|
| 200 |
-
// Board should be empty
|
| 201 |
-
const board = game.getBoard()
|
| 202 |
-
expect(board[2][2][0]).toBe(StoneType.EMPTY)
|
| 203 |
-
expect(board[3][2][0]).toBe(StoneType.EMPTY)
|
| 204 |
-
|
| 205 |
-
// Current player should be BLACK (ready to make first move)
|
| 206 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
|
| 207 |
-
})
|
| 208 |
-
|
| 209 |
-
it('should rebuild board correctly at target step', () => {
|
| 210 |
-
game.jumpToStep(2) // After 2 moves
|
| 211 |
-
|
| 212 |
-
const board = game.getBoard()
|
| 213 |
-
expect(board[2][2][0]).toBe(StoneType.BLACK)
|
| 214 |
-
expect(board[3][2][0]).toBe(StoneType.WHITE)
|
| 215 |
-
expect(board[2][3][0]).toBe(StoneType.EMPTY) // Not placed yet
|
| 216 |
-
})
|
| 217 |
-
|
| 218 |
-
it('should set correct player after jump', () => {
|
| 219 |
-
game.jumpToStep(2) // After 2 moves (black, white)
|
| 220 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
|
| 221 |
-
|
| 222 |
-
game.jumpToStep(3) // After 3 moves
|
| 223 |
-
expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
|
| 224 |
-
})
|
| 225 |
-
|
| 226 |
-
it('should not jump to invalid index', () => {
|
| 227 |
-
const successNegative = game.jumpToStep(-5)
|
| 228 |
-
expect(successNegative).toBe(false)
|
| 229 |
-
|
| 230 |
-
const successTooLarge = game.jumpToStep(100)
|
| 231 |
-
expect(successTooLarge).toBe(false)
|
| 232 |
-
})
|
| 233 |
-
|
| 234 |
-
it('should do nothing when jumping to current step', () => {
|
| 235 |
-
const currentStep = game.getCurrentStep()
|
| 236 |
-
const success = game.jumpToStep(currentStep)
|
| 237 |
-
expect(success).toBe(false)
|
| 238 |
-
expect(game.getCurrentStep()).toBe(currentStep)
|
| 239 |
-
})
|
| 240 |
-
|
| 241 |
-
it('should allow jumping forward and backward', () => {
|
| 242 |
-
game.jumpToStep(1)
|
| 243 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 244 |
-
|
| 245 |
-
game.jumpToStep(3)
|
| 246 |
-
expect(game.getCurrentStep()).toBe(3)
|
| 247 |
-
|
| 248 |
-
game.jumpToStep(0)
|
| 249 |
-
expect(game.getCurrentStep()).toBe(0)
|
| 250 |
-
})
|
| 251 |
-
})
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
describe('Complex History Scenarios', () => {
|
| 255 |
-
it('should handle undo after jump', () => {
|
| 256 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Move 1
|
| 257 |
-
game.drop({ x: 3, y: 2, z: 0 }) // Move 2
|
| 258 |
-
|
| 259 |
-
game.jumpToStep(1) // Jump to to step 1 (after move 1)
|
| 260 |
-
expect(game.getCurrentStep()).toBe(1)
|
| 261 |
-
|
| 262 |
-
game.undo() // Should go back to start
|
| 263 |
-
|
| 264 |
-
expect(game.getCurrentStep()).toBe(0)
|
| 265 |
-
const board = game.getBoard()
|
| 266 |
-
expect(board[2][2][0]).toBe(StoneType.EMPTY)
|
| 267 |
-
})
|
| 268 |
-
|
| 269 |
-
it('should handle redo after jump', () => {
|
| 270 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Move 1
|
| 271 |
-
game.drop({ x: 3, y: 2, z: 0 }) // Move 2
|
| 272 |
-
|
| 273 |
-
game.jumpToStep(1) // Jump to to step 1 (after move 1)
|
| 274 |
-
game.redo() // Should move forward to move 2
|
| 275 |
-
|
| 276 |
-
expect(game.getCurrentStep()).toBe(2)
|
| 277 |
-
const board = game.getBoard()
|
| 278 |
-
expect(board[3][2][0]).toBe(StoneType.WHITE)
|
| 279 |
-
})
|
| 280 |
-
|
| 281 |
-
it('should maintain history integrity across operations', () => {
|
| 282 |
-
game.drop({ x: 2, y: 2, z: 0 })
|
| 283 |
-
game.drop({ x: 3, y: 2, z: 0 })
|
| 284 |
-
game.drop({ x: 2, y: 3, z: 0 })
|
| 285 |
-
|
| 286 |
-
const originalHistory = game.getHistory()
|
| 287 |
-
expect(originalHistory.length).toBe(3)
|
| 288 |
-
|
| 289 |
-
// Jump back and forth
|
| 290 |
-
game.jumpToStep(0)
|
| 291 |
-
game.jumpToStep(2)
|
| 292 |
-
game.undo()
|
| 293 |
-
game.redo()
|
| 294 |
-
|
| 295 |
-
// History should remain unchanged
|
| 296 |
-
const finalHistory = game.getHistory()
|
| 297 |
-
expect(finalHistory.length).toBe(3)
|
| 298 |
-
expect(finalHistory).toEqual(originalHistory)
|
| 299 |
-
})
|
| 300 |
-
})
|
| 301 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/trigoGame.parserInit.test.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Test for TGN Parser Initialization and Functionality
|
| 3 |
-
*/
|
| 4 |
-
|
| 5 |
-
import { describe, it, expect, beforeAll } from "vitest";
|
| 6 |
-
import { TrigoGame, StoneType, validateTGN } from "@inc/trigo/game";
|
| 7 |
-
import { initializeParsers } from "@inc/trigo/parserInit";
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
describe("TGN Parser - Full Integration (with synchronous API)", () => {
|
| 11 |
-
|
| 12 |
-
// Initialize parsers before running tests
|
| 13 |
-
beforeAll(async () => {
|
| 14 |
-
await initializeParsers();
|
| 15 |
-
});
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
describe("Parser Initialization", () => {
|
| 19 |
-
|
| 20 |
-
it("should initialize parsers successfully", async () => {
|
| 21 |
-
// If we get here without error, initialization succeeded
|
| 22 |
-
expect(true).toBe(true);
|
| 23 |
-
});
|
| 24 |
-
|
| 25 |
-
});
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
describe("Synchronous TGN Parsing", () => {
|
| 29 |
-
|
| 30 |
-
it("should validate TGN synchronously (no await)", () => {
|
| 31 |
-
const tgn = `[Board 5x5x5]
|
| 32 |
-
1. 000 y00`;
|
| 33 |
-
|
| 34 |
-
// This should NOT be an async function - should work synchronously
|
| 35 |
-
const result = validateTGN(tgn);
|
| 36 |
-
|
| 37 |
-
expect(result.valid).toBe(true);
|
| 38 |
-
expect(result.error).toBeUndefined();
|
| 39 |
-
});
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
it("should detect invalid TGN synchronously", () => {
|
| 43 |
-
const tgn = `[Board invalid]`;
|
| 44 |
-
|
| 45 |
-
// Synchronous validation
|
| 46 |
-
const result = validateTGN(tgn);
|
| 47 |
-
|
| 48 |
-
expect(result.valid).toBe(false);
|
| 49 |
-
expect(result.error).toBeDefined();
|
| 50 |
-
});
|
| 51 |
-
|
| 52 |
-
});
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
describe("Synchronous Game Import (fromTGN)", () => {
|
| 56 |
-
|
| 57 |
-
it("should import TGN synchronously (no await)", () => {
|
| 58 |
-
const tgn = `[Event "Test Game"]
|
| 59 |
-
[Board 5x5x5]
|
| 60 |
-
[Black "Alice"]
|
| 61 |
-
[White "Bob"]
|
| 62 |
-
|
| 63 |
-
1. 000 y00
|
| 64 |
-
2. 0y0 yy0`;
|
| 65 |
-
|
| 66 |
-
// This should NOT be an async function - should work synchronously
|
| 67 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 68 |
-
|
| 69 |
-
expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
|
| 70 |
-
expect(game.getHistory()).toHaveLength(4);
|
| 71 |
-
expect(game.getGameStatus()).toBe("playing");
|
| 72 |
-
});
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
it("should replay moves correctly after import", () => {
|
| 76 |
-
const tgn = `[Board 5x5x5]
|
| 77 |
-
1. 000 y00
|
| 78 |
-
2. 0y0 yy0`;
|
| 79 |
-
|
| 80 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 81 |
-
const board = game.getBoard();
|
| 82 |
-
|
| 83 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK); // 000
|
| 84 |
-
expect(board[3][2][2]).toBe(StoneType.WHITE); // y00
|
| 85 |
-
expect(board[2][3][2]).toBe(StoneType.BLACK); // 0y0
|
| 86 |
-
expect(board[3][3][2]).toBe(StoneType.WHITE); // yy0
|
| 87 |
-
});
|
| 88 |
-
|
| 89 |
-
});
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
describe("Round-trip Testing", () => {
|
| 93 |
-
|
| 94 |
-
it("should export and re-import TGN without loss", () => {
|
| 95 |
-
// Create original game
|
| 96 |
-
const game1 = new TrigoGame({ x: 5, y: 5, z: 5 });
|
| 97 |
-
game1.startGame();
|
| 98 |
-
game1.drop({ x: 2, y: 2, z: 2 });
|
| 99 |
-
game1.drop({ x: 3, y: 2, z: 2 });
|
| 100 |
-
game1.drop({ x: 2, y: 3, z: 2 });
|
| 101 |
-
|
| 102 |
-
// Export to TGN
|
| 103 |
-
const tgn = game1.toTGN({
|
| 104 |
-
event: "Test",
|
| 105 |
-
black: "Alice",
|
| 106 |
-
white: "Bob"
|
| 107 |
-
});
|
| 108 |
-
|
| 109 |
-
// Re-import - using synchronous fromTGN (no await!)
|
| 110 |
-
const game2 = TrigoGame.fromTGN(tgn);
|
| 111 |
-
|
| 112 |
-
// Verify boards match
|
| 113 |
-
const board1 = game1.getBoard();
|
| 114 |
-
const board2 = game2.getBoard();
|
| 115 |
-
|
| 116 |
-
for (let x = 0; x < 5; x++) {
|
| 117 |
-
for (let y = 0; y < 5; y++) {
|
| 118 |
-
for (let z = 0; z < 5; z++) {
|
| 119 |
-
expect(board2[x][y][z]).toBe(board1[x][y][z]);
|
| 120 |
-
}
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
// Verify move counts match
|
| 125 |
-
expect(game2.getHistory()).toHaveLength(game1.getHistory().length);
|
| 126 |
-
});
|
| 127 |
-
|
| 128 |
-
});
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
describe("Lotus Architecture Compliance", () => {
|
| 132 |
-
|
| 133 |
-
it("should use pre-built parser from public/lib", () => {
|
| 134 |
-
// This test verifies that the parser was loaded successfully
|
| 135 |
-
// If it throws an error, the parser module was not initialized
|
| 136 |
-
const tgn = `[Board 5x5x5]`;
|
| 137 |
-
const result = validateTGN(tgn);
|
| 138 |
-
expect(result.valid).toBe(true);
|
| 139 |
-
});
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
it("should support both browser and Node.js equally", () => {
|
| 143 |
-
// The same synchronous API works in both environments
|
| 144 |
-
// Browser: imports from /lib/tgnParser.js (served by Vite)
|
| 145 |
-
// Node.js: imports from public/lib/tgnParser.js
|
| 146 |
-
|
| 147 |
-
const tgn = `[Board 5x5x5]
|
| 148 |
-
1. 000 y00`;
|
| 149 |
-
|
| 150 |
-
// No environment checks needed - just use the API
|
| 151 |
-
const result = validateTGN(tgn);
|
| 152 |
-
const game = TrigoGame.fromTGN(tgn);
|
| 153 |
-
|
| 154 |
-
expect(result.valid).toBe(true);
|
| 155 |
-
expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
|
| 156 |
-
});
|
| 157 |
-
|
| 158 |
-
});
|
| 159 |
-
|
| 160 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/trigoGame.rules.test.ts
DELETED
|
@@ -1,356 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* TrigoGame Rules Tests
|
| 3 |
-
*
|
| 4 |
-
* Tests capture logic, Ko rule, suicide rule, and territory calculation
|
| 5 |
-
*/
|
| 6 |
-
|
| 7 |
-
import { describe, it, expect, beforeEach } from 'vitest'
|
| 8 |
-
import { TrigoGame, StoneType } from '@inc/trigo/game'
|
| 9 |
-
import type { BoardShape } from '@inc/trigo/types'
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
describe('TrigoGame - Game Rules', () => {
|
| 13 |
-
let game: TrigoGame
|
| 14 |
-
const defaultShape: BoardShape = { x: 5, y: 5, z: 1 } // 2D board (single z layer)
|
| 15 |
-
|
| 16 |
-
beforeEach(() => {
|
| 17 |
-
game = new TrigoGame(defaultShape)
|
| 18 |
-
game.startGame()
|
| 19 |
-
})
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
describe('Capture Logic', () => {
|
| 23 |
-
it('should capture a surrounded stone (2D case)', () => {
|
| 24 |
-
// Surround a white stone with black on 2D board (z=0)
|
| 25 |
-
// B . B
|
| 26 |
-
// B W B
|
| 27 |
-
// . B .
|
| 28 |
-
|
| 29 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black left
|
| 30 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White (target)
|
| 31 |
-
game.drop({ x: 4, y: 2, z: 0 }) // Black right
|
| 32 |
-
game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
|
| 33 |
-
game.drop({ x: 3, y: 3, z: 0 }) // Black bottom
|
| 34 |
-
game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
|
| 35 |
-
game.drop({ x: 3, y: 1, z: 0 }) // Black top - completes capture
|
| 36 |
-
|
| 37 |
-
// White stone at (3,2,0) should be captured
|
| 38 |
-
const board = game.getBoard()
|
| 39 |
-
expect(board[3][2][0]).toBe(StoneType.EMPTY)
|
| 40 |
-
})
|
| 41 |
-
|
| 42 |
-
it('should track captured stones in history', () => {
|
| 43 |
-
// Simple capture scenario on 2D board
|
| 44 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black left
|
| 45 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White (target)
|
| 46 |
-
game.drop({ x: 4, y: 2, z: 0 }) // Black right
|
| 47 |
-
game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
|
| 48 |
-
game.drop({ x: 3, y: 3, z: 0 }) // Black bottom
|
| 49 |
-
game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
|
| 50 |
-
game.drop({ x: 3, y: 1, z: 0 }) // Black top - captures
|
| 51 |
-
|
| 52 |
-
const lastStep = game.getLastStep()
|
| 53 |
-
expect(lastStep).toBeDefined()
|
| 54 |
-
expect(lastStep?.capturedPositions).toBeDefined()
|
| 55 |
-
expect(lastStep?.capturedPositions?.length).toBeGreaterThan(0)
|
| 56 |
-
})
|
| 57 |
-
|
| 58 |
-
it('should update captured counts', () => {
|
| 59 |
-
// Capture one white stone on 2D board
|
| 60 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black left
|
| 61 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White (target)
|
| 62 |
-
game.drop({ x: 4, y: 2, z: 0 }) // Black right
|
| 63 |
-
game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
|
| 64 |
-
game.drop({ x: 3, y: 3, z: 0 }) // Black bottom
|
| 65 |
-
game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
|
| 66 |
-
game.drop({ x: 3, y: 1, z: 0 }) // Black top - captures
|
| 67 |
-
|
| 68 |
-
const counts = game.getCapturedCounts()
|
| 69 |
-
expect(counts.white).toBeGreaterThan(0)
|
| 70 |
-
})
|
| 71 |
-
|
| 72 |
-
it('should not capture stones with liberties', () => {
|
| 73 |
-
// Place stones but leave an escape route
|
| 74 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black
|
| 75 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White
|
| 76 |
-
game.drop({ x: 3, y: 3, z: 0 }) // Black
|
| 77 |
-
game.drop({ x: 4, y: 2, z: 0 }) // White still has liberties
|
| 78 |
-
|
| 79 |
-
const board = game.getBoard()
|
| 80 |
-
expect(board[3][2][0]).toBe(StoneType.WHITE) // Not captured
|
| 81 |
-
})
|
| 82 |
-
})
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
describe('Ko Rule (打劫)', () => {
|
| 86 |
-
// Ko Rule (from Wikipedia):
|
| 87 |
-
// "A special rule that prevents immediate repetition of position, by a short 'loop'
|
| 88 |
-
// in which a single stone is captured, and another single stone immediately taken back.
|
| 89 |
-
// The immediate recapture is forbidden, for ONE TURN only."
|
| 90 |
-
//
|
| 91 |
-
// Example Ko situation:
|
| 92 |
-
// Initial: After BLACK captures: After WHITE recaptures (ILLEGAL):
|
| 93 |
-
// . B . . B . . B .
|
| 94 |
-
// B W B --> B . B --> B W B (same as initial!)
|
| 95 |
-
// . W . . W . . W .
|
| 96 |
-
//
|
| 97 |
-
// The Ko rule implementation checks:
|
| 98 |
-
// 1. Previous move captured exactly ONE stone
|
| 99 |
-
// 2. Current move would capture exactly ONE stone
|
| 100 |
-
// 3. Player is placing at the previously captured position
|
| 101 |
-
// → If all true, it's a Ko violation (immediate recapture forbidden)
|
| 102 |
-
|
| 103 |
-
it('should detect Ko using TGN example: bb ab ca ba aa', () => {
|
| 104 |
-
// Real Ko example in TGN notation:
|
| 105 |
-
// 1. bb ab (BLACK at (1,1), WHITE at (0,1))
|
| 106 |
-
// 2. ca ba (BLACK at (2,0), WHITE at (1,0))
|
| 107 |
-
// 3. aa (BLACK at (0,0) - captures WHITE at ba)
|
| 108 |
-
// Then WHITE playing at ba would be Ko violation
|
| 109 |
-
//
|
| 110 |
-
// Board after moves 1-2: After move 3 (BLACK aa):
|
| 111 |
-
// a b c a b c
|
| 112 |
-
// a . W B (y=0) a B . B (y=0, WHITE captured)
|
| 113 |
-
// b W B . (y=1) b W B . (y=1)
|
| 114 |
-
|
| 115 |
-
game.drop({ x: 1, y: 1, z: 0 }) // Move 1: BLACK bb
|
| 116 |
-
game.drop({ x: 0, y: 1, z: 0 }) // Move 1: WHITE ab
|
| 117 |
-
game.drop({ x: 2, y: 0, z: 0 }) // Move 2: BLACK ca
|
| 118 |
-
game.drop({ x: 1, y: 0, z: 0 }) // Move 2: WHITE ba
|
| 119 |
-
|
| 120 |
-
// Before capture, verify WHITE at ba (1,0) is surrounded
|
| 121 |
-
let board = game.getBoard()
|
| 122 |
-
expect(board[1][0][0]).toBe(StoneType.WHITE) // WHITE at ba
|
| 123 |
-
|
| 124 |
-
// Move 3: BLACK aa captures WHITE at ba
|
| 125 |
-
game.drop({ x: 0, y: 0, z: 0 }) // BLACK aa
|
| 126 |
-
|
| 127 |
-
// Verify capture happened
|
| 128 |
-
board = game.getBoard()
|
| 129 |
-
expect(board[1][0][0]).toBe(StoneType.EMPTY) // ba is now empty
|
| 130 |
-
expect(board[0][0][0]).toBe(StoneType.BLACK) // aa has BLACK
|
| 131 |
-
|
| 132 |
-
const lastStep = game.getLastStep()
|
| 133 |
-
expect(lastStep?.capturedPositions).toBeDefined()
|
| 134 |
-
expect(lastStep?.capturedPositions?.length).toBe(1)
|
| 135 |
-
expect(lastStep?.capturedPositions?.[0]).toEqual({ x: 1, y: 0, z: 0 })
|
| 136 |
-
|
| 137 |
-
// Now if WHITE immediately plays at ba, it would capture BLACK at aa
|
| 138 |
-
// and recreate the original position - this is Ko!
|
| 139 |
-
const result = game.isValidMove({ x: 1, y: 0, z: 0 })
|
| 140 |
-
|
| 141 |
-
expect(result.valid).toBe(false)
|
| 142 |
-
expect(result.reason).toContain('Ko')
|
| 143 |
-
})
|
| 144 |
-
|
| 145 |
-
it('should allow recapture after intervening moves (Ko resolved)', () => {
|
| 146 |
-
// Same Ko setup as above
|
| 147 |
-
game.drop({ x: 1, y: 1, z: 0 }) // BLACK bb
|
| 148 |
-
game.drop({ x: 0, y: 1, z: 0 }) // WHITE ab
|
| 149 |
-
game.drop({ x: 2, y: 0, z: 0 }) // BLACK ca
|
| 150 |
-
game.drop({ x: 1, y: 0, z: 0 }) // WHITE ba
|
| 151 |
-
game.drop({ x: 0, y: 0, z: 0 }) // BLACK aa - captures WHITE at ba
|
| 152 |
-
|
| 153 |
-
// Per Ko rule: "immediate recapture is forbidden, for ONE TURN only"
|
| 154 |
-
// WHITE must play elsewhere first (Ko threat)
|
| 155 |
-
game.drop({ x: 3, y: 3, z: 0 }) // WHITE plays elsewhere
|
| 156 |
-
game.drop({ x: 4, y: 4, z: 0 }) // BLACK responds elsewhere
|
| 157 |
-
|
| 158 |
-
// After intervening moves, Ko is resolved
|
| 159 |
-
// WHITE can now play at ba
|
| 160 |
-
const result = game.isValidMove({ x: 1, y: 0, z: 0 })
|
| 161 |
-
|
| 162 |
-
expect(result.valid).toBe(true)
|
| 163 |
-
expect(result.reason).toBeUndefined()
|
| 164 |
-
})
|
| 165 |
-
|
| 166 |
-
it('should verify Ko rule logic after single-stone capture', () => {
|
| 167 |
-
// Additional test using the simple capture pattern
|
| 168 |
-
game.drop({ x: 2, y: 2, z: 0 }) // BLACK
|
| 169 |
-
game.drop({ x: 3, y: 2, z: 0 }) // WHITE (will be captured)
|
| 170 |
-
game.drop({ x: 4, y: 2, z: 0 }) // BLACK right
|
| 171 |
-
game.drop({ x: 0, y: 0, z: 0 }) // WHITE elsewhere
|
| 172 |
-
game.drop({ x: 3, y: 3, z: 0 }) // BLACK bottom
|
| 173 |
-
game.drop({ x: 0, y: 1, z: 0 }) // WHITE elsewhere
|
| 174 |
-
game.drop({ x: 3, y: 1, z: 0 }) // BLACK top - captures WHITE at (3,2,0)
|
| 175 |
-
|
| 176 |
-
// Verify capture happened and was recorded
|
| 177 |
-
const board = game.getBoard()
|
| 178 |
-
expect(board[3][2][0]).toBe(StoneType.EMPTY)
|
| 179 |
-
|
| 180 |
-
const lastStep = game.getLastStep()
|
| 181 |
-
expect(lastStep?.capturedPositions).toBeDefined()
|
| 182 |
-
expect(lastStep?.capturedPositions?.length).toBe(1)
|
| 183 |
-
expect(lastStep?.capturedPositions?.[0]).toEqual({ x: 3, y: 2, z: 0 })
|
| 184 |
-
|
| 185 |
-
// Move validation at the captured position should run Ko checks
|
| 186 |
-
// In this specific case, WHITE playing at (3,2,0) would be suicide (not Ko)
|
| 187 |
-
// because WHITE cannot capture any BLACK stones from that position
|
| 188 |
-
const result = game.isValidMove({ x: 3, y: 2, z: 0 })
|
| 189 |
-
expect(result.valid).toBe(false)
|
| 190 |
-
// Will be 'suicide' in this case, but Ko logic is still checked first
|
| 191 |
-
})
|
| 192 |
-
})
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
describe('Suicide Rule', () => {
|
| 196 |
-
it('should prevent suicide move (placing stone with no liberties)', () => {
|
| 197 |
-
// Surround a position completely with opponent's stones on 2D board
|
| 198 |
-
game.drop({ x: 1, y: 2, z: 0 }) // Black elsewhere
|
| 199 |
-
game.drop({ x: 2, y: 2, z: 0 }) // White
|
| 200 |
-
game.drop({ x: 1, y: 3, z: 0 }) // Black elsewhere
|
| 201 |
-
game.drop({ x: 4, y: 2, z: 0 }) // White
|
| 202 |
-
game.drop({ x: 1, y: 1, z: 0 }) // Black elsewhere
|
| 203 |
-
game.drop({ x: 3, y: 3, z: 0 }) // White
|
| 204 |
-
game.drop({ x: 1, y: 0, z: 0 }) // Black elsewhere
|
| 205 |
-
game.drop({ x: 3, y: 1, z: 0 }) // White
|
| 206 |
-
|
| 207 |
-
// Position (3,2,0) is now surrounded by white stones (4 neighbors on 2D)
|
| 208 |
-
// Black trying to play there would be suicide
|
| 209 |
-
const result = game.isValidMove({ x: 3, y: 2, z: 0 })
|
| 210 |
-
|
| 211 |
-
if (result.valid === false) {
|
| 212 |
-
expect(result.reason).toContain('suicide')
|
| 213 |
-
}
|
| 214 |
-
})
|
| 215 |
-
|
| 216 |
-
it('should allow move that captures opponent (not suicide)', () => {
|
| 217 |
-
// Set up a position where placing a stone captures opponent
|
| 218 |
-
// thereby creating liberties for itself
|
| 219 |
-
game.drop({ x: 2, y: 2, z: 0 }) // Black
|
| 220 |
-
game.drop({ x: 3, y: 2, z: 0 }) // White (will be captured)
|
| 221 |
-
game.drop({ x: 4, y: 2, z: 0 }) // Black
|
| 222 |
-
game.drop({ x: 3, y: 1, z: 0 }) // White elsewhere
|
| 223 |
-
game.drop({ x: 3, y: 3, z: 0 }) // Black
|
| 224 |
-
game.drop({ x: 1, y: 1, z: 0 }) // White elsewhere
|
| 225 |
-
|
| 226 |
-
// Black placing at (2,1,0) captures white, so it's not suicide
|
| 227 |
-
const result = game.isValidMove({ x: 2, y: 1, z: 0 })
|
| 228 |
-
expect(result.valid).toBe(true)
|
| 229 |
-
})
|
| 230 |
-
})
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
describe('Territory Calculation', () => {
|
| 234 |
-
it('should calculate territory for empty board', () => {
|
| 235 |
-
const territory = game.getTerritory()
|
| 236 |
-
expect(territory).toBeDefined()
|
| 237 |
-
expect(territory.black).toBe(0)
|
| 238 |
-
expect(territory.white).toBe(0)
|
| 239 |
-
expect(territory.neutral).toBeGreaterThan(0)
|
| 240 |
-
})
|
| 241 |
-
|
| 242 |
-
it('should calculate territory with some stones', () => {
|
| 243 |
-
// Place a few stones
|
| 244 |
-
game.drop({ x: 0, y: 0, z: 0 }) // Black
|
| 245 |
-
game.drop({ x: 4, y: 4, z: 4 }) // White
|
| 246 |
-
game.drop({ x: 0, y: 0, z: 1 }) // Black
|
| 247 |
-
game.drop({ x: 4, y: 4, z: 3 }) // White
|
| 248 |
-
|
| 249 |
-
const territory = game.getTerritory()
|
| 250 |
-
expect(territory).toBeDefined()
|
| 251 |
-
// With stones placed, some territory should be claimed
|
| 252 |
-
expect(territory.black + territory.white + territory.neutral).toBeGreaterThan(0)
|
| 253 |
-
})
|
| 254 |
-
|
| 255 |
-
it('should cache territory calculation', () => {
|
| 256 |
-
const territory1 = game.getTerritory()
|
| 257 |
-
const territory2 = game.getTerritory()
|
| 258 |
-
|
| 259 |
-
// Should return same result (cached)
|
| 260 |
-
expect(territory1).toEqual(territory2)
|
| 261 |
-
})
|
| 262 |
-
|
| 263 |
-
it('should invalidate territory cache after move', () => {
|
| 264 |
-
const territory1 = game.getTerritory()
|
| 265 |
-
|
| 266 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 267 |
-
|
| 268 |
-
const territory2 = game.getTerritory()
|
| 269 |
-
|
| 270 |
-
// Territory may have changed
|
| 271 |
-
// At minimum, it should recalculate (not use stale cache)
|
| 272 |
-
expect(territory2).toBeDefined()
|
| 273 |
-
})
|
| 274 |
-
})
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
describe('Game End Conditions', () => {
|
| 278 |
-
it('should end game and calculate winner on double pass', () => {
|
| 279 |
-
// Set up a game state
|
| 280 |
-
game.drop({ x: 0, y: 0, z: 0 }) // Black
|
| 281 |
-
game.drop({ x: 4, y: 4, z: 4 }) // White
|
| 282 |
-
|
| 283 |
-
// Double pass
|
| 284 |
-
game.pass() // Black
|
| 285 |
-
game.pass() // White
|
| 286 |
-
|
| 287 |
-
expect(game.getGameStatus()).toBe('finished')
|
| 288 |
-
const result = game.getGameResult()
|
| 289 |
-
expect(result).toBeDefined()
|
| 290 |
-
expect(result?.winner).toBeDefined()
|
| 291 |
-
expect(result?.reason).toBe('double-pass')
|
| 292 |
-
expect(result?.score).toBeDefined()
|
| 293 |
-
})
|
| 294 |
-
|
| 295 |
-
it('should declare winner based on territory', () => {
|
| 296 |
-
// Create a situation where black has more territory
|
| 297 |
-
game.drop({ x: 0, y: 0, z: 0 }) // Black
|
| 298 |
-
game.drop({ x: 0, y: 0, z: 1 }) // White
|
| 299 |
-
game.drop({ x: 1, y: 0, z: 0 }) // Black
|
| 300 |
-
game.drop({ x: 0, y: 1, z: 1 }) // White
|
| 301 |
-
game.drop({ x: 0, y: 1, z: 0 }) // Black
|
| 302 |
-
game.drop({ x: 1, y: 0, z: 1 }) // White
|
| 303 |
-
game.drop({ x: 1, y: 1, z: 0 }) // Black
|
| 304 |
-
|
| 305 |
-
game.pass()
|
| 306 |
-
game.pass()
|
| 307 |
-
|
| 308 |
-
const result = game.getGameResult()
|
| 309 |
-
expect(result?.winner).toBeDefined()
|
| 310 |
-
// Winner determined by territory + captured stones
|
| 311 |
-
})
|
| 312 |
-
|
| 313 |
-
it('should handle draw situation', () => {
|
| 314 |
-
// Empty board - should be a draw
|
| 315 |
-
game.pass()
|
| 316 |
-
game.pass()
|
| 317 |
-
|
| 318 |
-
const result = game.getGameResult()
|
| 319 |
-
expect(result).toBeDefined()
|
| 320 |
-
// In perfectly equal situation, could be draw
|
| 321 |
-
})
|
| 322 |
-
})
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
describe('Complex Scenarios', () => {
|
| 326 |
-
it('should handle multiple captures in one move', () => {
|
| 327 |
-
// This would require a specific board setup
|
| 328 |
-
// where one move captures multiple groups
|
| 329 |
-
// For now, test that the system handles it gracefully
|
| 330 |
-
|
| 331 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 332 |
-
const counts = game.getCapturedCounts()
|
| 333 |
-
expect(counts).toBeDefined()
|
| 334 |
-
})
|
| 335 |
-
|
| 336 |
-
it('should maintain game integrity across complex moves', () => {
|
| 337 |
-
// Play a realistic game sequence
|
| 338 |
-
const moves = [
|
| 339 |
-
{ x: 2, y: 2, z: 0 },
|
| 340 |
-
{ x: 3, y: 2, z: 0 },
|
| 341 |
-
{ x: 2, y: 3, z: 0 },
|
| 342 |
-
{ x: 3, y: 3, z: 0 },
|
| 343 |
-
{ x: 1, y: 2, z: 0 },
|
| 344 |
-
]
|
| 345 |
-
|
| 346 |
-
for (const move of moves) {
|
| 347 |
-
const success = game.drop(move)
|
| 348 |
-
expect(success).toBe(true)
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
// Game should be in valid state
|
| 352 |
-
expect(game.getCurrentStep()).toBe(moves.length)
|
| 353 |
-
expect(game.getHistory().length).toBe(moves.length)
|
| 354 |
-
})
|
| 355 |
-
})
|
| 356 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/trigoGame.state.test.ts
DELETED
|
@@ -1,406 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* TrigoGame State Management and Persistence Tests
|
| 3 |
-
*
|
| 4 |
-
* Tests game status, serialization, and session storage
|
| 5 |
-
*/
|
| 6 |
-
|
| 7 |
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
| 8 |
-
import { TrigoGame, StoneType } from '@inc/trigo/game'
|
| 9 |
-
import type { BoardShape } from '@inc/trigo/types'
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
describe('TrigoGame - State Management', () => {
|
| 13 |
-
let game: TrigoGame
|
| 14 |
-
const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
|
| 15 |
-
|
| 16 |
-
beforeEach(() => {
|
| 17 |
-
game = new TrigoGame(defaultShape)
|
| 18 |
-
})
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
describe('Game Status', () => {
|
| 22 |
-
it('should start with idle status', () => {
|
| 23 |
-
expect(game.getGameStatus()).toBe('idle')
|
| 24 |
-
})
|
| 25 |
-
|
| 26 |
-
it('should change to playing when started', () => {
|
| 27 |
-
game.startGame()
|
| 28 |
-
expect(game.getGameStatus()).toBe('playing')
|
| 29 |
-
})
|
| 30 |
-
|
| 31 |
-
it('should change to finished on surrender', () => {
|
| 32 |
-
game.startGame()
|
| 33 |
-
game.surrender()
|
| 34 |
-
expect(game.getGameStatus()).toBe('finished')
|
| 35 |
-
})
|
| 36 |
-
|
| 37 |
-
it('should change to finished on double pass', () => {
|
| 38 |
-
game.startGame()
|
| 39 |
-
game.pass()
|
| 40 |
-
game.pass()
|
| 41 |
-
expect(game.getGameStatus()).toBe('finished')
|
| 42 |
-
})
|
| 43 |
-
|
| 44 |
-
it('should allow manual status change', () => {
|
| 45 |
-
game.setGameStatus('paused')
|
| 46 |
-
expect(game.getGameStatus()).toBe('paused')
|
| 47 |
-
})
|
| 48 |
-
|
| 49 |
-
it('should check if game is active', () => {
|
| 50 |
-
expect(game.isGameActive()).toBe(false)
|
| 51 |
-
game.startGame()
|
| 52 |
-
expect(game.isGameActive()).toBe(true)
|
| 53 |
-
game.surrender()
|
| 54 |
-
expect(game.isGameActive()).toBe(false)
|
| 55 |
-
})
|
| 56 |
-
})
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
describe('Game Result', () => {
|
| 60 |
-
beforeEach(() => {
|
| 61 |
-
game.startGame()
|
| 62 |
-
})
|
| 63 |
-
|
| 64 |
-
it('should have no result initially', () => {
|
| 65 |
-
expect(game.getGameResult()).toBeUndefined()
|
| 66 |
-
})
|
| 67 |
-
|
| 68 |
-
it('should set result on resignation', () => {
|
| 69 |
-
game.surrender()
|
| 70 |
-
const result = game.getGameResult()
|
| 71 |
-
|
| 72 |
-
expect(result).toBeDefined()
|
| 73 |
-
expect(result?.winner).toBe('white')
|
| 74 |
-
expect(result?.reason).toBe('resignation')
|
| 75 |
-
})
|
| 76 |
-
|
| 77 |
-
it('should set result on double pass', () => {
|
| 78 |
-
game.drop({ x: 0, y: 0, z: 0 }) // Black
|
| 79 |
-
game.pass() // White
|
| 80 |
-
game.pass() // Black
|
| 81 |
-
|
| 82 |
-
const result = game.getGameResult()
|
| 83 |
-
expect(result).toBeDefined()
|
| 84 |
-
expect(result?.reason).toBe('double-pass')
|
| 85 |
-
expect(result?.score).toBeDefined()
|
| 86 |
-
})
|
| 87 |
-
|
| 88 |
-
it('should include territory score in result', () => {
|
| 89 |
-
game.pass()
|
| 90 |
-
game.pass()
|
| 91 |
-
|
| 92 |
-
const result = game.getGameResult()
|
| 93 |
-
expect(result?.score).toBeDefined()
|
| 94 |
-
expect(result?.score?.black).toBeDefined()
|
| 95 |
-
expect(result?.score?.white).toBeDefined()
|
| 96 |
-
})
|
| 97 |
-
})
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
describe('Pass Count', () => {
|
| 101 |
-
beforeEach(() => {
|
| 102 |
-
game.startGame()
|
| 103 |
-
})
|
| 104 |
-
|
| 105 |
-
it('should start with zero passes', () => {
|
| 106 |
-
expect(game.getPassCount()).toBe(0)
|
| 107 |
-
})
|
| 108 |
-
|
| 109 |
-
it('should increment on pass', () => {
|
| 110 |
-
game.pass()
|
| 111 |
-
expect(game.getPassCount()).toBe(1)
|
| 112 |
-
|
| 113 |
-
game.pass()
|
| 114 |
-
expect(game.getPassCount()).toBe(2)
|
| 115 |
-
})
|
| 116 |
-
|
| 117 |
-
it('should reset on stone placement', () => {
|
| 118 |
-
game.pass()
|
| 119 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 120 |
-
expect(game.getPassCount()).toBe(0)
|
| 121 |
-
})
|
| 122 |
-
|
| 123 |
-
it('should maintain correct count across undo', () => {
|
| 124 |
-
game.pass()
|
| 125 |
-
expect(game.getPassCount()).toBe(1)
|
| 126 |
-
|
| 127 |
-
game.undo()
|
| 128 |
-
expect(game.getPassCount()).toBe(0)
|
| 129 |
-
})
|
| 130 |
-
})
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
describe('Callbacks', () => {
|
| 134 |
-
it('should trigger onStepAdvance callback', () => {
|
| 135 |
-
const onStepAdvance = vi.fn()
|
| 136 |
-
const gameWithCallbacks = new TrigoGame(defaultShape, { onStepAdvance })
|
| 137 |
-
|
| 138 |
-
gameWithCallbacks.startGame()
|
| 139 |
-
gameWithCallbacks.drop({ x: 2, y: 2, z: 2 })
|
| 140 |
-
|
| 141 |
-
expect(onStepAdvance).toHaveBeenCalledOnce()
|
| 142 |
-
})
|
| 143 |
-
|
| 144 |
-
it('should trigger onStepBack callback on undo', () => {
|
| 145 |
-
const onStepBack = vi.fn()
|
| 146 |
-
const gameWithCallbacks = new TrigoGame(defaultShape, { onStepBack })
|
| 147 |
-
|
| 148 |
-
gameWithCallbacks.startGame()
|
| 149 |
-
gameWithCallbacks.drop({ x: 2, y: 2, z: 2 })
|
| 150 |
-
gameWithCallbacks.undo()
|
| 151 |
-
|
| 152 |
-
expect(onStepBack).toHaveBeenCalledOnce()
|
| 153 |
-
})
|
| 154 |
-
|
| 155 |
-
it('should trigger onCapture callback', () => {
|
| 156 |
-
const onCapture = vi.fn()
|
| 157 |
-
const gameWithCallbacks = new TrigoGame(defaultShape, { onCapture })
|
| 158 |
-
|
| 159 |
-
gameWithCallbacks.startGame()
|
| 160 |
-
|
| 161 |
-
// Create capture scenario
|
| 162 |
-
gameWithCallbacks.drop({ x: 2, y: 2, z: 2 }) // Black
|
| 163 |
-
gameWithCallbacks.drop({ x: 3, y: 2, z: 2 }) // White (target)
|
| 164 |
-
gameWithCallbacks.drop({ x: 4, y: 2, z: 2 }) // Black
|
| 165 |
-
gameWithCallbacks.drop({ x: 3, y: 1, z: 2 }) // White
|
| 166 |
-
gameWithCallbacks.drop({ x: 3, y: 3, z: 2 }) // Black
|
| 167 |
-
gameWithCallbacks.drop({ x: 3, y: 1, z: 1 }) // White
|
| 168 |
-
gameWithCallbacks.drop({ x: 3, y: 2, z: 1 }) // Black
|
| 169 |
-
gameWithCallbacks.drop({ x: 3, y: 1, z: 3 }) // White
|
| 170 |
-
gameWithCallbacks.drop({ x: 3, y: 2, z: 3 }) // Black - captures
|
| 171 |
-
|
| 172 |
-
// onCapture should have been called if capture occurred
|
| 173 |
-
if (onCapture.mock.calls.length > 0) {
|
| 174 |
-
expect(onCapture).toHaveBeenCalled()
|
| 175 |
-
}
|
| 176 |
-
})
|
| 177 |
-
|
| 178 |
-
it('should trigger onWin callback on game end', () => {
|
| 179 |
-
const onWin = vi.fn()
|
| 180 |
-
const gameWithCallbacks = new TrigoGame(defaultShape, { onWin })
|
| 181 |
-
|
| 182 |
-
gameWithCallbacks.startGame()
|
| 183 |
-
gameWithCallbacks.surrender()
|
| 184 |
-
|
| 185 |
-
expect(onWin).toHaveBeenCalledOnce()
|
| 186 |
-
})
|
| 187 |
-
})
|
| 188 |
-
})
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
describe('TrigoGame - Serialization', () => {
|
| 192 |
-
let game: TrigoGame
|
| 193 |
-
const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
|
| 194 |
-
|
| 195 |
-
beforeEach(() => {
|
| 196 |
-
game = new TrigoGame(defaultShape)
|
| 197 |
-
game.startGame()
|
| 198 |
-
})
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
describe('JSON Serialization', () => {
|
| 202 |
-
it('should serialize game state', () => {
|
| 203 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 204 |
-
game.drop({ x: 3, y: 2, z: 2 })
|
| 205 |
-
|
| 206 |
-
const json = game.toJSON()
|
| 207 |
-
|
| 208 |
-
expect(json).toBeDefined()
|
| 209 |
-
expect(json).toHaveProperty('shape')
|
| 210 |
-
expect(json).toHaveProperty('currentPlayer')
|
| 211 |
-
expect(json).toHaveProperty('currentStepIndex')
|
| 212 |
-
expect(json).toHaveProperty('history')
|
| 213 |
-
expect(json).toHaveProperty('board')
|
| 214 |
-
expect(json).toHaveProperty('gameStatus')
|
| 215 |
-
expect(json).toHaveProperty('passCount')
|
| 216 |
-
})
|
| 217 |
-
|
| 218 |
-
it('should deserialize game state', () => {
|
| 219 |
-
// Set up a game state
|
| 220 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 221 |
-
game.drop({ x: 3, y: 2, z: 2 })
|
| 222 |
-
game.pass()
|
| 223 |
-
|
| 224 |
-
const json = game.toJSON()
|
| 225 |
-
|
| 226 |
-
// Create new game and load state
|
| 227 |
-
const newGame = new TrigoGame()
|
| 228 |
-
const success = newGame.fromJSON(json)
|
| 229 |
-
|
| 230 |
-
expect(success).toBe(true)
|
| 231 |
-
expect(newGame.getCurrentStep()).toBe(game.getCurrentStep())
|
| 232 |
-
expect(newGame.getCurrentPlayer()).toBe(game.getCurrentPlayer())
|
| 233 |
-
expect(newGame.getGameStatus()).toBe(game.getGameStatus())
|
| 234 |
-
expect(newGame.getPassCount()).toBe(game.getPassCount())
|
| 235 |
-
})
|
| 236 |
-
|
| 237 |
-
it('should preserve board state through serialization', () => {
|
| 238 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 239 |
-
game.drop({ x: 3, y: 2, z: 2 })
|
| 240 |
-
|
| 241 |
-
const originalBoard = game.getBoard()
|
| 242 |
-
const json = game.toJSON()
|
| 243 |
-
|
| 244 |
-
const newGame = new TrigoGame()
|
| 245 |
-
newGame.fromJSON(json)
|
| 246 |
-
const restoredBoard = newGame.getBoard()
|
| 247 |
-
|
| 248 |
-
// Check key positions
|
| 249 |
-
expect(restoredBoard[2][2][2]).toBe(originalBoard[2][2][2])
|
| 250 |
-
expect(restoredBoard[3][2][2]).toBe(originalBoard[3][2][2])
|
| 251 |
-
})
|
| 252 |
-
|
| 253 |
-
it('should preserve history through serialization', () => {
|
| 254 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 255 |
-
game.drop({ x: 3, y: 2, z: 2 })
|
| 256 |
-
|
| 257 |
-
const originalHistory = game.getHistory()
|
| 258 |
-
const json = game.toJSON()
|
| 259 |
-
|
| 260 |
-
const newGame = new TrigoGame()
|
| 261 |
-
newGame.fromJSON(json)
|
| 262 |
-
const restoredHistory = newGame.getHistory()
|
| 263 |
-
|
| 264 |
-
expect(restoredHistory.length).toBe(originalHistory.length)
|
| 265 |
-
})
|
| 266 |
-
|
| 267 |
-
it('should handle malformed JSON gracefully', () => {
|
| 268 |
-
const newGame = new TrigoGame()
|
| 269 |
-
const success = newGame.fromJSON({ invalid: 'data' })
|
| 270 |
-
|
| 271 |
-
// Should return false for invalid data
|
| 272 |
-
expect(success).toBe(false)
|
| 273 |
-
})
|
| 274 |
-
})
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
describe('Session Storage', () => {
|
| 278 |
-
// Mock sessionStorage for testing
|
| 279 |
-
const sessionStorageMock = (() => {
|
| 280 |
-
let store: Record<string, string> = {}
|
| 281 |
-
|
| 282 |
-
return {
|
| 283 |
-
getItem: (key: string) => store[key] || null,
|
| 284 |
-
setItem: (key: string, value: string) => { store[key] = value },
|
| 285 |
-
removeItem: (key: string) => { delete store[key] },
|
| 286 |
-
clear: () => { store = {} }
|
| 287 |
-
}
|
| 288 |
-
})()
|
| 289 |
-
|
| 290 |
-
beforeEach(() => {
|
| 291 |
-
// Set up globalThis.sessionStorage mock
|
| 292 |
-
if (typeof globalThis !== 'undefined') {
|
| 293 |
-
(globalThis as any).sessionStorage = sessionStorageMock
|
| 294 |
-
sessionStorageMock.clear()
|
| 295 |
-
}
|
| 296 |
-
})
|
| 297 |
-
|
| 298 |
-
it('should save to session storage', () => {
|
| 299 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 300 |
-
|
| 301 |
-
const success = game.saveToSessionStorage('testKey')
|
| 302 |
-
expect(success).toBe(true)
|
| 303 |
-
|
| 304 |
-
const saved = sessionStorageMock.getItem('testKey')
|
| 305 |
-
expect(saved).toBeDefined()
|
| 306 |
-
expect(saved).not.toBeNull()
|
| 307 |
-
})
|
| 308 |
-
|
| 309 |
-
it('should load from session storage', () => {
|
| 310 |
-
// Save game state
|
| 311 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 312 |
-
game.drop({ x: 3, y: 2, z: 2 })
|
| 313 |
-
game.saveToSessionStorage('testKey')
|
| 314 |
-
|
| 315 |
-
// Create new game and load
|
| 316 |
-
const newGame = new TrigoGame()
|
| 317 |
-
const success = newGame.loadFromSessionStorage('testKey')
|
| 318 |
-
|
| 319 |
-
expect(success).toBe(true)
|
| 320 |
-
expect(newGame.getCurrentStep()).toBe(2)
|
| 321 |
-
|
| 322 |
-
const board = newGame.getBoard()
|
| 323 |
-
expect(board[2][2][2]).toBe(StoneType.BLACK)
|
| 324 |
-
expect(board[3][2][2]).toBe(StoneType.WHITE)
|
| 325 |
-
})
|
| 326 |
-
|
| 327 |
-
it('should clear session storage', () => {
|
| 328 |
-
game.saveToSessionStorage('testKey')
|
| 329 |
-
|
| 330 |
-
game.clearSessionStorage('testKey')
|
| 331 |
-
|
| 332 |
-
const saved = sessionStorageMock.getItem('testKey')
|
| 333 |
-
expect(saved).toBeNull()
|
| 334 |
-
})
|
| 335 |
-
|
| 336 |
-
it('should return false when loading non-existent data', () => {
|
| 337 |
-
const newGame = new TrigoGame()
|
| 338 |
-
const success = newGame.loadFromSessionStorage('nonExistentKey')
|
| 339 |
-
|
| 340 |
-
expect(success).toBe(false)
|
| 341 |
-
})
|
| 342 |
-
|
| 343 |
-
it('should use default key when not specified', () => {
|
| 344 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 345 |
-
game.saveToSessionStorage() // No key specified
|
| 346 |
-
|
| 347 |
-
const saved = sessionStorageMock.getItem('trigoGameState')
|
| 348 |
-
expect(saved).toBeDefined()
|
| 349 |
-
})
|
| 350 |
-
})
|
| 351 |
-
})
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
describe('TrigoGame - Statistics', () => {
|
| 355 |
-
let game: TrigoGame
|
| 356 |
-
const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
|
| 357 |
-
|
| 358 |
-
beforeEach(() => {
|
| 359 |
-
game = new TrigoGame(defaultShape)
|
| 360 |
-
game.startGame()
|
| 361 |
-
})
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
describe('getStats', () => {
|
| 365 |
-
it('should return initial stats', () => {
|
| 366 |
-
const stats = game.getStats()
|
| 367 |
-
|
| 368 |
-
expect(stats.totalMoves).toBe(0)
|
| 369 |
-
expect(stats.blackMoves).toBe(0)
|
| 370 |
-
expect(stats.whiteMoves).toBe(0)
|
| 371 |
-
expect(stats.capturedByBlack).toBe(0)
|
| 372 |
-
expect(stats.capturedByWhite).toBe(0)
|
| 373 |
-
expect(stats.territory).toBeDefined()
|
| 374 |
-
})
|
| 375 |
-
|
| 376 |
-
it('should count moves correctly', () => {
|
| 377 |
-
game.drop({ x: 2, y: 2, z: 2 }) // Black
|
| 378 |
-
game.drop({ x: 3, y: 2, z: 2 }) // White
|
| 379 |
-
game.drop({ x: 2, y: 3, z: 2 }) // Black
|
| 380 |
-
|
| 381 |
-
const stats = game.getStats()
|
| 382 |
-
expect(stats.totalMoves).toBe(3)
|
| 383 |
-
expect(stats.blackMoves).toBe(2)
|
| 384 |
-
expect(stats.whiteMoves).toBe(1)
|
| 385 |
-
})
|
| 386 |
-
|
| 387 |
-
it('should not count pass moves in move counts', () => {
|
| 388 |
-
game.drop({ x: 2, y: 2, z: 2 }) // Black
|
| 389 |
-
game.pass() // White
|
| 390 |
-
game.drop({ x: 2, y: 3, z: 2 }) // Black
|
| 391 |
-
|
| 392 |
-
const stats = game.getStats()
|
| 393 |
-
expect(stats.blackMoves).toBe(2)
|
| 394 |
-
expect(stats.whiteMoves).toBe(0)
|
| 395 |
-
})
|
| 396 |
-
|
| 397 |
-
it('should include territory calculation', () => {
|
| 398 |
-
game.drop({ x: 0, y: 0, z: 0 })
|
| 399 |
-
|
| 400 |
-
const stats = game.getStats()
|
| 401 |
-
expect(stats.territory.black).toBeDefined()
|
| 402 |
-
expect(stats.territory.white).toBeDefined()
|
| 403 |
-
expect(stats.territory.neutral).toBeDefined()
|
| 404 |
-
})
|
| 405 |
-
})
|
| 406 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/trigoGame.tgn.test.ts
DELETED
|
@@ -1,229 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* TrigoGame TGN Export Tests
|
| 3 |
-
*
|
| 4 |
-
* Tests the TGN (Trigo Game Notation) export functionality
|
| 5 |
-
*/
|
| 6 |
-
|
| 7 |
-
import { describe, it, expect, beforeEach } from 'vitest'
|
| 8 |
-
import { TrigoGame, StoneType } from '@inc/trigo/game'
|
| 9 |
-
import type { BoardShape } from '@inc/trigo/types'
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
describe('TrigoGame - TGN Export', () => {
|
| 13 |
-
let game: TrigoGame
|
| 14 |
-
const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
beforeEach(() => {
|
| 18 |
-
game = new TrigoGame(defaultShape)
|
| 19 |
-
game.startGame()
|
| 20 |
-
})
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
describe('Basic TGN Export', () => {
|
| 24 |
-
it('should export empty game with board size', () => {
|
| 25 |
-
const tgn = game.toTGN()
|
| 26 |
-
|
| 27 |
-
expect(tgn).toContain('[Board 5x5x5]')
|
| 28 |
-
expect(tgn.trim()).not.toBe('')
|
| 29 |
-
})
|
| 30 |
-
|
| 31 |
-
it('should export 2D board format correctly', () => {
|
| 32 |
-
const game2d = new TrigoGame({ x: 19, y: 19, z: 1 })
|
| 33 |
-
game2d.startGame()
|
| 34 |
-
const tgn = game2d.toTGN()
|
| 35 |
-
|
| 36 |
-
expect(tgn).toContain('[Board 19x19]')
|
| 37 |
-
})
|
| 38 |
-
|
| 39 |
-
it('should export game with metadata', () => {
|
| 40 |
-
const tgn = game.toTGN({
|
| 41 |
-
event: 'World Championship',
|
| 42 |
-
site: 'Tokyo',
|
| 43 |
-
date: '2025.10.31',
|
| 44 |
-
black: 'Alice',
|
| 45 |
-
white: 'Bob'
|
| 46 |
-
})
|
| 47 |
-
|
| 48 |
-
expect(tgn).toContain('[Event "World Championship"]')
|
| 49 |
-
expect(tgn).toContain('[Site "Tokyo"]')
|
| 50 |
-
expect(tgn).toContain('[Date "2025.10.31"]')
|
| 51 |
-
expect(tgn).toContain('[Black "Alice"]')
|
| 52 |
-
expect(tgn).toContain('[White "Bob"]')
|
| 53 |
-
})
|
| 54 |
-
|
| 55 |
-
it('should include optional metadata tags', () => {
|
| 56 |
-
const tgn = game.toTGN({
|
| 57 |
-
rules: 'Chinese',
|
| 58 |
-
timeControl: '30+10',
|
| 59 |
-
application: 'Trigo v1.0'
|
| 60 |
-
})
|
| 61 |
-
|
| 62 |
-
expect(tgn).toContain('[Rules "Chinese"]')
|
| 63 |
-
expect(tgn).toContain('[TimeControl "30+10"]')
|
| 64 |
-
expect(tgn).toContain('[Application "Trigo v1.0"]')
|
| 65 |
-
})
|
| 66 |
-
})
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
describe('Move Sequence Export', () => {
|
| 70 |
-
it('should export simple move sequence', () => {
|
| 71 |
-
// Play some moves
|
| 72 |
-
game.drop({ x: 2, y: 2, z: 2 }) // Black at center - "000"
|
| 73 |
-
game.drop({ x: 3, y: 2, z: 2 }) // White - "y00"
|
| 74 |
-
game.drop({ x: 2, y: 3, z: 2 }) // Black - "0y0"
|
| 75 |
-
|
| 76 |
-
const tgn = game.toTGN()
|
| 77 |
-
|
| 78 |
-
expect(tgn).toContain('1. 000 y00')
|
| 79 |
-
expect(tgn).toContain('2. 0y0')
|
| 80 |
-
})
|
| 81 |
-
|
| 82 |
-
it('should export moves with correct coordinates', () => {
|
| 83 |
-
// Test corner positions
|
| 84 |
-
game.drop({ x: 0, y: 0, z: 0 }) // Black - "aaa"
|
| 85 |
-
game.drop({ x: 4, y: 4, z: 4 }) // White - "zzz"
|
| 86 |
-
|
| 87 |
-
const tgn = game.toTGN()
|
| 88 |
-
|
| 89 |
-
expect(tgn).toContain('1. aaa zzz')
|
| 90 |
-
})
|
| 91 |
-
|
| 92 |
-
it('should export pass moves', () => {
|
| 93 |
-
game.drop({ x: 2, y: 2, z: 2 }) // Black
|
| 94 |
-
game.pass() // White passes
|
| 95 |
-
game.drop({ x: 3, y: 2, z: 2 }) // Black
|
| 96 |
-
|
| 97 |
-
const tgn = game.toTGN()
|
| 98 |
-
|
| 99 |
-
expect(tgn).toContain('1. 000 Pass')
|
| 100 |
-
expect(tgn).toContain('2. y00')
|
| 101 |
-
})
|
| 102 |
-
|
| 103 |
-
it('should export multiple rounds correctly', () => {
|
| 104 |
-
// Play 4 moves (2 rounds)
|
| 105 |
-
game.drop({ x: 0, y: 0, z: 0 }) // 1. Black
|
| 106 |
-
game.drop({ x: 1, y: 0, z: 0 }) // 1. White
|
| 107 |
-
game.drop({ x: 0, y: 1, z: 0 }) // 2. Black
|
| 108 |
-
game.drop({ x: 1, y: 1, z: 0 }) // 2. White
|
| 109 |
-
|
| 110 |
-
const tgn = game.toTGN()
|
| 111 |
-
|
| 112 |
-
expect(tgn).toContain('1. aaa baa')
|
| 113 |
-
expect(tgn).toContain('2. aba bba')
|
| 114 |
-
})
|
| 115 |
-
})
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
describe('Game Result Export', () => {
|
| 119 |
-
it('should export resignation result', () => {
|
| 120 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 121 |
-
game.surrender() // White surrenders
|
| 122 |
-
|
| 123 |
-
const tgn = game.toTGN()
|
| 124 |
-
|
| 125 |
-
expect(tgn).toContain('[Result "B+Resign"]')
|
| 126 |
-
})
|
| 127 |
-
|
| 128 |
-
it('should export double pass result', () => {
|
| 129 |
-
game.drop({ x: 2, y: 2, z: 2 }) // Black
|
| 130 |
-
game.drop({ x: 3, y: 2, z: 2 }) // White
|
| 131 |
-
game.pass() // Black pass
|
| 132 |
-
game.pass() // White pass - game ends
|
| 133 |
-
|
| 134 |
-
const tgn = game.toTGN()
|
| 135 |
-
|
| 136 |
-
// Result should be present (winner determined by territory)
|
| 137 |
-
expect(tgn).toContain('[Result "')
|
| 138 |
-
})
|
| 139 |
-
})
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
describe('Complete Game Example', () => {
|
| 143 |
-
it('should export a complete game with all elements', () => {
|
| 144 |
-
const tgn = game.toTGN({
|
| 145 |
-
event: 'Test Game',
|
| 146 |
-
site: 'Local',
|
| 147 |
-
date: '2025.11.03',
|
| 148 |
-
black: 'Player 1',
|
| 149 |
-
white: 'Player 2',
|
| 150 |
-
rules: 'Chinese',
|
| 151 |
-
application: 'Trigo Test'
|
| 152 |
-
})
|
| 153 |
-
|
| 154 |
-
// Should have metadata section
|
| 155 |
-
expect(tgn).toContain('[Event "Test Game"]')
|
| 156 |
-
expect(tgn).toContain('[Site "Local"]')
|
| 157 |
-
expect(tgn).toContain('[Date "2025.11.03"]')
|
| 158 |
-
expect(tgn).toContain('[Black "Player 1"]')
|
| 159 |
-
expect(tgn).toContain('[White "Player 2"]')
|
| 160 |
-
expect(tgn).toContain('[Board 5x5x5]')
|
| 161 |
-
expect(tgn).toContain('[Rules "Chinese"]')
|
| 162 |
-
expect(tgn).toContain('[Application "Trigo Test"]')
|
| 163 |
-
|
| 164 |
-
// Should have empty line after metadata
|
| 165 |
-
const lines = tgn.split('\n')
|
| 166 |
-
const boardLineIndex = lines.findIndex(l => l.includes('[Board'))
|
| 167 |
-
const applicationLineIndex = lines.findIndex(l => l.includes('[Application'))
|
| 168 |
-
// Empty line after last metadata tag
|
| 169 |
-
expect(lines[applicationLineIndex + 1]).toBe('')
|
| 170 |
-
})
|
| 171 |
-
|
| 172 |
-
it('should format output with proper line breaks', () => {
|
| 173 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 174 |
-
game.drop({ x: 3, y: 2, z: 2 })
|
| 175 |
-
|
| 176 |
-
const tgn = game.toTGN()
|
| 177 |
-
const lines = tgn.split('\n')
|
| 178 |
-
|
| 179 |
-
// Should end with empty line
|
| 180 |
-
expect(lines[lines.length - 1]).toBe('')
|
| 181 |
-
|
| 182 |
-
// Should have moves on separate lines from metadata
|
| 183 |
-
const moveLines = lines.filter(l => l.match(/^\d+\./))
|
| 184 |
-
expect(moveLines.length).toBeGreaterThan(0)
|
| 185 |
-
})
|
| 186 |
-
})
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
describe('Edge Cases', () => {
|
| 190 |
-
it('should handle game with no moves', () => {
|
| 191 |
-
const tgn = game.toTGN()
|
| 192 |
-
|
| 193 |
-
expect(tgn).toContain('[Board 5x5x5]')
|
| 194 |
-
// Should not have any move numbers
|
| 195 |
-
expect(tgn).not.toMatch(/\d+\./)
|
| 196 |
-
})
|
| 197 |
-
|
| 198 |
-
it('should handle incomplete rounds', () => {
|
| 199 |
-
game.drop({ x: 2, y: 2, z: 2 }) // Only black's move
|
| 200 |
-
|
| 201 |
-
const tgn = game.toTGN()
|
| 202 |
-
|
| 203 |
-
expect(tgn).toContain('1. 000')
|
| 204 |
-
// Should not have white's move on this line
|
| 205 |
-
const lines = tgn.split('\n')
|
| 206 |
-
const moveLine = lines.find(l => l.startsWith('1.'))
|
| 207 |
-
expect(moveLine).not.toContain(' z') // No white move
|
| 208 |
-
})
|
| 209 |
-
|
| 210 |
-
it('should export TGN that can be read back', () => {
|
| 211 |
-
// Play a simple game
|
| 212 |
-
game.drop({ x: 2, y: 2, z: 2 })
|
| 213 |
-
game.drop({ x: 3, y: 2, z: 2 })
|
| 214 |
-
game.drop({ x: 2, y: 3, z: 2 })
|
| 215 |
-
game.drop({ x: 3, y: 3, z: 2 })
|
| 216 |
-
|
| 217 |
-
const tgn = game.toTGN({
|
| 218 |
-
black: 'Alice',
|
| 219 |
-
white: 'Bob'
|
| 220 |
-
})
|
| 221 |
-
|
| 222 |
-
// TGN should be valid format
|
| 223 |
-
expect(tgn).toContain('[Black "Alice"]')
|
| 224 |
-
expect(tgn).toContain('[White "Bob"]')
|
| 225 |
-
expect(tgn).toContain('1. 000 y00')
|
| 226 |
-
expect(tgn).toContain('2. 0y0 yy0')
|
| 227 |
-
})
|
| 228 |
-
})
|
| 229 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/game/verify_capture.test.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
import { describe, it } from 'vitest'
|
| 2 |
-
import { TrigoGame, StoneType } from '@inc/trigo/game'
|
| 3 |
-
|
| 4 |
-
describe('Verify Capture Logic', () => {
|
| 5 |
-
it('step by step capture verification', () => {
|
| 6 |
-
const game = new TrigoGame({ x: 5, y: 5, z: 1 })
|
| 7 |
-
game.startGame()
|
| 8 |
-
|
| 9 |
-
console.log("\n=== Step-by-step Capture Verification ===")
|
| 10 |
-
console.log("Target: White at (3, 2, 0)")
|
| 11 |
-
console.log("Need to surround with Black at:")
|
| 12 |
-
console.log(" Left: (2, 2, 0)")
|
| 13 |
-
console.log(" Right: (4, 2, 0)")
|
| 14 |
-
console.log(" Top: (3, 1, 0)")
|
| 15 |
-
console.log(" Bottom: (3, 3, 0)")
|
| 16 |
-
|
| 17 |
-
// Move 1: Black left
|
| 18 |
-
game.drop({ x: 2, y: 2, z: 0 })
|
| 19 |
-
console.log("\nMove 1: Black at (2, 2, 0) - LEFT of white")
|
| 20 |
-
|
| 21 |
-
// Move 2: White target
|
| 22 |
-
game.drop({ x: 3, y: 2, z: 0 })
|
| 23 |
-
console.log("Move 2: White at (3, 2, 0) - TARGET")
|
| 24 |
-
|
| 25 |
-
// Move 3: Black right
|
| 26 |
-
game.drop({ x: 4, y: 2, z: 0 })
|
| 27 |
-
console.log("Move 3: Black at (4, 2, 0) - RIGHT of white")
|
| 28 |
-
|
| 29 |
-
// Move 4: White elsewhere
|
| 30 |
-
game.drop({ x: 0, y: 0, z: 0 })
|
| 31 |
-
console.log("Move 4: White at (0, 0, 0) - elsewhere")
|
| 32 |
-
|
| 33 |
-
// Move 5: Black bottom
|
| 34 |
-
game.drop({ x: 3, y: 3, z: 0 })
|
| 35 |
-
console.log("Move 5: Black at (3, 3, 0) - BOTTOM of white")
|
| 36 |
-
|
| 37 |
-
// Move 6: White elsewhere
|
| 38 |
-
game.drop({ x: 0, y: 1, z: 0 })
|
| 39 |
-
console.log("Move 6: White at (0, 1, 0) - elsewhere")
|
| 40 |
-
|
| 41 |
-
console.log("\nBefore final move - White's neighbors:")
|
| 42 |
-
const board = game.getBoard()
|
| 43 |
-
console.log(` Left (2, 2, 0): ${board[2][2][0]} (${board[2][2][0] === StoneType.BLACK ? 'BLACK' : 'other'})`)
|
| 44 |
-
console.log(` Right (4, 2, 0): ${board[4][2][0]} (${board[4][2][0] === StoneType.BLACK ? 'BLACK' : 'other'})`)
|
| 45 |
-
console.log(` Top (3, 1, 0): ${board[3][1][0]} (${board[3][1][0] === StoneType.EMPTY ? 'EMPTY' : 'occupied'})`)
|
| 46 |
-
console.log(` Bottom (3, 3, 0): ${board[3][3][0]} (${board[3][3][0] === StoneType.BLACK ? 'BLACK' : 'other'})`)
|
| 47 |
-
console.log(` Target (3, 2, 0): ${board[3][2][0]} (WHITE=${StoneType.WHITE})`)
|
| 48 |
-
|
| 49 |
-
// Move 7: Black top - should capture
|
| 50 |
-
console.log("\nMove 7: Black at (3, 1, 0) - TOP of white (should capture)")
|
| 51 |
-
game.drop({ x: 3, y: 1, z: 0 })
|
| 52 |
-
|
| 53 |
-
console.log("\n=== After final move ===")
|
| 54 |
-
const finalBoard = game.getBoard()
|
| 55 |
-
console.log(`Target (3, 2, 0): ${finalBoard[3][2][0]} (should be 0=EMPTY if captured)`)
|
| 56 |
-
console.log(`Neighbors after capture:`)
|
| 57 |
-
console.log(` Left (2, 2, 0): ${finalBoard[2][2][0]}`)
|
| 58 |
-
console.log(` Right (4, 2, 0): ${finalBoard[4][2][0]}`)
|
| 59 |
-
console.log(` Top (3, 1, 0): ${finalBoard[3][1][0]}`)
|
| 60 |
-
console.log(` Bottom (3, 3, 0): ${finalBoard[3][3][0]}`)
|
| 61 |
-
|
| 62 |
-
const lastStep = game.getLastStep()
|
| 63 |
-
console.log(`\nCapture info:`)
|
| 64 |
-
console.log(` capturedPositions: ${JSON.stringify(lastStep?.capturedPositions)}`)
|
| 65 |
-
console.log(` captured count: ${game.getCapturedCounts().white}`)
|
| 66 |
-
|
| 67 |
-
// Verification
|
| 68 |
-
if (finalBoard[3][2][0] === StoneType.EMPTY) {
|
| 69 |
-
console.log("\n✅ SUCCESS: White was captured!")
|
| 70 |
-
} else {
|
| 71 |
-
console.log("\n❌ FAILED: White was NOT captured")
|
| 72 |
-
console.log("All neighbors should be BLACK:")
|
| 73 |
-
console.log(` Left: ${finalBoard[2][2][0] === StoneType.BLACK ? '✅' : '❌'}`)
|
| 74 |
-
console.log(` Right: ${finalBoard[4][2][0] === StoneType.BLACK ? '✅' : '❌'}`)
|
| 75 |
-
console.log(` Top: ${finalBoard[3][1][0] === StoneType.BLACK ? '✅' : '❌'}`)
|
| 76 |
-
console.log(` Bottom: ${finalBoard[3][3][0] === StoneType.BLACK ? '✅' : '❌'}`)
|
| 77 |
-
}
|
| 78 |
-
})
|
| 79 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/mctsTerminalPropagation.test.ts
DELETED
|
@@ -1,257 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Unit Tests for MCTS Terminal State Propagation
|
| 3 |
-
*
|
| 4 |
-
* Tests the minimax propagation logic where nodes with all terminal children
|
| 5 |
-
* are themselves marked as terminal.
|
| 6 |
-
*
|
| 7 |
-
* Based on GPT-5.1 review recommendations.
|
| 8 |
-
*/
|
| 9 |
-
|
| 10 |
-
import { describe, it, expect, beforeEach } from "vitest";
|
| 11 |
-
import { TrigoGame } from "../inc/trigo/game.js";
|
| 12 |
-
import { MCTSAgent } from "../inc/mctsAgent.js";
|
| 13 |
-
import { TrigoTreeAgent } from "../inc/trigoTreeAgent.js";
|
| 14 |
-
import { TrigoEvaluationAgent } from "../inc/trigoEvaluationAgent.js";
|
| 15 |
-
import { ModelInferencer } from "../inc/modelInferencer.js";
|
| 16 |
-
import type { Move } from "../inc/trigo/types.js";
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
/**
|
| 20 |
-
* Mock Tree Agent that returns uniform priors
|
| 21 |
-
*/
|
| 22 |
-
class MockTreeAgent extends TrigoTreeAgent {
|
| 23 |
-
constructor() {
|
| 24 |
-
super(null as any);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
async scoreMoves(game: TrigoGame, moves: Move[]): Promise<Array<{ move: Move; score: number }>> {
|
| 28 |
-
// Return uniform log probabilities
|
| 29 |
-
const logProb = Math.log(1.0 / moves.length);
|
| 30 |
-
return moves.map(move => ({ move, score: logProb }));
|
| 31 |
-
}
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
/**
|
| 36 |
-
* Mock Evaluation Agent that returns fixed values
|
| 37 |
-
*/
|
| 38 |
-
class MockEvaluationAgent extends TrigoEvaluationAgent {
|
| 39 |
-
private mockValue: number;
|
| 40 |
-
|
| 41 |
-
constructor(value: number = 0.0) {
|
| 42 |
-
super(null as any);
|
| 43 |
-
this.mockValue = value;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
setMockValue(value: number): void {
|
| 47 |
-
this.mockValue = value;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
async evaluatePosition(game: TrigoGame): Promise<{ value: number; interpretation: string }> {
|
| 51 |
-
return {
|
| 52 |
-
value: this.mockValue,
|
| 53 |
-
interpretation: this.mockValue > 0 ? "White advantage" : "Black advantage"
|
| 54 |
-
};
|
| 55 |
-
}
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
/**
|
| 60 |
-
* Helper to access private MCTSNode properties via type assertion
|
| 61 |
-
*/
|
| 62 |
-
interface MCTSNode {
|
| 63 |
-
state: TrigoGame | null;
|
| 64 |
-
parent: MCTSNode | null;
|
| 65 |
-
action: Move | null;
|
| 66 |
-
N: Map<string, number>;
|
| 67 |
-
W: Map<string, number>;
|
| 68 |
-
Q: Map<string, number>;
|
| 69 |
-
P: Map<string, number>;
|
| 70 |
-
children: Map<string, MCTSNode>;
|
| 71 |
-
expanded: boolean;
|
| 72 |
-
terminalValue: number | null;
|
| 73 |
-
depth: number;
|
| 74 |
-
playerToMove: number;
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
describe("MCTS Terminal Propagation", () => {
|
| 79 |
-
let game: TrigoGame;
|
| 80 |
-
let treeAgent: MockTreeAgent;
|
| 81 |
-
let evalAgent: MockEvaluationAgent;
|
| 82 |
-
let mctsAgent: MCTSAgent;
|
| 83 |
-
|
| 84 |
-
beforeEach(() => {
|
| 85 |
-
treeAgent = new MockTreeAgent();
|
| 86 |
-
evalAgent = new MockEvaluationAgent(0.0);
|
| 87 |
-
mctsAgent = new MCTSAgent(treeAgent, evalAgent, {
|
| 88 |
-
numSimulations: 10,
|
| 89 |
-
cPuct: 1.0,
|
| 90 |
-
temperature: 1.0,
|
| 91 |
-
dirichletAlpha: 0.03,
|
| 92 |
-
dirichletEpsilon: 0.0 // Disable Dirichlet noise for testing
|
| 93 |
-
});
|
| 94 |
-
});
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
describe("Test 1: Single-Step Endgame", () => {
|
| 98 |
-
it("should propagate terminal value when Black chooses between terminal children", async () => {
|
| 99 |
-
// Setup: 5x1x1 board with 2 empty positions (a and b)
|
| 100 |
-
// Black to move, can play at position 0 (index 2) or position 1 (index 1)
|
| 101 |
-
game = new TrigoGame({ x: 5, y: 1, z: 1 });
|
| 102 |
-
game.startGame();
|
| 103 |
-
|
| 104 |
-
// Simulate near-terminal game: Black at z(4), y(3); White at a(0), b(1)
|
| 105 |
-
// Remaining: position 2 (center '0')
|
| 106 |
-
game.drop({ x: 4, y: 0, z: 0 }); // Black at z
|
| 107 |
-
game.drop({ x: 0, y: 0, z: 0 }); // White at a
|
| 108 |
-
game.drop({ x: 3, y: 0, z: 0 }); // Black at y
|
| 109 |
-
game.drop({ x: 1, y: 0, z: 0 }); // White at b
|
| 110 |
-
|
| 111 |
-
// Now Black to move at position 2 (center)
|
| 112 |
-
// This will end the game
|
| 113 |
-
const result = await mctsAgent.selectMove(game, 5);
|
| 114 |
-
|
| 115 |
-
// Verify move was selected
|
| 116 |
-
expect(result.move).toBeDefined();
|
| 117 |
-
|
| 118 |
-
// Check that terminal propagation happened
|
| 119 |
-
// (Terminal value should be set on nodes)
|
| 120 |
-
expect(result.rootValue).toBeDefined();
|
| 121 |
-
});
|
| 122 |
-
});
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
describe("Test 2: Two-Step Propagation", () => {
|
| 126 |
-
it("should propagate terminal values up two levels", async () => {
|
| 127 |
-
// Setup: Near-terminal position
|
| 128 |
-
game = new TrigoGame({ x: 3, y: 1, z: 1 });
|
| 129 |
-
game.startGame();
|
| 130 |
-
|
| 131 |
-
// Black at position 0, White at position 2
|
| 132 |
-
// Remaining: position 1
|
| 133 |
-
game.drop({ x: 0, y: 0, z: 0 }); // Black
|
| 134 |
-
game.drop({ x: 2, y: 0, z: 0 }); // White
|
| 135 |
-
|
| 136 |
-
// Now Black to move - only one move possible
|
| 137 |
-
const result = await mctsAgent.selectMove(game, 3);
|
| 138 |
-
|
| 139 |
-
// After enough simulations, should mark nodes as terminal
|
| 140 |
-
expect(result.move).toBeDefined();
|
| 141 |
-
});
|
| 142 |
-
});
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
describe("Test 3: White vs Black Turn Correctness", () => {
|
| 146 |
-
it("should use max for White's turn and min for Black's turn", async () => {
|
| 147 |
-
// This is a conceptual test - we verify via the implementation
|
| 148 |
-
// White should maximize Q-values (white-positive)
|
| 149 |
-
// Black should minimize Q-values (white-positive)
|
| 150 |
-
|
| 151 |
-
// Setup simple 3x1x1 board
|
| 152 |
-
game = new TrigoGame({ x: 3, y: 1, z: 1 });
|
| 153 |
-
game.startGame();
|
| 154 |
-
|
| 155 |
-
// Run a few simulations
|
| 156 |
-
const result = await mctsAgent.selectMove(game, 1);
|
| 157 |
-
|
| 158 |
-
// Verify basic functionality works
|
| 159 |
-
expect(result.move).toBeDefined();
|
| 160 |
-
expect(result.visitCounts.size).toBeGreaterThan(0);
|
| 161 |
-
});
|
| 162 |
-
});
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
describe("Test 4: Terminal Leaf with No Actions", () => {
|
| 166 |
-
it("should handle terminal nodes with empty action sets", async () => {
|
| 167 |
-
// Create a finished game
|
| 168 |
-
game = new TrigoGame({ x: 3, y: 1, z: 1 });
|
| 169 |
-
game.startGame();
|
| 170 |
-
|
| 171 |
-
// Fill the board completely
|
| 172 |
-
game.drop({ x: 0, y: 0, z: 0 }); // Black
|
| 173 |
-
game.drop({ x: 1, y: 0, z: 0 }); // White
|
| 174 |
-
game.drop({ x: 2, y: 0, z: 0 }); // Black
|
| 175 |
-
|
| 176 |
-
// Game is now terminal (no more moves)
|
| 177 |
-
// Double-pass to finish
|
| 178 |
-
game.pass();
|
| 179 |
-
game.pass();
|
| 180 |
-
|
| 181 |
-
// Should return immediately without error
|
| 182 |
-
const result = await mctsAgent.selectMove(game, 4);
|
| 183 |
-
|
| 184 |
-
expect(result.move.isPass).toBe(true);
|
| 185 |
-
expect(result.rootValue).toBeDefined();
|
| 186 |
-
});
|
| 187 |
-
});
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
describe("Test 5: Selection Skips Terminal Nodes", () => {
|
| 191 |
-
it("should not expand terminal nodes during selection", async () => {
|
| 192 |
-
// Setup near-terminal position
|
| 193 |
-
game = new TrigoGame({ x: 5, y: 1, z: 1 });
|
| 194 |
-
game.startGame();
|
| 195 |
-
|
| 196 |
-
// Play several moves to approach terminal state
|
| 197 |
-
game.drop({ x: 0, y: 0, z: 0 }); // Black at a
|
| 198 |
-
game.drop({ x: 4, y: 0, z: 0 }); // White at z
|
| 199 |
-
game.drop({ x: 1, y: 0, z: 0 }); // Black at b
|
| 200 |
-
game.drop({ x: 3, y: 0, z: 0 }); // White at y
|
| 201 |
-
|
| 202 |
-
// One position left (center)
|
| 203 |
-
const result = await mctsAgent.selectMove(game, 5);
|
| 204 |
-
|
| 205 |
-
expect(result.move).toBeDefined();
|
| 206 |
-
|
| 207 |
-
// Should have visit counts for available moves
|
| 208 |
-
expect(result.visitCounts.size).toBeGreaterThan(0);
|
| 209 |
-
});
|
| 210 |
-
});
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
describe("Test 6: White-Positive Minimax Verification", () => {
|
| 214 |
-
it("should correctly apply minimax with white-positive values", async () => {
|
| 215 |
-
// Test scenario from verification doc:
|
| 216 |
-
// White to move with children [+5, +1, -2] should choose +5
|
| 217 |
-
// Black to move with children [+5, +1, -2] should choose -2
|
| 218 |
-
|
| 219 |
-
game = new TrigoGame({ x: 3, y: 1, z: 1 });
|
| 220 |
-
game.startGame();
|
| 221 |
-
|
| 222 |
-
// Run MCTS
|
| 223 |
-
const result = await mctsAgent.selectMove(game, 1);
|
| 224 |
-
|
| 225 |
-
// Basic verification
|
| 226 |
-
expect(result.move).toBeDefined();
|
| 227 |
-
expect(result.rootValue).toBeDefined();
|
| 228 |
-
});
|
| 229 |
-
});
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
describe("Test 7: 5x1x1 Board Complete Game", () => {
|
| 233 |
-
it("should handle 5x1x1 board terminal propagation correctly", async () => {
|
| 234 |
-
// Test terminal propagation on a 5x1x1 board
|
| 235 |
-
// Focus on verifying the mechanics work, not exact game outcome
|
| 236 |
-
|
| 237 |
-
game = new TrigoGame({ x: 5, y: 1, z: 1 });
|
| 238 |
-
game.startGame();
|
| 239 |
-
|
| 240 |
-
// Play a few moves
|
| 241 |
-
game.drop({ x: 2, y: 0, z: 0 }); // Black center
|
| 242 |
-
game.drop({ x: 0, y: 0, z: 0 }); // White a
|
| 243 |
-
game.drop({ x: 4, y: 0, z: 0 }); // Black z
|
| 244 |
-
|
| 245 |
-
// Now run MCTS to find best move for White
|
| 246 |
-
// Should handle near-terminal position correctly
|
| 247 |
-
const result = await mctsAgent.selectMove(game, 4);
|
| 248 |
-
|
| 249 |
-
expect(result.move).toBeDefined();
|
| 250 |
-
expect(result.visitCounts.size).toBeGreaterThan(0);
|
| 251 |
-
expect(result.rootValue).toBeDefined();
|
| 252 |
-
|
| 253 |
-
// The key test: terminal propagation should work without errors
|
| 254 |
-
// Even if we don't have terminal nodes yet, the mechanism should be ready
|
| 255 |
-
});
|
| 256 |
-
});
|
| 257 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/testMCTSSingleStep.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Test MCTS consistency between TypeScript and C++
|
| 3 |
-
*
|
| 4 |
-
* Compare policy priors, visits, and Q-values
|
| 5 |
-
* on empty 5x5 board for comparison with C++ implementation
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
import { TrigoTreeAgent } from "../inc/trigoTreeAgent.js";
|
| 9 |
-
import { TrigoEvaluationAgent } from "../inc/trigoEvaluationAgent.js";
|
| 10 |
-
import { TrigoGame } from "../inc/trigo/game.js";
|
| 11 |
-
import { Stone } from "../inc/trigo/types.js";
|
| 12 |
-
import { encodeAb0yz } from "../inc/trigo/ab0yz.js";
|
| 13 |
-
import { ModelInferencer } from "../inc/modelInferencer.js";
|
| 14 |
-
import * as ort from "onnxruntime-node";
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
async function testMCTSSingleStep() {
|
| 18 |
-
console.log("============================================================================");
|
| 19 |
-
console.log("MCTS Single Step Consistency Test (TypeScript)");
|
| 20 |
-
console.log("============================================================================");
|
| 21 |
-
console.log();
|
| 22 |
-
|
| 23 |
-
// Load models - use tree/evaluation model pair
|
| 24 |
-
const modelDir = "/home/camus/work/trigo/trigo-web/public/onnx/20251204-trigo-value-gpt2-l6-h64-251125-lr500";
|
| 25 |
-
const treeModelPath = `${modelDir}/GPT2CausalLM_ep0019_tree.onnx`;
|
| 26 |
-
const evalModelPath = `${modelDir}/GPT2CausalLM_ep0019_evaluation.onnx`;
|
| 27 |
-
|
| 28 |
-
console.log("Loading models...");
|
| 29 |
-
console.log(` Tree model: ${treeModelPath}`);
|
| 30 |
-
console.log(` Eval model: ${evalModelPath}`);
|
| 31 |
-
console.log();
|
| 32 |
-
|
| 33 |
-
// Create ONNX sessions
|
| 34 |
-
const sessionOptions: ort.InferenceSession.SessionOptions = {
|
| 35 |
-
executionProviders: ["cpu"]
|
| 36 |
-
};
|
| 37 |
-
|
| 38 |
-
const treeSession = await ort.InferenceSession.create(treeModelPath, sessionOptions);
|
| 39 |
-
const evalSession = await ort.InferenceSession.create(evalModelPath, sessionOptions);
|
| 40 |
-
|
| 41 |
-
// Create inferencers with proper initialization
|
| 42 |
-
const treeInferencer = new ModelInferencer(ort.Tensor as any, {
|
| 43 |
-
vocabSize: 128,
|
| 44 |
-
seqLen: 256
|
| 45 |
-
});
|
| 46 |
-
treeInferencer.setSession(treeSession as any);
|
| 47 |
-
|
| 48 |
-
const evalInferencer = new ModelInferencer(ort.Tensor as any, {
|
| 49 |
-
vocabSize: 128,
|
| 50 |
-
seqLen: 256
|
| 51 |
-
});
|
| 52 |
-
evalInferencer.setSession(evalSession as any);
|
| 53 |
-
|
| 54 |
-
console.log("[ModelInferencer] ✓ Session set successfully");
|
| 55 |
-
console.log();
|
| 56 |
-
|
| 57 |
-
// Create agents
|
| 58 |
-
const treeAgent = new TrigoTreeAgent(treeInferencer);
|
| 59 |
-
const evaluationAgent = new TrigoEvaluationAgent(evalInferencer);
|
| 60 |
-
|
| 61 |
-
// Setup game: 5x5 board, empty
|
| 62 |
-
const shape = { x: 5, y: 5, z: 1 };
|
| 63 |
-
const game = new TrigoGame(shape);
|
| 64 |
-
game.startGame();
|
| 65 |
-
|
| 66 |
-
console.log("Game Configuration:");
|
| 67 |
-
console.log(" Board: 5×5×1");
|
| 68 |
-
console.log(" Position: Empty board (Move 1)");
|
| 69 |
-
console.log(` Current player: ${game.getCurrentPlayer() === Stone.Black ? "Black" : "White"}`);
|
| 70 |
-
const validMoves = game.validMovePositions(game.getCurrentPlayer());
|
| 71 |
-
console.log(` Valid moves: ${validMoves.length}`);
|
| 72 |
-
console.log();
|
| 73 |
-
|
| 74 |
-
// Step 1: Get policy priors directly from tree agent
|
| 75 |
-
console.log("Step 1: Getting policy priors from tree agent...");
|
| 76 |
-
const currentPlayer = game.getCurrentPlayer() === Stone.Black ? "black" : "white";
|
| 77 |
-
const moves = validMoves.map(pos => ({
|
| 78 |
-
x: pos.x,
|
| 79 |
-
y: pos.y,
|
| 80 |
-
z: pos.z,
|
| 81 |
-
player: currentPlayer as "black" | "white"
|
| 82 |
-
}));
|
| 83 |
-
moves.push({ player: currentPlayer as "black" | "white", isPass: true });
|
| 84 |
-
|
| 85 |
-
const scoredMoves = await treeAgent.scoreMoves(game, moves);
|
| 86 |
-
|
| 87 |
-
// Sort by score descending
|
| 88 |
-
scoredMoves.sort((a, b) => b.score - a.score);
|
| 89 |
-
|
| 90 |
-
// Compute priors via exp-normalize
|
| 91 |
-
const maxScore = Math.max(...scoredMoves.map(m => m.score));
|
| 92 |
-
const expScores = scoredMoves.map(m => Math.exp(m.score - maxScore));
|
| 93 |
-
const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
|
| 94 |
-
|
| 95 |
-
console.log();
|
| 96 |
-
console.log("Top 5 moves by policy prior (log score):");
|
| 97 |
-
console.log("| Rank | Move | Position | Log Score | Prior |");
|
| 98 |
-
console.log("|------|------|----------|-----------|-------|");
|
| 99 |
-
|
| 100 |
-
for (let i = 0; i < Math.min(5, scoredMoves.length); i++) {
|
| 101 |
-
const m = scoredMoves[i];
|
| 102 |
-
let moveStr: string;
|
| 103 |
-
let posStr: string;
|
| 104 |
-
|
| 105 |
-
if (m.move.isPass) {
|
| 106 |
-
moveStr = "Pass";
|
| 107 |
-
posStr = "N/A";
|
| 108 |
-
} else {
|
| 109 |
-
moveStr = encodeAb0yz([m.move.x!, m.move.y!, m.move.z!], [shape.x, shape.y, shape.z]);
|
| 110 |
-
posStr = `(${m.move.x},${m.move.y},${m.move.z})`;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
const prior = expScores[i] / sumExp;
|
| 114 |
-
console.log(`| ${i + 1} | ${moveStr} | ${posStr} | ${m.score.toFixed(6)} | ${prior.toFixed(6)} |`);
|
| 115 |
-
}
|
| 116 |
-
console.log();
|
| 117 |
-
|
| 118 |
-
// Step 2: Get value estimate directly
|
| 119 |
-
console.log("Step 2: Getting value estimate...");
|
| 120 |
-
const evaluation = await evaluationAgent.evaluatePosition(game);
|
| 121 |
-
console.log(` Value: ${evaluation.value.toFixed(6)} (${evaluation.interpretation})`);
|
| 122 |
-
console.log();
|
| 123 |
-
|
| 124 |
-
// Summary for comparison
|
| 125 |
-
console.log("============================================================================");
|
| 126 |
-
console.log("Summary for C++ Comparison:");
|
| 127 |
-
console.log("============================================================================");
|
| 128 |
-
console.log();
|
| 129 |
-
console.log("Policy Priors (top 5):");
|
| 130 |
-
for (let i = 0; i < Math.min(5, scoredMoves.length); i++) {
|
| 131 |
-
const m = scoredMoves[i];
|
| 132 |
-
let moveStr: string;
|
| 133 |
-
|
| 134 |
-
if (m.move.isPass) {
|
| 135 |
-
moveStr = "Pass";
|
| 136 |
-
} else {
|
| 137 |
-
moveStr = encodeAb0yz([m.move.x!, m.move.y!, m.move.z!], [shape.x, shape.y, shape.z]);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
const prior = expScores[i] / sumExp;
|
| 141 |
-
console.log(` ${i + 1}. ${moveStr} log_score=${m.score.toFixed(6)} prior=${prior.toFixed(6)}`);
|
| 142 |
-
}
|
| 143 |
-
console.log();
|
| 144 |
-
console.log(`Value estimate: ${evaluation.value.toFixed(6)}`);
|
| 145 |
-
console.log();
|
| 146 |
-
|
| 147 |
-
console.log("Expected C++ results (from test_mcts_single_step):");
|
| 148 |
-
console.log(" 1. az log_score=-2.855756 prior=0.170112");
|
| 149 |
-
console.log(" 2. aa log_score=-2.880302 prior=0.165987");
|
| 150 |
-
console.log(" 3. yz log_score=-3.696474 prior=0.073386");
|
| 151 |
-
console.log(" 4. ya log_score=-3.754188 prior=0.069271");
|
| 152 |
-
console.log(" 5. bz log_score=-3.799809 prior=0.066182");
|
| 153 |
-
console.log();
|
| 154 |
-
console.log("Note: C++ uses prefix-cached model, TypeScript uses tree/evaluation models");
|
| 155 |
-
console.log(" Different models may produce different policy distributions.");
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
testMCTSSingleStep().catch(console.error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/testMCTSWithVisits.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Test MCTS with visits and Q-values - TypeScript vs C++
|
| 3 |
-
*
|
| 4 |
-
* Run actual MCTS simulations and compare:
|
| 5 |
-
* - Policy priors
|
| 6 |
-
* - Visit counts
|
| 7 |
-
* - Q-values
|
| 8 |
-
*/
|
| 9 |
-
|
| 10 |
-
import { MCTSAgent } from "../inc/mctsAgent.js";
|
| 11 |
-
import { TrigoTreeAgent } from "../inc/trigoTreeAgent.js";
|
| 12 |
-
import { TrigoEvaluationAgent } from "../inc/trigoEvaluationAgent.js";
|
| 13 |
-
import { TrigoGame } from "../inc/trigo/game.js";
|
| 14 |
-
import { Stone } from "../inc/trigo/types.js";
|
| 15 |
-
import { encodeAb0yz } from "../inc/trigo/ab0yz.js";
|
| 16 |
-
import { ModelInferencer } from "../inc/modelInferencer.js";
|
| 17 |
-
import * as ort from "onnxruntime-node";
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
async function testMCTSWithVisits() {
|
| 21 |
-
console.log("============================================================================");
|
| 22 |
-
console.log("MCTS with Visits and Q-values Test (TypeScript)");
|
| 23 |
-
console.log("============================================================================");
|
| 24 |
-
console.log();
|
| 25 |
-
|
| 26 |
-
// Load models
|
| 27 |
-
const modelDir = "/home/camus/work/trigo/trigo-web/public/onnx/20251204-trigo-value-gpt2-l6-h64-251125-lr500";
|
| 28 |
-
const treeModelPath = `${modelDir}/GPT2CausalLM_ep0019_tree.onnx`;
|
| 29 |
-
const evalModelPath = `${modelDir}/GPT2CausalLM_ep0019_evaluation.onnx`;
|
| 30 |
-
|
| 31 |
-
console.log("Loading models...");
|
| 32 |
-
|
| 33 |
-
const sessionOptions: ort.InferenceSession.SessionOptions = {
|
| 34 |
-
executionProviders: ["cpu"]
|
| 35 |
-
};
|
| 36 |
-
|
| 37 |
-
const treeSession = await ort.InferenceSession.create(treeModelPath, sessionOptions);
|
| 38 |
-
const evalSession = await ort.InferenceSession.create(evalModelPath, sessionOptions);
|
| 39 |
-
|
| 40 |
-
const treeInferencer = new ModelInferencer(ort.Tensor as any, {
|
| 41 |
-
vocabSize: 128,
|
| 42 |
-
seqLen: 256
|
| 43 |
-
});
|
| 44 |
-
treeInferencer.setSession(treeSession as any);
|
| 45 |
-
|
| 46 |
-
const evalInferencer = new ModelInferencer(ort.Tensor as any, {
|
| 47 |
-
vocabSize: 128,
|
| 48 |
-
seqLen: 256
|
| 49 |
-
});
|
| 50 |
-
evalInferencer.setSession(evalSession as any);
|
| 51 |
-
|
| 52 |
-
console.log("✓ Models loaded");
|
| 53 |
-
console.log();
|
| 54 |
-
|
| 55 |
-
const treeAgent = new TrigoTreeAgent(treeInferencer);
|
| 56 |
-
const evaluationAgent = new TrigoEvaluationAgent(evalInferencer);
|
| 57 |
-
|
| 58 |
-
// Setup game
|
| 59 |
-
const shape = { x: 5, y: 5, z: 1 };
|
| 60 |
-
const game = new TrigoGame(shape);
|
| 61 |
-
game.startGame();
|
| 62 |
-
|
| 63 |
-
console.log("Game Configuration:");
|
| 64 |
-
console.log(` Board: ${shape.x}×${shape.y}×${shape.z}`);
|
| 65 |
-
console.log(` Current player: ${game.getCurrentPlayer() === Stone.Black ? "Black" : "White"}`);
|
| 66 |
-
console.log(` Valid moves: ${game.validMovePositions(game.getCurrentPlayer()).length}`);
|
| 67 |
-
console.log();
|
| 68 |
-
|
| 69 |
-
// MCTS configuration - match C++ settings
|
| 70 |
-
const numSimulations = 10; // Use 10 simulations to get meaningful visits
|
| 71 |
-
console.log("MCTS Configuration:");
|
| 72 |
-
console.log(` Simulations: ${numSimulations}`);
|
| 73 |
-
console.log(" c_puct: 1.0");
|
| 74 |
-
console.log(" Temperature: 0.01 (near-greedy)");
|
| 75 |
-
console.log(" Dirichlet noise: DISABLED (epsilon=0)");
|
| 76 |
-
console.log();
|
| 77 |
-
|
| 78 |
-
// Create MCTS agent with NO Dirichlet noise for reproducibility
|
| 79 |
-
const mctsAgent = new MCTSAgent(treeAgent, evaluationAgent, {
|
| 80 |
-
numSimulations: numSimulations,
|
| 81 |
-
cPuct: 1.0,
|
| 82 |
-
temperature: 0.01,
|
| 83 |
-
dirichletAlpha: 0.03,
|
| 84 |
-
dirichletEpsilon: 0.0 // NO noise
|
| 85 |
-
});
|
| 86 |
-
|
| 87 |
-
console.log("Running MCTS search...");
|
| 88 |
-
console.log("------------------------------------------------------------");
|
| 89 |
-
|
| 90 |
-
const startTime = Date.now();
|
| 91 |
-
const result = await mctsAgent.selectMove(game, 0);
|
| 92 |
-
const elapsedTime = Date.now() - startTime;
|
| 93 |
-
|
| 94 |
-
console.log("------------------------------------------------------------");
|
| 95 |
-
console.log();
|
| 96 |
-
|
| 97 |
-
// We need to access the internal node to get Q-values
|
| 98 |
-
// Since MCTSAgent doesn't expose them directly, we'll run scoreMoves separately
|
| 99 |
-
// to get policy priors, then show what MCTS selected
|
| 100 |
-
|
| 101 |
-
// Get policy priors separately
|
| 102 |
-
const currentPlayer = game.getCurrentPlayer() === Stone.Black ? "black" : "white";
|
| 103 |
-
const validPositions = game.validMovePositions(game.getCurrentPlayer());
|
| 104 |
-
const moves = validPositions.map(pos => ({
|
| 105 |
-
x: pos.x,
|
| 106 |
-
y: pos.y,
|
| 107 |
-
z: pos.z,
|
| 108 |
-
player: currentPlayer as "black" | "white"
|
| 109 |
-
}));
|
| 110 |
-
moves.push({ player: currentPlayer as "black" | "white", isPass: true });
|
| 111 |
-
|
| 112 |
-
const scoredMoves = await treeAgent.scoreMoves(game, moves);
|
| 113 |
-
scoredMoves.sort((a, b) => b.score - a.score);
|
| 114 |
-
|
| 115 |
-
// Compute priors
|
| 116 |
-
const maxScore = Math.max(...scoredMoves.map(m => m.score));
|
| 117 |
-
const expScores = scoredMoves.map(m => Math.exp(m.score - maxScore));
|
| 118 |
-
const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
|
| 119 |
-
|
| 120 |
-
// Build a map from action key to prior
|
| 121 |
-
const priorMap = new Map<string, number>();
|
| 122 |
-
for (let i = 0; i < scoredMoves.length; i++) {
|
| 123 |
-
const m = scoredMoves[i];
|
| 124 |
-
const actionKey = m.move.isPass ? "pass" : `${m.move.x},${m.move.y},${m.move.z}`;
|
| 125 |
-
priorMap.set(actionKey, expScores[i] / sumExp);
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
// Collect results with visits
|
| 129 |
-
const results: Array<{
|
| 130 |
-
move: string;
|
| 131 |
-
position: string;
|
| 132 |
-
visits: number;
|
| 133 |
-
prior: number;
|
| 134 |
-
}> = [];
|
| 135 |
-
|
| 136 |
-
for (const [actionKey, visits] of result.visitCounts.entries()) {
|
| 137 |
-
let moveNotation: string;
|
| 138 |
-
let posStr: string;
|
| 139 |
-
|
| 140 |
-
if (actionKey === "pass") {
|
| 141 |
-
moveNotation = "Pass";
|
| 142 |
-
posStr = "N/A";
|
| 143 |
-
} else {
|
| 144 |
-
const [x, y, z] = actionKey.split(",").map(Number);
|
| 145 |
-
moveNotation = encodeAb0yz([x, y, z], [shape.x, shape.y, shape.z]);
|
| 146 |
-
posStr = `(${x},${y},${z})`;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
const prior = priorMap.get(actionKey) ?? 0;
|
| 150 |
-
|
| 151 |
-
results.push({
|
| 152 |
-
move: moveNotation,
|
| 153 |
-
position: posStr,
|
| 154 |
-
visits: visits,
|
| 155 |
-
prior: prior
|
| 156 |
-
});
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
// Sort by visits descending, then by prior
|
| 160 |
-
results.sort((a, b) => {
|
| 161 |
-
if (b.visits !== a.visits) return b.visits - a.visits;
|
| 162 |
-
return b.prior - a.prior;
|
| 163 |
-
});
|
| 164 |
-
|
| 165 |
-
// Print results
|
| 166 |
-
console.log("MCTS Results (sorted by visits):");
|
| 167 |
-
console.log("| Rank | Move | Position | Visits | Prior | Notes |");
|
| 168 |
-
console.log("|------|------|----------|--------|-------|-------|");
|
| 169 |
-
|
| 170 |
-
for (let i = 0; i < Math.min(10, results.length); i++) {
|
| 171 |
-
const r = results[i];
|
| 172 |
-
const marker = r.visits > 0 ? "Explored" : "";
|
| 173 |
-
console.log(`| ${i + 1} | ${r.move} | ${r.position} | ${r.visits} | ${r.prior.toFixed(6)} | ${marker} |`);
|
| 174 |
-
}
|
| 175 |
-
console.log();
|
| 176 |
-
|
| 177 |
-
// Print selected move
|
| 178 |
-
console.log("Selected Move:");
|
| 179 |
-
if (result.move.isPass) {
|
| 180 |
-
console.log(" Move: Pass");
|
| 181 |
-
} else {
|
| 182 |
-
const notation = encodeAb0yz([result.move.x!, result.move.y!, result.move.z!], [shape.x, shape.y, shape.z]);
|
| 183 |
-
console.log(` Move: ${notation} (${result.move.x},${result.move.y},${result.move.z})`);
|
| 184 |
-
}
|
| 185 |
-
console.log(` Root value: ${result.rootValue.toFixed(6)}`);
|
| 186 |
-
console.log();
|
| 187 |
-
|
| 188 |
-
// Summary statistics
|
| 189 |
-
let totalVisits = 0;
|
| 190 |
-
let exploredMoves = 0;
|
| 191 |
-
for (const [_, visits] of result.visitCounts.entries()) {
|
| 192 |
-
totalVisits += visits;
|
| 193 |
-
if (visits > 0) exploredMoves++;
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
console.log("Statistics:");
|
| 197 |
-
console.log(` Total visits: ${totalVisits}`);
|
| 198 |
-
console.log(` Explored moves: ${exploredMoves} / ${result.visitCounts.size}`);
|
| 199 |
-
console.log(` Elapsed time: ${elapsedTime}ms`);
|
| 200 |
-
console.log();
|
| 201 |
-
|
| 202 |
-
console.log("============================================================================");
|
| 203 |
-
console.log("C++ Comparison (from test_mcts_single_step with 1 simulation):");
|
| 204 |
-
console.log(" Selected: ya (3,0,0)");
|
| 205 |
-
console.log(" Visits: 1");
|
| 206 |
-
console.log(" Q-value: -0.082214");
|
| 207 |
-
console.log(" Prior: 0.069271");
|
| 208 |
-
console.log("============================================================================");
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
testMCTSWithVisits().catch(console.error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/tgn/19x19.tgn
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
[Board 19x19]
|
| 3 |
-
|
| 4 |
-
1. dd wx
|
| 5 |
-
2. dw wd
|
| 6 |
-
3. xv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trigo-web/tests/tgn/meta.tgn
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
[Event "World 3D Go Championship 2025"]
|
| 3 |
-
[Site "Tokyo, Japan"]
|
| 4 |
-
[Date "2025.10.31"]
|
| 5 |
-
[Black "Alice Chen"]
|
| 6 |
-
[White "Bob Smith"]
|
| 7 |
-
[Result B+ 5stones]
|
| 8 |
-
[Board 5x5x5]
|
| 9 |
-
[Rules "Chinese"]
|
| 10 |
-
[TimeControl "30+10"]
|
| 11 |
-
[Application "Trigo v1.0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|