@vimp-games/snakes 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/assets/vimp_snakes_core_bg-BqmtH1zs.wasm +0 -0
- package/dist/client-DZrdJlPY.js +7 -0
- package/dist/core-node/package.json +16 -0
- package/dist/core-node/vimp_snakes_core.d.ts +231 -0
- package/dist/core-node/vimp_snakes_core.js +743 -0
- package/dist/core-node/vimp_snakes_core_bg.wasm +0 -0
- package/dist/core-node/vimp_snakes_core_bg.wasm.d.ts +56 -0
- package/dist/host-ClLuGvxl.js +1 -0
- package/dist/manifest.json +38 -0
- package/dist/maps/arena.json +1 -0
- package/dist/sounds/death.mp3 +0 -0
- package/dist/sounds/death.webm +0 -0
- package/dist/sounds/shot.mp3 +0 -0
- package/dist/sounds/shot.webm +0 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lgick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Vimp Snakes
|
|
2
|
+
|
|
3
|
+
A VIMP game plugin (`@vimp-games/snakes`): a snake arena.
|
|
4
|
+
|
|
5
|
+
## The game
|
|
6
|
+
|
|
7
|
+
One circular arena. Snakes are always moving; you steer, you never stop.
|
|
8
|
+
Crystals appear at random in three sizes — eat them to grow, and the bigger the
|
|
9
|
+
crystal the more you grow and the more you score.
|
|
10
|
+
|
|
11
|
+
| Key | Does |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `A` / `D` | turn left / right |
|
|
14
|
+
| `W` | boost — nearly double speed, paid for in crystals, which drop behind you |
|
|
15
|
+
| `R` | respawn after a crash (the OK button of the result screen presses it for you) |
|
|
16
|
+
|
|
17
|
+
Three ways it ends and only one of them is your fault:
|
|
18
|
+
|
|
19
|
+
- your head touches the boundary of the disc — you crash;
|
|
20
|
+
- your head touches **another** snake's body — you crash, and everything you
|
|
21
|
+
were carrying scatters over the map where you died;
|
|
22
|
+
- your head touches **your own** tail — nothing happens, it passes over.
|
|
23
|
+
|
|
24
|
+
The stat table (`Tab`) is a single leaderboard ranked by crystals carried right
|
|
25
|
+
now. Everyone is a player; there are no teams.
|
|
26
|
+
|
|
27
|
+
## Run it
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
npm run core:build # REQUIRED before npm run dev
|
|
32
|
+
npm run dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`npm run core:build` is not optional and not a later step: the dev harness
|
|
36
|
+
(`dev/main.js`) imports the wasm out of `core/pkg-web/`, so until the Rust core
|
|
37
|
+
has been built once Vite fails to resolve the import and `npm run dev` dies on
|
|
38
|
+
startup. The same holds before the master is started against this package,
|
|
39
|
+
even in dev mode — build the core and then `npm run build` at least once.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run core:build # wasm-pack -> core/pkg-web (runtime) + core/pkg-node (tests)
|
|
45
|
+
npm run core:test # cargo test --workspace, including the motion parity suite
|
|
46
|
+
npm run build # dist/: both bundles, maps, sounds, manifest.json
|
|
47
|
+
npm run check:contract # static engine<->game contract check (vimp-contract)
|
|
48
|
+
npm test # vitest
|
|
49
|
+
npm run dev # a match against bots in the tab
|
|
50
|
+
npm run audio:process # ffmpeg: assets/audio-raw/ -> build/sounds/ (optional)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The headless runner lives in the engine checkout and is the primary
|
|
54
|
+
verification loop — see `scenarios/` below.
|
|
55
|
+
|
|
56
|
+
## Layout
|
|
57
|
+
|
|
58
|
+
| Path | What |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `core/` | the Rust crate `vimp-snakes-core` — movement, growth, collisions |
|
|
61
|
+
| `src/host/` | HostPlugin: runs in a Web Worker, no DOM and no PixiJS |
|
|
62
|
+
| `src/client/` | ClientPlugin: render parts, bakers and the result screen |
|
|
63
|
+
| `src/config/` | game, client, auth, snapshot and sound configuration |
|
|
64
|
+
| `src/data/` | the map, the snake model, the palette and the arena theme |
|
|
65
|
+
| `scenarios/` | headless scenarios for `npm run sim` |
|
|
66
|
+
| `scripts/` | build steps: bundles -> `dist/` + `manifest.json` |
|
|
67
|
+
| `dev/` | the standalone dev harness — never published |
|
|
68
|
+
|
|
69
|
+
Only `dist/` is published (`files: ["dist"]`).
|
|
70
|
+
|
|
71
|
+
## Three decisions worth knowing before reading the code
|
|
72
|
+
|
|
73
|
+
**The core owns life and death; the engine is never told.** No
|
|
74
|
+
`CoreEvent::Death` is ever emitted. That is what makes the round endless (a
|
|
75
|
+
round only ends through a reported kill) and what makes respawning possible at
|
|
76
|
+
all (the engine has no per-player respawn inside a round — its only spawn
|
|
77
|
+
primitive is private to `_startRound`). The consequence is that the engine also
|
|
78
|
+
never writes `score` or `deaths`, which is why `src/host/StatBridge.js` writes
|
|
79
|
+
them instead, off the core's `custom` events. See the note atop
|
|
80
|
+
`src/config/game.js`.
|
|
81
|
+
|
|
82
|
+
**There is no physics.** A snake is a polyline; the arena edge, other bodies
|
|
83
|
+
and crystals are distance tests. The engine's Rapier world is stepped and stays
|
|
84
|
+
empty. That is what makes the client's prediction exact — the predictor runs
|
|
85
|
+
the same `core/src/motion.rs` the host does, with no solver in between to
|
|
86
|
+
disagree about.
|
|
87
|
+
|
|
88
|
+
**The arena is derived, not configured.** The `game` half of the init JSON is a
|
|
89
|
+
fixed field set, and a free-form `gameConfig.parts.*` key reaches the client and
|
|
90
|
+
never the core — so the disc comes out of the map grid by one formula both
|
|
91
|
+
halves apply (`core/src/arena.rs`, `src/client/parts/Arena.js`,
|
|
92
|
+
`src/data/maps/arena.js`).
|
|
93
|
+
|
|
94
|
+
## Headless verification
|
|
95
|
+
|
|
96
|
+
From the **engine** checkout, with this package linked:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm run sim -- --game <path to vimp-snakes> --scenario <path>/scenarios/movement.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Scenario | Exercises |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `movement.json` | cruising and turning, with prediction drift checked against tight thresholds |
|
|
105
|
+
| `crash-and-respawn.json` | driving into the boundary, staying dead, the respawn key |
|
|
106
|
+
| `growth.json` | two players, three bots, crystals, the boost, `/spawn` |
|
|
107
|
+
|
|
108
|
+
All three are expected to pass with `--determinism`. Two invariants skip by
|
|
109
|
+
design: `roundLifecycle` (this game has no round end) and, in two of the three
|
|
110
|
+
scenarios, `predictionDrift` (a crash and a respawn are legitimate one-off
|
|
111
|
+
spikes — `movement.json` is the one that watches for drift that *grows*).
|
|
112
|
+
|
|
113
|
+
## Sounds without ffmpeg
|
|
114
|
+
|
|
115
|
+
The real pipeline is `assets/audio-raw/*.wav` → `npm run audio:process` →
|
|
116
|
+
`build/sounds/*.{webm,mp3}`. That needs ffmpeg, so the package also ships
|
|
117
|
+
ready-made placeholders in `assets/sounds/`: when `build/sounds/` is absent,
|
|
118
|
+
`scripts/copy-game-sounds.js` falls back to them and the first build stays green
|
|
119
|
+
on a bare machine. The two cues are `pickup` and `death`, both played
|
|
120
|
+
positionally by `src/client/parts/Snake.js`.
|
|
121
|
+
|
|
122
|
+
## Engine
|
|
123
|
+
|
|
124
|
+
- `vimp-engine` `^0.10.0`
|
|
125
|
+
- `vimp-engine-core` `0.3.2`
|
|
126
|
+
|
|
127
|
+
Game id: `snakes`. The plugin contract lives in the engine repository
|
|
128
|
+
under `docs/ai/`.
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{Container as F,Graphics as T,Sprite as Z,Rectangle as ee}from"pixi.js";const te=3,ne='#panel{gap:16px;font:600 20px/1 system-ui,sans-serif;color:#e6ecf5;text-shadow:0 1px 2px rgb(0 0 0 / 60%)}#panel-crystals:before{content:"◆ ";color:#7ef9ff}#panel-length:before{content:"↔ ";opacity:.7}#panel-time{opacity:.8}#panel-dead{display:none}#snakes-gameover{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:#060a149e;font:400 16px/1.5 system-ui,sans-serif;color:#e6ecf5}#snakes-gameover[hidden]{display:none}.snakes-gameover-card{min-width:260px;padding:28px 32px;border:1px solid #39507f;border-radius:12px;background:#141c30;box-shadow:0 18px 48px #00000073;text-align:center}.snakes-gameover-card h2{margin:0 0 4px;font-size:22px;font-weight:600;letter-spacing:.02em}.snakes-gameover-score{margin:0 0 22px;opacity:.75}.snakes-gameover-score span{font-size:40px;font-weight:700;color:#7ef9ff;vertical-align:-2px}.snakes-gameover-card button{min-width:120px;padding:10px 20px;border:0;border-radius:8px;background:#3ab7ff;color:#06182a;font:inherit;font-weight:600;cursor:pointer}.snakes-gameover-card button:hover,.snakes-gameover-card button:focus-visible{background:#7ed0ff}',re={background:856608,floor:1317936,edge:3756159,edgeWidth:10,rings:4,ringColor:1911111},v={innerScale:.62,innerDarken:.55,smoothing:4,eye:16251903,pupil:1054760,boostGlow:16771466},se=.5;class oe extends F{constructor(e){super(),this.zIndex=0,!(Array.isArray(e)||e.type==="dynamic")&&this._draw(e)}_draw({map:e,step:t}){const n=e.length,_=(e[0]?.length??0)*t,i=n*t,c=Math.min(_,i)/2,a=_/2,l=i/2,{background:f,floor:d,edge:u,edgeWidth:b,rings:m,ringColor:Q}=re,w=new T;w.rect(-_,-i,_*3,i*3),w.fill(f),w.circle(a,l,c),w.fill(d);for(let I=1;I<m;I+=1)w.circle(a,l,c*I/m),w.stroke({color:Q,width:2,alpha:se});w.circle(a,l,c),w.stroke({color:u,width:b,alignment:1}),this.addChild(w)}update(){}destroy(){super.destroy({children:!0})}}const D=[16731469,16752412,16765503,10214972,3065014,3848191,5531903,11623679,16735432,16250871,8250065,16753828],Y=[8321535,10146047,13154047,12386267,16771466,16757702],B=[{value:1,radius:8},{value:3,radius:13},{value:8,radius:20}],V=16,A=V*2,S={ANGLE:A,RADIUS:A+1,CRYSTALS:A+2,COLOR:A+3,BOOST:A+4};function ie(r,e){const t=Math.round((r>>16&255)*e),n=Math.round((r>>8&255)*e),o=Math.round((r&255)*e);return t<<16|n<<8|o}function ce(r,e){const t=r.length-1;if(t<1)return r;const n=[r[0]];for(let o=0;o<t;o+=1){const _=r[Math.max(o-1,0)],i=r[o],c=r[o+1],a=r[Math.min(o+2,t)];for(let l=1;l<=e;l+=1){const f=l/e,d=f*f,u=d*f;n.push([.5*(2*i[0]+(-_[0]+c[0])*f+(2*_[0]-5*i[0]+4*c[0]-a[0])*d+(-_[0]+3*i[0]-3*c[0]+a[0])*u),.5*(2*i[1]+(-_[1]+c[1])*f+(2*_[1]-5*i[1]+4*c[1]-a[1])*d+(-_[1]+3*i[1]-3*c[1]+a[1])*u)])}}return n}class _e extends F{constructor(e,t,n={}){super(),this.zIndex=3,this._sound=n.soundManager,this._body=new T,this._head=new T,this.addChild(this._body,this._head),this._headAt=[0,0],this._crystals=null,this.update(e)}update(e){const t=e[S.RADIUS]||1,n=e[S.ANGLE]||0,o=e[S.CRYSTALS]||0,_=!!e[S.BOOST],i=D[(e[S.COLOR]||0)%D.length],c=[];for(let a=0;a<V;a+=1)c.push([e[a*2]||0,e[a*2+1]||0]);this._drawBody(c,t,i,_),this._drawHead(c[0],n,t,i),this._crystals!==null&&o>this._crystals&&this._play("pickup",c[0]),this._crystals=o,this._headAt=c[0]}_drawBody(e,t,n,o){const _=ce(e,v.smoothing),i=this._body;i.clear(),i.moveTo(_[0][0],_[0][1]);for(let c=1;c<_.length;c+=1)i.lineTo(_[c][0],_[c][1]);o&&i.stroke({color:v.boostGlow,width:t*2.6,alpha:.22,cap:"round",join:"round"}),i.stroke({color:n,width:t*2,cap:"round",join:"round"}),i.stroke({color:ie(n,v.innerDarken),width:t*2*v.innerScale,alpha:.45,cap:"round",join:"round"})}_drawHead([e,t],n,o,_){const i=this._head;i.clear(),i.circle(e,t,o),i.fill(_);const c=Math.cos(n),a=Math.sin(n),l=o*.5,f=o*.42,d=o*.32;for(const u of[-1,1]){const b=e+c*f-a*l*u,m=t+a*f+c*l*u;i.circle(b,m,d),i.fill(v.eye),i.circle(b+c*d*.35,m+a*d*.35,d*.5),i.fill(v.pupil)}}_play(e,[t,n]){this._sound?.registerSound(e,{position:{x:t,y:n}})}destroy(){this._play("death",this._headAt),super.destroy({children:!0})}}const O={X:0,Y:1,TIER:2,COLOR:3},ae=32;class le extends F{constructor(e,t){super(),this.zIndex=2,this._sprite=new Z(t.crystalGem),this._sprite.anchor.set(.5),this.addChild(this._sprite),this.update(e)}update(e){this.x=e[O.X]||0,this.y=e[O.Y]||0;const t=B[e[O.TIER]||0]??B[0];this._sprite.scale.set(t.radius/ae),this._sprite.tint=Y[(e[O.COLOR]||0)%Y.length],this._sprite.rotation=(this.x+this.y)%360*(Math.PI/180)}destroy(){super.destroy({children:!0})}}const fe={Arena:oe,Snake:_e,Crystal:le};function de(r,e){const{radius:t,facets:n}=r,o=t*2,_=new T,i=(f,d,u=0)=>{const b=(d+u)/n*Math.PI*2-Math.PI/2;return[t+Math.cos(b)*f,t+Math.sin(b)*f]},c=[];for(let f=0;f<n;f+=1)c.push(i(t*.94,f));_.poly(c.flat()),_.fill({color:16777215,alpha:.85});const a=[];for(let f=0;f<n;f+=1)a.push(i(t*.5,f,.5));_.poly(a.flat()),_.fill({color:16777215,alpha:1});const l=e.generateTexture({target:_,frame:new ee(0,0,o,o)});return _.destroy(!0),l}const pe={crystalGem:de},R="snakes-gameover",ue=82,G="d",U="c";function he(r,e){const t=new KeyboardEvent(r,{keyCode:e,bubbles:!0});t.keyCode!==e&&(Object.defineProperty(t,"keyCode",{value:e}),Object.defineProperty(t,"which",{value:e})),window.dispatchEvent(t)}function ge(r){const e={};for(const t of r??[]){const n=String(t).indexOf(":");n>0&&(e[t.slice(0,n)]=Number(t.slice(n+1)))}return e}class be{constructor(){this._root=null,this._score=null,this._visible=!1,this._lastCrystals=0}mount(){if(this._root||typeof document>"u")return;const e=document.createElement("div");e.id=R,e.hidden=!0,e.innerHTML=`
|
|
2
|
+
<div class="${R}-card">
|
|
3
|
+
<h2>You crashed</h2>
|
|
4
|
+
<p class="${R}-score"><span>0</span> crystals</p>
|
|
5
|
+
<button type="button">OK</button>
|
|
6
|
+
</div>
|
|
7
|
+
`,this._score=e.querySelector(`.${R}-score span`),e.querySelector("button").addEventListener("click",()=>this._respawn()),document.body.appendChild(e),this._root=e}onPanel(e){if(!this._root)return;const t=ge(e);if(U in t&&(this._lastCrystals=t[U]),!(G in t))return;const n=t[G];n>0?this._show(n-1):this._hide()}reset(){this._hide(),this._lastCrystals=0}_show(e){this._visible||(this._visible=!0,this._score.textContent=String(e),this._root.hidden=!1)}_hide(){this._visible&&(this._visible=!1,this._root.hidden=!0)}_respawn(){this._hide();for(const e of["keydown","keyup"])he(e,ue)}}const we="modulepreload",ye=function(r){return"/"+r},$={},me=function(e,t,n){let o=Promise.resolve();if(t&&t.length>0){let a=function(l){return Promise.all(l.map(f=>Promise.resolve(f).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};document.getElementsByTagName("link");const i=document.querySelector("meta[property=csp-nonce]"),c=i?.nonce||i?.getAttribute("nonce");o=a(t.map(l=>{if(l=ye(l),l in $)return;$[l]=!0;const f=l.endsWith(".css"),d=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${l}"]${d}`))return;const u=document.createElement("link");if(u.rel=f?"stylesheet":we,f||(u.as="script"),u.crossOrigin="",u.href=l,c&&u.setAttribute("nonce",c),document.head.appendChild(u),f)return new Promise((b,m)=>{u.addEventListener("load",b),u.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${l}`)))})}))}function _(i){const c=new Event("vite:preloadError",{cancelable:!0});if(c.payload=i,window.dispatchEvent(c),!c.defaultPrevented)throw i}return o.then(i=>{for(const c of i||[])c.status==="rejected"&&_(c.reason);return e().catch(_)})},ve=r=>(r??"").endsWith(".js"),xe=r=>import(r),Ae=()=>me(()=>Promise.resolve().then(()=>Te),[]),M=new be,Me={id:"snakes",engineApi:te,async createClientCore(r,{wasmUrl:e}={}){if(ve(e)){const _=await xe(e);return{core:new _.ClientCore(r),memory:null}}const{default:t,ClientCore:n}=await Ae(),o=await t({module_or_path:e});return{core:new n(r),memory:o.memory}},parts:fe,bakers:pe,styles:ne,hooks:{onAuth(r,e){r.set_model(e.model),M.mount(),M.reset()},onPanel(r,e){r.sync_panel(JSON.stringify(e)),M.onPanel(e)},onLocalAction(){return null}}};class W{__destroy_into_raw(){const e=this.__wbg_ptr;return this.__wbg_ptr=0,q.unregister(this),e}free(){const e=this.__destroy_into_raw();s.__wbg_clientcore_free(e,0)}apply_input(e,t,n){const o=h(e,s.__wbindgen_malloc,s.__wbindgen_realloc),_=p,i=h(t,s.__wbindgen_malloc,s.__wbindgen_realloc),c=p;s.clientcore_apply_input(this.__wbg_ptr,o,_,i,c,n)}debug_json(){let e,t;try{const n=s.clientcore_debug_json(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}decode_frame(e){let t,n;try{const o=z(e,s.__wbindgen_malloc),_=p,i=s.clientcore_decode_frame(this.__wbg_ptr,o,_);return t=i[0],n=i[1],g(i[0],i[1])}finally{s.__wbindgen_free(t,n,1)}}hot_ptr(){return s.clientcore_hot_ptr(this.__wbg_ptr)>>>0}hot_values(){const e=s.clientcore_hot_values(this.__wbg_ptr);var t=j(e[0],e[1]).slice();return s.__wbindgen_free(e[0],e[1]*4,4),t}my_game_id(){return s.clientcore_my_game_id(this.__wbg_ptr)}constructor(e){const t=h(e,s.__wbindgen_malloc,s.__wbindgen_realloc),n=p,o=s.clientcore_new(t,n);if(o[2])throw y(o[1]);return this.__wbg_ptr=o[0],q.register(this,this.__wbg_ptr,this),this}offset(){return s.clientcore_offset(this.__wbg_ptr)}push_frame(e,t){const n=z(e,s.__wbindgen_malloc),o=p;return s.clientcore_push_frame(this.__wbg_ptr,n,o,t)!==0}reset(){s.clientcore_reset(this.__wbg_ptr)}resync(){s.clientcore_resync(this.__wbg_ptr)}sample(e){return s.clientcore_sample(this.__wbg_ptr,e)>>>0}set_active(e){s.clientcore_set_active(this.__wbg_ptr,e)}set_map(e){const t=h(e,s.__wbindgen_malloc,s.__wbindgen_realloc),n=p,o=s.clientcore_set_map(this.__wbg_ptr,t,n);if(o[1])throw y(o[0])}set_model(e){const t=h(e,s.__wbindgen_malloc,s.__wbindgen_realloc),n=p;s.clientcore_set_model(this.__wbg_ptr,t,n)}sync_panel(e){const t=h(e,s.__wbindgen_malloc,s.__wbindgen_realloc),n=p;s.clientcore_sync_panel(this.__wbg_ptr,t,n)}take_divergence(){let e,t;try{const n=s.clientcore_take_divergence(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}take_frames(){let e,t;try{const n=s.clientcore_take_frames(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}}Symbol.dispose&&(W.prototype[Symbol.dispose]=W.prototype.free);class N{__destroy_into_raw(){const e=this.__wbg_ptr;return this.__wbg_ptr=0,K.unregister(this),e}free(){const e=this.__destroy_into_raw();s.__wbg_gamecore_free(e,0)}alive_players(){const e=s.gamecore_alive_players(this.__wbg_ptr);var t=j(e[0],e[1]).slice();return s.__wbindgen_free(e[0],e[1]*4,4),t}apply_input(e,t,n,o){const _=h(n,s.__wbindgen_malloc,s.__wbindgen_realloc),i=p,c=h(o,s.__wbindgen_malloc,s.__wbindgen_realloc),a=p;s.gamecore_apply_input(this.__wbg_ptr,e,t,_,i,c,a)}body_has_events(){return s.gamecore_body_has_events(this.__wbg_ptr)!==0}clear(){s.gamecore_clear(this.__wbg_ptr)}debug_json(){let e,t;try{const n=s.gamecore_debug_json(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}deserialize_state(e){const t=z(e,s.__wbindgen_malloc),n=p,o=s.gamecore_deserialize_state(this.__wbg_ptr,t,n);if(o[1])throw y(o[0])}frame_bytes(){const e=s.gamecore_frame_bytes(this.__wbg_ptr);var t=H(e[0],e[1]).slice();return s.__wbindgen_free(e[0],e[1]*1,1),t}frame_ptr(){return s.gamecore_frame_ptr(this.__wbg_ptr)>>>0}is_alive(e){return s.gamecore_is_alive(this.__wbg_ptr,e)!==0}last_input_seq(e){return s.gamecore_last_input_seq(this.__wbg_ptr,e)>>>0}load_map(e){const t=h(e,s.__wbindgen_malloc,s.__wbindgen_realloc),n=p,o=s.gamecore_load_map(this.__wbg_ptr,t,n);if(o[1])throw y(o[0])}map_info(){let e,t;try{const n=s.gamecore_map_info(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}constructor(e){const t=h(e,s.__wbindgen_malloc,s.__wbindgen_realloc),n=p,o=s.gamecore_new(t,n);if(o[2])throw y(o[1]);return this.__wbg_ptr=o[0],K.register(this,this.__wbg_ptr,this),this}pack_body(){const e=s.gamecore_pack_body(this.__wbg_ptr);if(e[1])throw y(e[0])}pack_frame(e,t,n,o,_,i,c,a){var l=ke(c)?0:h(c,s.__wbindgen_malloc,s.__wbindgen_realloc),f=p;return s.gamecore_pack_frame(this.__wbg_ptr,e,t,n,o,_,i,l,f,a)>>>0}players_data(){let e,t;try{const n=s.gamecore_players_data(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}position_of(e){const t=s.gamecore_position_of(this.__wbg_ptr,e);var n=j(t[0],t[1]).slice();return s.__wbindgen_free(t[0],t[1]*4,4),n}remove_actor(e){s.gamecore_remove_actor(this.__wbg_ptr,e)}remove_players_and_shots(){let e,t;try{const n=s.gamecore_remove_players_and_shots(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}remove_scripted_actor(e){s.gamecore_remove_scripted_actor(this.__wbg_ptr,e)}reset_actor(e,t,n,o,_){s.gamecore_reset_actor(this.__wbg_ptr,e,t,n,o,_)}reset_all_vitals(){s.gamecore_reset_all_vitals(this.__wbg_ptr)}serialize_state(){const e=s.gamecore_serialize_state(this.__wbg_ptr);if(e[3])throw y(e[2]);var t=H(e[0],e[1]).slice();return s.__wbindgen_free(e[0],e[1]*1,1),t}spawn_actor(e,t,n,o,_,i){const c=h(t,s.__wbindgen_malloc,s.__wbindgen_realloc),a=p,l=s.gamecore_spawn_actor(this.__wbg_ptr,e,c,a,n,o,_,i);if(l[1])throw y(l[0])}spawn_scripted_actor(e,t,n,o,_,i){const c=h(t,s.__wbindgen_malloc,s.__wbindgen_realloc),a=p,l=s.gamecore_spawn_scripted_actor(this.__wbg_ptr,e,c,a,n,o,_,i);if(l[1])throw y(l[0])}step(e){s.gamecore_step(this.__wbg_ptr,e)}take_events(){let e,t;try{const n=s.gamecore_take_events(this.__wbg_ptr);return e=n[0],t=n[1],g(n[0],n[1])}finally{s.__wbindgen_free(e,t,1)}}}Symbol.dispose&&(N.prototype[Symbol.dispose]=N.prototype.free);function X(){return{__proto__:null,"./vimp_snakes_core_bg.js":{__proto__:null,__wbg_Error_408e67f47ca7b58b:function(e,t){return Error(g(e,t))},__wbg___wbindgen_throw_bb96b2010945f0bc:function(e,t){throw new Error(g(e,t))},__wbindgen_init_externref_table:function(){const e=s.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}}}}const q=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(r=>s.__wbg_clientcore_free(r,1)),K=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(r=>s.__wbg_gamecore_free(r,1));function j(r,e){return r=r>>>0,Se().subarray(r/4,r/4+e)}function H(r,e){return r=r>>>0,x().subarray(r/1,r/1+e)}let k=null;function Se(){return(k===null||k.byteLength===0)&&(k=new Float32Array(s.memory.buffer)),k}function g(r,e){return Ce(r>>>0,e)}let E=null;function x(){return(E===null||E.byteLength===0)&&(E=new Uint8Array(s.memory.buffer)),E}function ke(r){return r==null}function z(r,e){const t=e(r.length*1,1)>>>0;return x().set(r,t/1),p=r.length,t}function h(r,e,t){if(t===void 0){const c=C.encode(r),a=e(c.length,1)>>>0;return x().subarray(a,a+c.length).set(c),p=c.length,a}let n=r.length,o=e(n,1)>>>0;const _=x();let i=0;for(;i<n;i++){const c=r.charCodeAt(i);if(c>127)break;_[o+i]=c}if(i!==n){i!==0&&(r=r.slice(i)),o=t(o,n,n=i+r.length*3,1)>>>0;const c=x().subarray(o+i,o+n),a=C.encodeInto(r,c);i+=a.written,o=t(o,n,i,1)>>>0}return p=i,o}function y(r){const e=s.__wbindgen_externrefs.get(r);return s.__externref_table_dealloc(r),e}let L=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});L.decode();const Ee=2146435072;let P=0;function Ce(r,e){return P+=e,P>=Ee&&(L=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),L.decode(),P=e),L.decode(x().subarray(r,r+e))}const C=new TextEncoder;"encodeInto"in C||(C.encodeInto=function(r,e){const t=C.encode(r);return e.set(t),{read:r.length,written:t.length}});let p=0,s;function J(r,e){return s=r.exports,k=null,E=null,s.__wbindgen_start(),s}async function Oe(r,e){if(typeof Response=="function"&&r instanceof Response){if(!r.ok)throw new Error(`failed to fetch Wasm: ${r.status} ${r.statusText} fetching '${r.url}'`);if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(r,e)}catch(o){if(t(r.type)&&r.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",o);else throw o}const n=await r.arrayBuffer();return await WebAssembly.instantiate(n,e)}else{const n=await WebAssembly.instantiate(r,e);return n instanceof WebAssembly.Instance?{instance:n,module:r}:n}function t(n){switch(n){case"basic":case"cors":case"default":return!0}return!1}}function Re(r){if(s!==void 0)return s;r!==void 0&&(Object.getPrototypeOf(r)===Object.prototype?{module:r}=r:console.warn("using deprecated parameters for `initSync()`; pass a single object instead"));const e=X();r instanceof WebAssembly.Module||(r=new WebAssembly.Module(r));const t=new WebAssembly.Instance(r,e);return J(t)}async function Le(r){if(s!==void 0)return s;r!==void 0&&(Object.getPrototypeOf(r)===Object.prototype?{module_or_path:r}=r:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),r===void 0&&(r=new URL("/assets/vimp_snakes_core_bg-BqmtH1zs.wasm",import.meta.url));const e=X();(typeof r=="string"||typeof Request=="function"&&r instanceof Request||typeof URL=="function"&&r instanceof URL)&&(r=fetch(r));const{instance:t,module:n}=await Oe(await r,e);return J(t)}const Te=Object.freeze(Object.defineProperty({__proto__:null,ClientCore:W,GameCore:N,default:Le,initSync:Re},Symbol.toStringTag,{value:"Module"}));export{Me as default};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vimp-snakes-core",
|
|
3
|
+
"collaborators": [
|
|
4
|
+
"lgick"
|
|
5
|
+
],
|
|
6
|
+
"description": "Vimp Snakes — game simulation + wasm-bindgen ABI on top of vimp-engine-core.",
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"files": [
|
|
10
|
+
"vimp_snakes_core_bg.wasm",
|
|
11
|
+
"vimp_snakes_core.js",
|
|
12
|
+
"vimp_snakes_core.d.ts"
|
|
13
|
+
],
|
|
14
|
+
"main": "vimp_snakes_core.js",
|
|
15
|
+
"types": "vimp_snakes_core.d.ts"
|
|
16
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client half of the core: interpolation of snapshots and prediction of the
|
|
6
|
+
* local snake. Lives in the main thread of the tab.
|
|
7
|
+
*/
|
|
8
|
+
export class ClientCore {
|
|
9
|
+
free(): void;
|
|
10
|
+
[Symbol.dispose](): void;
|
|
11
|
+
/**
|
|
12
|
+
* Ввод игрока: action ('down'/'up') + имя клавиши — в историю
|
|
13
|
+
* предикта.
|
|
14
|
+
*/
|
|
15
|
+
apply_input(action: string, key_name: string, local_now: number): void;
|
|
16
|
+
/**
|
|
17
|
+
* Дамп клиентского состояния для отладки: сетевой буфер
|
|
18
|
+
* (глубина, окно seq, оффсет, последний кадр), свой gameId,
|
|
19
|
+
* размеры hot-буфера и очереди событийных кадров.
|
|
20
|
+
*/
|
|
21
|
+
debug_json(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Чистая распаковка кадра v3 → JSON {port, seq, serverTime,
|
|
24
|
+
* camera, player, snapshot} (замена unpackFrame в тестах);
|
|
25
|
+
* 'null' при несовпадении версии или повреждённом кадре.
|
|
26
|
+
*/
|
|
27
|
+
decode_frame(data: Uint8Array): string;
|
|
28
|
+
/**
|
|
29
|
+
* Указатель на hot-буфер (zero-copy чтение из JS:
|
|
30
|
+
* new Float32Array(wasm.memory.buffer, ptr, len) — view
|
|
31
|
+
* пересоздавать каждый тик, рост памяти WASM инвалидирует
|
|
32
|
+
* buffer).
|
|
33
|
+
*/
|
|
34
|
+
hot_ptr(): number;
|
|
35
|
+
/**
|
|
36
|
+
* Копия hot-буфера (nodejs-таргет; горячий путь браузера —
|
|
37
|
+
* hot_ptr).
|
|
38
|
+
*/
|
|
39
|
+
hot_values(): Float32Array;
|
|
40
|
+
/**
|
|
41
|
+
* Свой gameId из последнего player-блока; -1, если ещё не
|
|
42
|
+
* приходил.
|
|
43
|
+
*/
|
|
44
|
+
my_game_id(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Builds the client core from `{engine: {...}, game: {...}}` assembled
|
|
47
|
+
* by the engine from CONFIG_DATA.
|
|
48
|
+
*/
|
|
49
|
+
constructor(config_json: string);
|
|
50
|
+
/**
|
|
51
|
+
* EMA-оценка (serverTime − localNow); NaN, если кадров ещё не
|
|
52
|
+
* было. Это разница часов (`Date.now` хоста против
|
|
53
|
+
* `performance.now` клиента), а **не** латентность: за оценку
|
|
54
|
+
* RTT её принимать нельзя.
|
|
55
|
+
*/
|
|
56
|
+
offset(): number;
|
|
57
|
+
/**
|
|
58
|
+
* Бинарный кадр из транспорта: распаковка, вставка в буфер по
|
|
59
|
+
* seq (+дедупликация/опоздавшие), reconciliation предикта по
|
|
60
|
+
* player-блоку. false — кадр отброшен (чужой порт/версия/
|
|
61
|
+
* повреждён).
|
|
62
|
+
*/
|
|
63
|
+
push_frame(data: Uint8Array, local_now: number): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Полный сброс (порт CLEAR).
|
|
66
|
+
*/
|
|
67
|
+
reset(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Ресинк часов после долгой паузы вкладки: сброс сетевого
|
|
70
|
+
* буфера и очереди кадров без обнуления предикта.
|
|
71
|
+
*/
|
|
72
|
+
resync(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Весь рендер-тик: выдача пересечённых кадров (фильтр дублей
|
|
75
|
+
* своих эффектов → JSON-очередь), интерполяция, шаг предикта,
|
|
76
|
+
* запись hot-буфера. Возвращает длину hot-буфера в
|
|
77
|
+
* f32-элементах.
|
|
78
|
+
*/
|
|
79
|
+
sample(local_now: number): number;
|
|
80
|
+
/**
|
|
81
|
+
* Смена режима игрок/спектатор (KEYSET_DATA).
|
|
82
|
+
*/
|
|
83
|
+
set_active(active: boolean): void;
|
|
84
|
+
/**
|
|
85
|
+
* Данные карты (MAP_DATA): мир raycast + сброс буфера и
|
|
86
|
+
* предикта.
|
|
87
|
+
*/
|
|
88
|
+
set_map(map_json: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* Model of the local snake (known at authorization). Without it the
|
|
91
|
+
* predictor has no speed and no turn rate and cannot move anything.
|
|
92
|
+
*/
|
|
93
|
+
set_model(model: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Authoritative panel state (PANEL_DATA). Nothing in the prediction
|
|
96
|
+
* depends on it — the crystal count arrives in the player block — but the
|
|
97
|
+
* hook is called unconditionally and the plumbing is cheap to keep.
|
|
98
|
+
*/
|
|
99
|
+
sync_panel(panel_json: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Записи расхождения предикта с авторитетным состоянием
|
|
102
|
+
* (JSON {samples, violations, dropped, maxDelta, records});
|
|
103
|
+
* очередь очищается. 'null' — детектор выключен (конфиг без
|
|
104
|
+
* секции divergence, боевой путь).
|
|
105
|
+
*/
|
|
106
|
+
take_divergence(): string;
|
|
107
|
+
/**
|
|
108
|
+
* Событийные кадры JSON-строкой [{game, camera}, ...] в
|
|
109
|
+
* форме, готовой для applyShot; вызывать при флаге hasFrames
|
|
110
|
+
* hot-буфера, очередь очищается.
|
|
111
|
+
*/
|
|
112
|
+
take_frames(): string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Public ABI of the core for the JS host (Worker / Node test harness).
|
|
117
|
+
* The field names `state` and `packer` are a contract: the macro looks them
|
|
118
|
+
* up literally.
|
|
119
|
+
*/
|
|
120
|
+
export class GameCore {
|
|
121
|
+
free(): void;
|
|
122
|
+
[Symbol.dispose](): void;
|
|
123
|
+
/**
|
|
124
|
+
* Живые игроки плоским массивом [id, teamId, x, y, ...]
|
|
125
|
+
* (аналог Game.getAlivePlayers для меты).
|
|
126
|
+
*/
|
|
127
|
+
alive_players(): Float32Array;
|
|
128
|
+
/**
|
|
129
|
+
* Ввод игрока: seq + action ('down'/'up') + имя клавиши
|
|
130
|
+
* (wire-формат 'seq:action:name' разбирает JS-оболочка).
|
|
131
|
+
*/
|
|
132
|
+
apply_input(game_id: number, seq: number, action: string, key_name: string): void;
|
|
133
|
+
/**
|
|
134
|
+
* Содержал ли последний `pack_body()` событийные блоки
|
|
135
|
+
* (трассеры/бомбы/взрывы/удаления). JS-Worker вызывает после
|
|
136
|
+
* `pack_body()` для выбора канала WebRTC: события → meta
|
|
137
|
+
* (reliable), только позиции → state.
|
|
138
|
+
*/
|
|
139
|
+
body_has_events(): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Полная очистка мира (смена карты).
|
|
142
|
+
*/
|
|
143
|
+
clear(): void;
|
|
144
|
+
/**
|
|
145
|
+
* Курированный дамп мира для отладки (тела, коллайдеры,
|
|
146
|
+
* карта, нав-граф, spatial-сетка, rng, аккумулятор) —
|
|
147
|
+
* читаемая альтернатива serialize_state.
|
|
148
|
+
*/
|
|
149
|
+
debug_json(): string;
|
|
150
|
+
deserialize_state(data: Uint8Array): void;
|
|
151
|
+
/**
|
|
152
|
+
* Копия последнего кадра (nodejs-таргет не отдаёт память
|
|
153
|
+
* наружу; горячий путь браузера использует frame_ptr + память
|
|
154
|
+
* WASM).
|
|
155
|
+
*/
|
|
156
|
+
frame_bytes(): Uint8Array;
|
|
157
|
+
/**
|
|
158
|
+
* Указатель на буфер последнего кадра (zero-copy чтение из JS:
|
|
159
|
+
* new Uint8Array(wasm.memory.buffer, ptr, len)).
|
|
160
|
+
*/
|
|
161
|
+
frame_ptr(): number;
|
|
162
|
+
is_alive(game_id: number): boolean;
|
|
163
|
+
last_input_seq(game_id: number): number;
|
|
164
|
+
/**
|
|
165
|
+
* Загружает карту из JSON (см. scripts/export-maps.js).
|
|
166
|
+
*/
|
|
167
|
+
load_map(map_json: string): void;
|
|
168
|
+
/**
|
|
169
|
+
* Информация о загруженной карте: setId, масштабированные
|
|
170
|
+
* респауны, размеры мира (JSON).
|
|
171
|
+
*/
|
|
172
|
+
map_info(): string;
|
|
173
|
+
/**
|
|
174
|
+
* Builds the core from the JSON config `{engine: {...}, game: {...}}`
|
|
175
|
+
* assembled by the engine from src/config/.
|
|
176
|
+
*/
|
|
177
|
+
constructor(config_json: string);
|
|
178
|
+
/**
|
|
179
|
+
* Пакует broadcast-тело кадра, дренируя накопленные события
|
|
180
|
+
* снапшота. Вызывать один раз на отправляемый кадр (throttle
|
|
181
|
+
* частоты отправки — забота JS-оболочки).
|
|
182
|
+
*/
|
|
183
|
+
pack_body(): void;
|
|
184
|
+
/**
|
|
185
|
+
* Собирает per-user кадр v3 во внутренний буфер, возвращает
|
|
186
|
+
* длину. Кадр читается zero-copy через frame_ptr() + память
|
|
187
|
+
* WASM. player_id < 0 — кадр без player-блока (наблюдатель).
|
|
188
|
+
*/
|
|
189
|
+
pack_frame(server_time: number, seq: number, has_camera: boolean, camera_x: number, camera_y: number, force_reset: boolean, shake: string | null | undefined, player_id: number): number;
|
|
190
|
+
/**
|
|
191
|
+
* Полные данные всех игроков (Game.getPlayersData) одной
|
|
192
|
+
* JSON-строкой для первого кадра (FIRST_SHOT_DATA). Не
|
|
193
|
+
* дренирует накопители.
|
|
194
|
+
*/
|
|
195
|
+
players_data(): string;
|
|
196
|
+
/**
|
|
197
|
+
* Координаты танка [x, y] (скруглены до 2 знаков) или пустой
|
|
198
|
+
* массив.
|
|
199
|
+
*/
|
|
200
|
+
position_of(game_id: number): Float32Array;
|
|
201
|
+
remove_actor(game_id: number): void;
|
|
202
|
+
/**
|
|
203
|
+
* Удаляет игроков и снаряды, возвращает JSON-массив имён для
|
|
204
|
+
* очистки полотна клиентов (Game.removePlayersAndShots).
|
|
205
|
+
*/
|
|
206
|
+
remove_players_and_shots(): string;
|
|
207
|
+
remove_scripted_actor(game_id: number): void;
|
|
208
|
+
/**
|
|
209
|
+
* Респаун/смена команды (аналог Game.changePlayerData).
|
|
210
|
+
*/
|
|
211
|
+
reset_actor(game_id: number, team_id: number, x: number, y: number, angle_deg: number): void;
|
|
212
|
+
/**
|
|
213
|
+
* Сброс здоровья/боезапаса всех танков (аналог Panel.reset).
|
|
214
|
+
*/
|
|
215
|
+
reset_all_vitals(): void;
|
|
216
|
+
/**
|
|
217
|
+
* Дамп состояния симуляции (Worker Handoff, Этап 5.2).
|
|
218
|
+
*/
|
|
219
|
+
serialize_state(): Uint8Array;
|
|
220
|
+
spawn_actor(game_id: number, model: string, team_id: number, x: number, y: number, angle_deg: number): void;
|
|
221
|
+
spawn_scripted_actor(game_id: number, model: string, team_id: number, x: number, y: number, angle_deg: number): void;
|
|
222
|
+
/**
|
|
223
|
+
* Шаг симуляции: фиксированные подшаги физики + ИИ ботов.
|
|
224
|
+
*/
|
|
225
|
+
step(dt: number): void;
|
|
226
|
+
/**
|
|
227
|
+
* События за тик (kill/health/ammo/weapon/shake) одной
|
|
228
|
+
* JSON-строкой; буфер очищается.
|
|
229
|
+
*/
|
|
230
|
+
take_events(): string;
|
|
231
|
+
}
|