@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 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/`.
@@ -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
+ }