WenyiWU0111 commited on
Commit
71e7e75
·
verified ·
1 Parent(s): 368e19c

Playable Echo Climb (round 0 and round 30) in the hero

Browse files
.gitattributes CHANGED
@@ -157,3 +157,4 @@ media/pairs/godot_qwen/tycoon-music-label/final.mp4 filter=lfs diff=lfs merge=lf
157
  media/pairs/godot_qwen/tycoon-music-label/p0.mp4 filter=lfs diff=lfs merge=lfs -text
158
  media/pairs/godot_qwen/visualnovel-lastsignal/final.mp4 filter=lfs diff=lfs merge=lfs -text
159
  media/pairs/godot_qwen/visualnovel-lastsignal/p0.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
157
  media/pairs/godot_qwen/tycoon-music-label/p0.mp4 filter=lfs diff=lfs merge=lfs -text
158
  media/pairs/godot_qwen/visualnovel-lastsignal/final.mp4 filter=lfs diff=lfs merge=lfs -text
159
  media/pairs/godot_qwen/visualnovel-lastsignal/p0.mp4 filter=lfs diff=lfs merge=lfs -text
160
+ media/play/godot_gpt/platformer-echo-climb/r30/index.pck filter=lfs diff=lfs merge=lfs -text
index.html CHANGED
@@ -28,7 +28,8 @@
28
  <div class="hero-bg" aria-hidden="true">
29
  <video id="hero-video" muted loop playsinline preload="auto" data-src="hero_evo.mp4" data-poster="hero_evo.webp"></video>
30
  </div>
31
- <div class="hero-inner">
 
32
  <p class="eyebrow">Agentic verification · Progressive development</p>
33
  <h1>EvoGame</h1>
34
  <p class="hero-sub">Self-Evolving Game Generation through Agentic Verification and Progressive Development</p>
@@ -39,6 +40,20 @@
39
  <a class="btn" href="#" aria-disabled="true">Code <small>soon</small></a>
40
  <a class="btn" href="#" aria-disabled="true">Dataset <small>soon</small></a>
41
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  </div>
43
  </header>
44
 
 
28
  <div class="hero-bg" aria-hidden="true">
29
  <video id="hero-video" muted loop playsinline preload="auto" data-src="hero_evo.mp4" data-poster="hero_evo.webp"></video>
30
  </div>
31
+ <div class="hero-inner has-play">
32
+ <div class="hero-text">
33
  <p class="eyebrow">Agentic verification · Progressive development</p>
34
  <h1>EvoGame</h1>
35
  <p class="hero-sub">Self-Evolving Game Generation through Agentic Verification and Progressive Development</p>
 
40
  <a class="btn" href="#" aria-disabled="true">Code <small>soon</small></a>
41
  <a class="btn" href="#" aria-disabled="true">Dataset <small>soon</small></a>
42
  </div>
43
+ </div>
44
+ <section class="play-card" id="play" aria-labelledby="play-title" hidden>
45
+ <div class="play-top">
46
+ <span class="play-tag">Playable</span>
47
+ <b id="play-title"></b>
48
+ <span class="play-who" id="play-who"></span>
49
+ </div>
50
+ <div class="play-screen" id="play-screen"></div>
51
+ <div class="play-ctl">
52
+ <div class="seg" id="play-rounds" role="group" aria-label="Which build to play"></div>
53
+ <span class="play-keys" id="play-keys"></span>
54
+ <button class="btn btn-sm play-fs" id="play-fs" type="button" hidden>⛶ Fullscreen</button>
55
+ </div>
56
+ </section>
57
  </div>
58
  </header>
59
 
media/play/engine/godot.audio.position.worklet.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**************************************************************************/
2
+ /* godot.audio.position.worklet.js */
3
+ /**************************************************************************/
4
+ /* This file is part of: */
5
+ /* GODOT ENGINE */
6
+ /* https://godotengine.org */
7
+ /**************************************************************************/
8
+ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9
+ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10
+ /* */
11
+ /* Permission is hereby granted, free of charge, to any person obtaining */
12
+ /* a copy of this software and associated documentation files (the */
13
+ /* "Software"), to deal in the Software without restriction, including */
14
+ /* without limitation the rights to use, copy, modify, merge, publish, */
15
+ /* distribute, sublicense, and/or sell copies of the Software, and to */
16
+ /* permit persons to whom the Software is furnished to do so, subject to */
17
+ /* the following conditions: */
18
+ /* */
19
+ /* The above copyright notice and this permission notice shall be */
20
+ /* included in all copies or substantial portions of the Software. */
21
+ /* */
22
+ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23
+ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24
+ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25
+ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26
+ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27
+ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28
+ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29
+ /**************************************************************************/
30
+
31
+ class GodotPositionReportingProcessor extends AudioWorkletProcessor {
32
+ static get parameterDescriptors() {
33
+ return [
34
+ {
35
+ name: 'reset',
36
+ defaultValue: 0,
37
+ minValue: 0,
38
+ maxValue: 1,
39
+ automationRate: 'k-rate',
40
+ },
41
+ ];
42
+ }
43
+
44
+ constructor(...args) {
45
+ super(...args);
46
+ this.position = 0;
47
+ }
48
+
49
+ process(inputs, _outputs, parameters) {
50
+ if (parameters['reset'][0] > 0) {
51
+ this.position = 0;
52
+ }
53
+
54
+ if (inputs.length > 0) {
55
+ const input = inputs[0];
56
+ if (input.length > 0) {
57
+ this.position += input[0].length;
58
+ this.port.postMessage({ type: 'position', data: this.position });
59
+ }
60
+ }
61
+
62
+ return true;
63
+ }
64
+ }
65
+
66
+ registerProcessor('godot-position-reporting-processor', GodotPositionReportingProcessor);
media/play/engine/godot.audio.worklet.js ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**************************************************************************/
2
+ /* audio.worklet.js */
3
+ /**************************************************************************/
4
+ /* This file is part of: */
5
+ /* GODOT ENGINE */
6
+ /* https://godotengine.org */
7
+ /**************************************************************************/
8
+ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9
+ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10
+ /* */
11
+ /* Permission is hereby granted, free of charge, to any person obtaining */
12
+ /* a copy of this software and associated documentation files (the */
13
+ /* "Software"), to deal in the Software without restriction, including */
14
+ /* without limitation the rights to use, copy, modify, merge, publish, */
15
+ /* distribute, sublicense, and/or sell copies of the Software, and to */
16
+ /* permit persons to whom the Software is furnished to do so, subject to */
17
+ /* the following conditions: */
18
+ /* */
19
+ /* The above copyright notice and this permission notice shall be */
20
+ /* included in all copies or substantial portions of the Software. */
21
+ /* */
22
+ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23
+ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24
+ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25
+ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26
+ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27
+ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28
+ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29
+ /**************************************************************************/
30
+
31
+ class RingBuffer {
32
+ constructor(p_buffer, p_state, p_threads) {
33
+ this.buffer = p_buffer;
34
+ this.avail = p_state;
35
+ this.threads = p_threads;
36
+ this.rpos = 0;
37
+ this.wpos = 0;
38
+ }
39
+
40
+ data_left() {
41
+ return this.threads ? Atomics.load(this.avail, 0) : this.avail;
42
+ }
43
+
44
+ space_left() {
45
+ return this.buffer.length - this.data_left();
46
+ }
47
+
48
+ read(output) {
49
+ const size = this.buffer.length;
50
+ let from = 0;
51
+ let to_write = output.length;
52
+ if (this.rpos + to_write > size) {
53
+ const high = size - this.rpos;
54
+ output.set(this.buffer.subarray(this.rpos, size));
55
+ from = high;
56
+ to_write -= high;
57
+ this.rpos = 0;
58
+ }
59
+ if (to_write) {
60
+ output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
61
+ }
62
+ this.rpos += to_write;
63
+ if (this.threads) {
64
+ Atomics.add(this.avail, 0, -output.length);
65
+ Atomics.notify(this.avail, 0);
66
+ } else {
67
+ this.avail -= output.length;
68
+ }
69
+ }
70
+
71
+ write(p_buffer) {
72
+ const to_write = p_buffer.length;
73
+ const mw = this.buffer.length - this.wpos;
74
+ if (mw >= to_write) {
75
+ this.buffer.set(p_buffer, this.wpos);
76
+ this.wpos += to_write;
77
+ if (mw === to_write) {
78
+ this.wpos = 0;
79
+ }
80
+ } else {
81
+ const high = p_buffer.subarray(0, mw);
82
+ const low = p_buffer.subarray(mw);
83
+ this.buffer.set(high, this.wpos);
84
+ this.buffer.set(low);
85
+ this.wpos = low.length;
86
+ }
87
+ if (this.threads) {
88
+ Atomics.add(this.avail, 0, to_write);
89
+ Atomics.notify(this.avail, 0);
90
+ } else {
91
+ this.avail += to_write;
92
+ }
93
+ }
94
+ }
95
+
96
+ class GodotProcessor extends AudioWorkletProcessor {
97
+ constructor() {
98
+ super();
99
+ this.threads = false;
100
+ this.running = true;
101
+ this.lock = null;
102
+ this.notifier = null;
103
+ this.output = null;
104
+ this.output_buffer = new Float32Array();
105
+ this.input = null;
106
+ this.input_buffer = new Float32Array();
107
+ this.port.onmessage = (event) => {
108
+ const cmd = event.data['cmd'];
109
+ const data = event.data['data'];
110
+ this.parse_message(cmd, data);
111
+ };
112
+ }
113
+
114
+ process_notify() {
115
+ if (this.notifier) {
116
+ Atomics.add(this.notifier, 0, 1);
117
+ Atomics.notify(this.notifier, 0);
118
+ }
119
+ }
120
+
121
+ parse_message(p_cmd, p_data) {
122
+ if (p_cmd === 'start' && p_data) {
123
+ const state = p_data[0];
124
+ let idx = 0;
125
+ this.threads = true;
126
+ this.lock = state.subarray(idx, ++idx);
127
+ this.notifier = state.subarray(idx, ++idx);
128
+ const avail_in = state.subarray(idx, ++idx);
129
+ const avail_out = state.subarray(idx, ++idx);
130
+ this.input = new RingBuffer(p_data[1], avail_in, true);
131
+ this.output = new RingBuffer(p_data[2], avail_out, true);
132
+ } else if (p_cmd === 'stop') {
133
+ this.running = false;
134
+ this.output = null;
135
+ this.input = null;
136
+ this.lock = null;
137
+ this.notifier = null;
138
+ } else if (p_cmd === 'start_nothreads') {
139
+ this.output = new RingBuffer(p_data[0], p_data[0].length, false);
140
+ } else if (p_cmd === 'chunk') {
141
+ this.output.write(p_data);
142
+ }
143
+ }
144
+
145
+ static array_has_data(arr) {
146
+ return arr.length && arr[0].length && arr[0][0].length;
147
+ }
148
+
149
+ process(inputs, outputs, parameters) {
150
+ if (!this.running) {
151
+ return false; // Stop processing.
152
+ }
153
+ if (this.output === null) {
154
+ return true; // Not ready yet, keep processing.
155
+ }
156
+ const process_input = GodotProcessor.array_has_data(inputs);
157
+ if (process_input) {
158
+ const input = inputs[0];
159
+ const chunk = input[0].length * input.length;
160
+ if (this.input_buffer.length !== chunk) {
161
+ this.input_buffer = new Float32Array(chunk);
162
+ }
163
+ if (!this.threads) {
164
+ GodotProcessor.write_input(this.input_buffer, input);
165
+ this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
166
+ } else if (this.input.space_left() >= chunk) {
167
+ GodotProcessor.write_input(this.input_buffer, input);
168
+ this.input.write(this.input_buffer);
169
+ } else {
170
+ // this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
171
+ }
172
+ }
173
+ const process_output = GodotProcessor.array_has_data(outputs);
174
+ if (process_output) {
175
+ const output = outputs[0];
176
+ const chunk = output[0].length * output.length;
177
+ if (this.output_buffer.length !== chunk) {
178
+ this.output_buffer = new Float32Array(chunk);
179
+ }
180
+ if (this.output.data_left() >= chunk) {
181
+ this.output.read(this.output_buffer);
182
+ GodotProcessor.write_output(output, this.output_buffer);
183
+ if (!this.threads) {
184
+ this.port.postMessage({ 'cmd': 'read', 'data': chunk });
185
+ }
186
+ } else {
187
+ // this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
188
+ }
189
+ }
190
+ this.process_notify();
191
+ return true;
192
+ }
193
+
194
+ static write_output(dest, source) {
195
+ const channels = dest.length;
196
+ for (let ch = 0; ch < channels; ch++) {
197
+ for (let sample = 0; sample < dest[ch].length; sample++) {
198
+ dest[ch][sample] = source[sample * channels + ch];
199
+ }
200
+ }
201
+ }
202
+
203
+ static write_input(dest, source) {
204
+ const channels = source.length;
205
+ for (let ch = 0; ch < channels; ch++) {
206
+ for (let sample = 0; sample < source[ch].length; sample++) {
207
+ dest[sample * channels + ch] = source[ch][sample];
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ registerProcessor('godot-processor', GodotProcessor);
media/play/engine/godot.js ADDED
The diff for this file is too large to render. See raw diff
 
media/play/engine/godot.wasm.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1fc8bd3b0e1369cda3641b553a99db0a069be6b664483f48f568498acd2f0fb6
3
+ size 9379523
media/play/godot_gpt/platformer-echo-climb/r0/index.pck ADDED
Binary file (74.8 kB). View file
 
media/play/godot_gpt/platformer-echo-climb/r30/index.pck ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3932ef16d7e8a32d485659aa46e26ebe0f08c44bd8ebc77f9114ce87ee11faef
3
+ size 25377968
play.html ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>EvoGame player</title>
7
+ <!--
8
+ One shared Godot 4.6.2 web engine for every exported game.
9
+ play.html?engine=<base url of godot.js/.wasm.gz>&pck=<game pck url>&pck_size=<bytes>&wasm_gz=<bytes>
10
+ The engine's .wasm is stored gzipped; fetch() for it is answered from the .gz and inflated here.
11
+ -->
12
+ <style>
13
+ html, body { margin: 0; height: 100%; background: #05060a; overflow: hidden; }
14
+ #canvas { display: block; width: 100%; height: 100%; outline: none; }
15
+ #boot { position: absolute; inset: 0; display: grid; place-items: center; color: #b4bccd; font: 500 13px/1.5 ui-monospace, "JetBrains Mono", Menlo, monospace; text-align: center; padding: 16px; }
16
+ #boot[hidden] { display: none; }
17
+ #bar { width: min(320px, 70vw); height: 4px; margin: 10px auto 0; border-radius: 2px; background: #1c2130; overflow: hidden; }
18
+ #bar i { display: block; height: 100%; width: 0; background: #c6f432; transition: width .15s; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <canvas id="canvas" tabindex="0">Your browser does not support the canvas element.</canvas>
23
+ <div id="boot"><div><div id="msg">Loading engine…</div><div id="bar"><i></i></div></div></div>
24
+ <script>
25
+ (() => {
26
+ const q = new URLSearchParams(location.search);
27
+ const engineBase = q.get("engine");
28
+ const pck = q.get("pck");
29
+ const pckSize = +q.get("pck_size") || 0;
30
+ const wasmGz = +q.get("wasm_gz") || 0;
31
+ const msg = document.getElementById("msg"), bar = document.querySelector("#bar i");
32
+ const mb = (n) => (n / 1048576).toFixed(1);
33
+ const show = (text, frac) => { msg.textContent = text; if (frac != null) bar.style.width = `${Math.min(100, frac * 100)}%`; };
34
+ const fail = (e) => { console.error(e); show(`Could not start the game: ${e && e.message ? e.message : e}`); };
35
+
36
+ // Answer the engine's request for godot.wasm with the gzipped copy, inflated in the browser.
37
+ const realFetch = window.fetch.bind(window);
38
+ window.fetch = async (input, init) => {
39
+ const url = typeof input === "string" ? input : input.url;
40
+ if (!/\.wasm(\?|$)/.test(url)) return realFetch(input, init);
41
+ const r = await realFetch(url.replace(/\.wasm(\?|$)/, ".wasm.gz$1"), init);
42
+ if (!r.ok) return realFetch(input, init);
43
+ const reader = r.body.getReader(), chunks = [];
44
+ let got = 0;
45
+ for (;;) {
46
+ const { done, value } = await reader.read();
47
+ if (done) break;
48
+ chunks.push(value);
49
+ got += value.length;
50
+ if (wasmGz) show(`Loading engine ${mb(got)} / ${mb(wasmGz)} MB`, (got / wasmGz) * 0.5);
51
+ }
52
+ let blob = new Blob(chunks);
53
+ const head = new Uint8Array(await blob.slice(0, 2).arrayBuffer());
54
+ if (head[0] === 0x1f && head[1] === 0x8b) { // not already inflated by the server
55
+ show("Unpacking engine…", 0.5);
56
+ blob = await new Response(blob.stream().pipeThrough(new DecompressionStream("gzip"))).blob();
57
+ }
58
+ return new Response(blob, { headers: { "Content-Type": "application/wasm", "Content-Length": String(blob.size) } });
59
+ };
60
+
61
+ if (!engineBase || !pck) return fail("missing engine or pck");
62
+ const script = document.createElement("script");
63
+ script.src = `${engineBase}.js`;
64
+ script.onerror = () => fail("engine script did not load");
65
+ script.onload = () => {
66
+ const missing = Engine.getMissingFeatures({ threads: false });
67
+ if (missing.length) return fail(`this browser lacks ${missing.join(", ")}`);
68
+ const engine = new Engine({
69
+ executable: engineBase,
70
+ mainPack: pck,
71
+ args: [],
72
+ canvasResizePolicy: 2,
73
+ focusCanvas: true,
74
+ experimentalVK: false,
75
+ gdextensionLibs: [],
76
+ ensureCrossOriginIsolationHeaders: false,
77
+ fileSizes: { [pck]: pckSize },
78
+ });
79
+ engine.startGame({
80
+ onProgress: (cur, total) => {
81
+ if (total > 0) show(`Loading game ${mb(cur)} / ${mb(total)} MB`, 0.5 + (cur / total) * 0.5);
82
+ },
83
+ }).then(() => {
84
+ document.getElementById("boot").hidden = true;
85
+ document.getElementById("canvas").focus();
86
+ parent.postMessage({ evoPlay: "started" }, "*");
87
+ }, fail);
88
+ };
89
+ document.head.append(script);
90
+ addEventListener("pointerdown", () => document.getElementById("canvas").focus());
91
+ })();
92
+ </script>
93
+ </body>
94
+ </html>
static/css/v1.css CHANGED
@@ -81,3 +81,39 @@
81
  .modal.simple #m-twin { grid-column: 1; }
82
  .modal.simple .m-prompt, .modal.simple .m-h, .modal.simple #m-reqhint, .modal.simple #m-reqs, .modal.simple #m-evo { display: none; }
83
  @media (max-width: 760px) { .modal.simple .modal-info { grid-template-columns: 1fr; } .modal.simple .m-score { grid-column: 1; grid-row: auto; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  .modal.simple #m-twin { grid-column: 1; }
82
  .modal.simple .m-prompt, .modal.simple .m-h, .modal.simple #m-reqhint, .modal.simple #m-reqs, .modal.simple #m-evo { display: none; }
83
  @media (max-width: 760px) { .modal.simple .modal-info { grid-template-columns: 1fr; } .modal.simple .m-score { grid-column: 1; grid-row: auto; } }
84
+
85
+ /* ---------- playable hero ---------- */
86
+ .hero-inner.has-play { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); gap: clamp(28px, 4vw, 56px); align-items: center; max-width: 1320px; width: 100%; text-align: left; }
87
+ .has-play .hero-text { min-width: 0; }
88
+ .has-play h1 { font-size: clamp(56px, 8vw, 112px); }
89
+ .has-play .hero-sub { margin-inline: 0; }
90
+ .has-play .hero-cta { justify-content: flex-start; }
91
+ .play-card { background: rgba(10, 12, 18, .88); border: 1px solid var(--hover-border); border-radius: 18px; padding: 12px; box-shadow: 0 30px 80px rgba(0,0,0,.55); backdrop-filter: blur(10px); display: grid; gap: 10px; min-width: 0; }
92
+ .play-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 2px 4px 0; }
93
+ .play-top b { font: 700 17px var(--display); }
94
+ .play-tag { font: 700 10px var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--accent-ink); background: var(--accent); padding: 3px 7px; border-radius: 5px; }
95
+ .play-who { color: var(--muted); font-size: 13px; display: inline-flex; align-items: center; gap: 6px; }
96
+ .play-screen { position: relative; aspect-ratio: 16 / 9; max-width: 100%; border-radius: 10px; overflow: hidden; background: var(--media-bg); }
97
+ .play-screen iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
98
+ .play-poster { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; cursor: pointer; background: var(--media-bg); display: grid; place-items: center; color: var(--text); }
99
+ .play-poster img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: brightness(.55); transition: filter .2s, transform .4s; }
100
+ .play-poster:hover img, .play-poster:focus-visible img { filter: brightness(.7); transform: scale(1.02); }
101
+ .play-poster:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
102
+ .play-go { position: relative; display: grid; justify-items: center; gap: 10px; }
103
+ .play-go i { width: 76px; height: 76px; border-radius: 50%; background: var(--accent); display: grid; place-items: center; box-shadow: 0 0 0 10px rgba(198,244,50,.18); transition: transform .2s; }
104
+ .play-go i::before { content: ""; margin-left: 6px; border-left: 22px solid var(--accent-ink); border-top: 14px solid transparent; border-bottom: 14px solid transparent; }
105
+ .play-poster:hover .play-go i { transform: scale(1.06); }
106
+ .play-go b { font: 700 18px var(--display); text-shadow: 0 2px 12px rgba(0,0,0,.7); }
107
+ .play-go span { font: 500 12px var(--mono); color: var(--text-2); text-shadow: 0 1px 8px rgba(0,0,0,.8); }
108
+ .play-ctl { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
109
+ .play-ctl .seg button { display: inline-flex; gap: 8px; align-items: baseline; }
110
+ .play-ctl .seg button b { font: 700 12px var(--mono); color: var(--muted); }
111
+ .play-ctl .seg button.on b { color: var(--accent); }
112
+ .play-keys { font: 500 12px var(--mono); color: var(--muted); }
113
+ .play-fs { margin-left: auto; }
114
+ @media (max-width: 1020px) {
115
+ .hero-inner.has-play { grid-template-columns: 1fr; text-align: center; max-width: 820px; }
116
+ .has-play .hero-sub { margin-inline: auto; }
117
+ .has-play .hero-cta { justify-content: center; }
118
+ .play-card { text-align: left; }
119
+ }
static/data/play.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "engine": {
3
+ "base": "play/engine/godot",
4
+ "wasm": 37695054,
5
+ "wasm_gz": 9379523
6
+ },
7
+ "games": [
8
+ {
9
+ "arm": "godot_gpt",
10
+ "task": "platformer-echo-climb",
11
+ "title": "Echo Climb",
12
+ "model": "GPT-5.5",
13
+ "controls": "A / D move \u00b7 Space jump \u00b7 R restart",
14
+ "pair": "pairs/godot_gpt/platformer-echo-climb",
15
+ "scores": {
16
+ "r0": 0.4154,
17
+ "r30": 0.8908
18
+ },
19
+ "builds": {
20
+ "r0": {
21
+ "pck": "play/godot_gpt/platformer-echo-climb/r0/index.pck",
22
+ "size": 74800
23
+ },
24
+ "r30": {
25
+ "pck": "play/godot_gpt/platformer-echo-climb/r30/index.pck",
26
+ "size": 25377968
27
+ }
28
+ }
29
+ }
30
+ ]
31
+ }
static/js/v1.js CHANGED
@@ -12,6 +12,56 @@ Evo.boot(({ games, arms }) => {
12
  const hv = $("#hero-video");
13
  Evo.mountHeroVideo(hv, { src: hv.dataset.src, poster: hv.dataset.poster });
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  // ---------- headline: one row per generator ----------
16
  $("#headline").replaceChildren(...arms.map((a) => {
17
  const p0 = mean(a.games.map((g) => g.reward)), p1 = mean(a.games.map((g) => g.repaired.reward));
 
12
  const hv = $("#hero-video");
13
  Evo.mountHeroVideo(hv, { src: hv.dataset.src, poster: hv.dataset.poster });
14
 
15
+ // ---------- playable hero: the real round-0 and round-30 builds, in the browser ----------
16
+ (async function playable() {
17
+ const cfg = window.EVO_PLAY || await fetch(Evo.data("play.json")).then((r) => (r.ok ? r.json() : null)).catch(() => null);
18
+ const game = cfg?.games?.[0];
19
+ if (!game) return;
20
+ const screen = $("#play-screen"), rounds = $("#play-rounds"), fsBtn = $("#play-fs");
21
+ const ROUNDS = [["r0", "Round 0"], ["r30", "Round 30"]];
22
+ const label = (r) => ROUNDS.find(([k]) => k === r)[1];
23
+ let sel = "r30", running = false;
24
+ $("#play-title").textContent = game.title;
25
+ $("#play-who").replaceChildren(dot(game.model), `${game.model} · Godot 4`);
26
+ $("#play-keys").textContent = game.controls;
27
+
28
+ function idle() {
29
+ running = false;
30
+ fsBtn.hidden = true;
31
+ const mbs = Math.round((cfg.engine.wasm_gz + game.builds[sel].size) / 1048576);
32
+ const poster = h("button", { class: "play-poster", type: "button", "aria-label": `Play ${game.title}, ${label(sel)}` },
33
+ h("img", { src: Evo.media(`${game.pair}/${sel === "r0" ? "p0" : "final"}.webp`), alt: "" }),
34
+ h("span", { class: "play-go" }, h("i"), h("b", {}, `Play ${label(sel)} in your browser`), h("span", {}, `${mbs} MB download · keyboard`)));
35
+ poster.addEventListener("click", start);
36
+ screen.replaceChildren(poster);
37
+ }
38
+ function start() {
39
+ running = true;
40
+ Evo.pauseAll();
41
+ $("#hero-video").pause();
42
+ const b = game.builds[sel];
43
+ const src = "play.html?" + new URLSearchParams({
44
+ engine: Evo.media(cfg.engine.base), wasm_gz: cfg.engine.wasm_gz, pck: Evo.media(b.pck), pck_size: b.size,
45
+ });
46
+ const frame = h("iframe", { src, title: `${game.title}, ${label(sel)}`, allow: "autoplay; fullscreen; gamepad" });
47
+ frame.addEventListener("load", () => frame.contentWindow?.focus());
48
+ screen.replaceChildren(frame);
49
+ fsBtn.hidden = false;
50
+ }
51
+ rounds.replaceChildren(...ROUNDS.map(([r, t]) => h("button", {
52
+ type: "button", class: r === sel ? "on" : "", "data-r": r,
53
+ onclick: () => {
54
+ if (sel === r) return;
55
+ sel = r;
56
+ rounds.querySelectorAll("button").forEach((x) => x.classList.toggle("on", x.dataset.r === r));
57
+ running ? start() : idle();
58
+ },
59
+ }, t, h("b", {}, s100(game.scores[r])))));
60
+ fsBtn.addEventListener("click", () => screen.querySelector("iframe")?.requestFullscreen?.());
61
+ idle();
62
+ $("#play").hidden = false;
63
+ })();
64
+
65
  // ---------- headline: one row per generator ----------
66
  $("#headline").replaceChildren(...arms.map((a) => {
67
  const p0 = mean(a.games.map((g) => g.reward)), p1 = mean(a.games.map((g) => g.repaired.reward));