Tencent Hy4 AI Studio Coding Test: Crane, Globe & Wizard Game

Three standalone HTML outputs from Hy4 New, including correction turns, full source, and what remains untested.
Co-Founder & Lead Programmer of AcceleratedLogic AI
hy4. This is a hands-on artifact report, not a controlled benchmark or a model ranking.What happened in the session
tencent-hy4-glass-crane.html, tencent-hy4-procedural-globe.html, and tencent-hy4-wizard-islands.html in the Downloads folder. The files embedded below are those saved source documents. I checked that each begins with an HTML doctype and ends with </html>, and recorded their on-disk sizes. A Chromium smoke check loaded all three saved files without uncaught page errors.| Artifact | Saved file size | Runtime dependencies | Source-level result |
|---|---|---|---|
| Glass sphere and crane | 10,205 bytes | None; inline SVG and CSS | Complete HTML; static illustration |
| Procedural globe | 5,434 bytes | Three.js 0.160.0 and OrbitControls from unpkg | Complete HTML; procedural canvas texture, glow, particle ring, drag/zoom controls, and speed slider are present in source |
| Floating-island wizard game | 15,207 bytes | Three.js from unpkg | Complete HTML; movement, jumping, turrets, HUD, health, score, audio functions, and restart/game-over states are present in source |
What the files show
1. SVG glass sphere with a crane
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photorealistic 3D Glass Sphere with Origami Crane</title>
<style>
html,body{margin:0;padding:0;width:100%;height:100%;background:#030507;display:flex;justify-content:center;align-items:center;overflow:hidden}
svg{width:100vmin;height:100vmin;max-width:1000px;max-height:1000px;display:block}
</style>
</head>
<body>
<svg viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="bg" cx="50%" cy="40%" r="75%"><stop offset="0%" stop-color="#1c2b3a"/><stop offset="55%" stop-color="#0a121a"/><stop offset="100%" stop-color="#030507"/></radialGradient>
<radialGradient id="sh" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#000" stop-opacity="1"/><stop offset="60%" stop-color="#000" stop-opacity=".8"/><stop offset="100%" stop-color="#000" stop-opacity="0"/></radialGradient>
<radialGradient id="cs" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#fff" stop-opacity="1"/><stop offset="30%" stop-color="#b3e5fc" stop-opacity=".8"/><stop offset="70%" stop-color="#4fc3f7" stop-opacity=".3"/><stop offset="100%" stop-color="#0288d1" stop-opacity="0"/></radialGradient>
<radialGradient id="gb" cx="50%" cy="50%" r="50%" fx="42%" fy="38%"><stop offset="0%" stop-color="#162633" stop-opacity=".85"/><stop offset="65%" stop-color="#08121c" stop-opacity=".95"/><stop offset="100%" stop-color="#000" stop-opacity="1"/></radialGradient>
<radialGradient id="cr" cx="50%" cy="50%" r="50%"><stop offset="82%" stop-color="#000" stop-opacity="0"/><stop offset="91%" stop-color="#4fc3f7" stop-opacity=".35"/><stop offset="95%" stop-color="#e1f5fe" stop-opacity=".75"/><stop offset="98%" stop-color="#fff" stop-opacity=".95"/><stop offset="100%" stop-color="#0277bd" stop-opacity=".6"/></radialGradient>
<radialGradient id="gv" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#000" stop-opacity="0"/><stop offset="62%" stop-color="#000" stop-opacity="0"/><stop offset="85%" stop-color="#001525" stop-opacity=".55"/><stop offset="100%" stop-color="#00050a" stop-opacity=".95"/></radialGradient>
<radialGradient id="em" cx="50%" cy="50%" r="50%"><stop offset="55%" stop-color="#000"/><stop offset="80%" stop-color="#555"/><stop offset="100%" stop-color="#fff"/></radialGradient>
<mask id="emask"><circle cx="500" cy="450" r="350" fill="url(#em)"/></mask>
<linearGradient id="hm" x1="10%" y1="0%" x2="90%" y2="100%"><stop offset="0%" stop-color="#fff" stop-opacity=".95"/><stop offset="35%" stop-color="#fff" stop-opacity=".25"/><stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>
<linearGradient id="hb" x1="100%" y1="100%" x2="0%" y2="0%"><stop offset="0%" stop-color="#84ffff" stop-opacity=".7"/><stop offset="45%" stop-color="#80d8ff" stop-opacity=".15"/><stop offset="100%" stop-color="#80d8ff" stop-opacity="0"/></linearGradient>
<linearGradient id="wr" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#fff" stop-opacity="0"/><stop offset="25%" stop-color="#fff" stop-opacity=".8"/><stop offset="50%" stop-color="#fff" stop-opacity=".1"/><stop offset="75%" stop-color="#fff" stop-opacity=".9"/><stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>
<linearGradient id="rs" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#fff" stop-opacity=".85"/><stop offset="25%" stop-color="#fff" stop-opacity=".1"/><stop offset="65%" stop-color="#4fc3f7" stop-opacity=".35"/><stop offset="100%" stop-color="#fff" stop-opacity=".7"/></linearGradient>
<linearGradient id="gl" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ffab91"/><stop offset="100%" stop-color="#ff5252"/></linearGradient>
<linearGradient id="gm" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#e53935"/><stop offset="100%" stop-color="#c62828"/></linearGradient>
<linearGradient id="gd" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#8e0000"/><stop offset="100%" stop-color="#4a0000"/></linearGradient>
<linearGradient id="gs" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#3e0000"/><stop offset="100%" stop-color="#1a0000"/></linearGradient>
<filter id="sb" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="8"/></filter>
<filter id="hbF" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="22"/></filter>
<filter id="shb" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="28"/></filter>
<filter id="gw" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="cd" x="-40%" y="-40%" width="180%" height="180%"><feDropShadow dx="28" dy="38" stdDeviation="18" flood-color="#000814" flood-opacity=".85"/></filter>
<filter id="cc" x="-20%" y="-20%" width="140%" height="140%"><feFlood flood-color="#00e5ff" result="f"/><feComposite in="f" in2="SourceAlpha" operator="in" result="t"/><feGaussianBlur in="t" stdDeviation="4" result="b"/><feOffset in="b" dx="-7" dy="3" result="o"/><feComponentTransfer in="o"><feFuncA type="linear" slope=".75"/></feComponentTransfer></filter>
<filter id="crF" x="-20%" y="-20%" width="140%" height="140%"><feFlood flood-color="#ff0055" result="f"/><feComposite in="f" in2="SourceAlpha" operator="in" result="t"/><feGaussianBlur in="t" stdDeviation="4" result="b"/><feOffset in="b" dx="7" dy="-3" result="o"/><feComponentTransfer in="o"><feFuncA type="linear" slope=".75"/></feComponentTransfer></filter>
<clipPath id="sc"><circle cx="500" cy="450" r="350"/></clipPath>
<g id="crane">
<polygon points="510,490 640,320 560,410" fill="url(#gs)"/>
<polygon points="530,460 640,320 560,410" fill="url(#gd)"/>
<polygon points="500,420 280,260 380,380" fill="url(#gl)"/>
<polygon points="500,420 380,380 430,500" fill="url(#gm)"/>
<polygon points="280,260 320,290 380,380" fill="url(#gd)"/>
<polygon points="420,440 380,380 430,500" fill="url(#gs)"/>
<polygon points="500,420 720,260 620,380" fill="url(#gm)"/>
<polygon points="500,420 620,380 570,500" fill="url(#gd)"/>
<polygon points="720,260 680,290 620,380" fill="url(#gs)"/>
<polygon points="580,440 620,380 570,500" fill="url(#gm)"/>
<polygon points="470,460 490,490 390,310 370,280" fill="url(#gm)"/>
<polygon points="470,460 450,470 360,290 370,280" fill="url(#gl)"/>
<polygon points="370,280 390,310 420,310 400,280" fill="url(#gl)"/>
<polygon points="370,280 400,280 350,340" fill="url(#gm)"/>
<polygon points="390,310 420,310 350,340" fill="url(#gd)"/>
<polygon points="500,420 430,500 500,530" fill="url(#gl)"/>
<polygon points="500,420 570,500 500,530" fill="url(#gm)"/>
<polygon points="430,500 500,580 500,530" fill="url(#gm)"/>
<polygon points="570,500 500,580 500,530" fill="url(#gd)"/>
</g>
</defs>
<rect width="1000" height="1000" fill="url(#bg)"/>
<ellipse cx="500" cy="800" rx="290" ry="48" fill="url(#sh)" opacity=".9" filter="url(#shb)"/>
<ellipse cx="500" cy="800" rx="150" ry="22" fill="#000" opacity=".95" filter="url(#sb)"/>
<ellipse cx="500" cy="800" rx="75" ry="18" fill="url(#cs)" opacity=".85" filter="url(#sb)"/>
<ellipse cx="500" cy="800" rx="22" ry="6" fill="#fff" opacity=".9" filter="url(#gw)"/>
<g clip-path="url(#sc)">
<circle cx="500" cy="450" r="350" fill="url(#gb)"/>
<circle cx="500" cy="450" r="350" fill="url(#cr)"/>
<g transform="translate(500,450) scale(1.08) translate(-500,-450)">
<g mask="url(#emask)">
<use href="#crane" filter="url(#cc)"/>
<use href="#crane" filter="url(#crF)"/>
</g>
<g filter="url(#cd)">
<use href="#crane" stroke="#2a0000" stroke-width="1.2" stroke-opacity=".65" stroke-linejoin="round"/>
</g>
<path d="M280,260 L500,420 L720,260 M370,280 L470,460 M500,420 L430,500 M500,420 L500,530 M640,320 L530,460" fill="none" stroke="rgba(255,210,210,.75)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M500,530 L500,580" fill="none" stroke="rgba(255,171,145,.5)" stroke-width="1"/>
</g>
<circle cx="500" cy="450" r="350" fill="url(#gv)"/>
<rect x="50" y="260" width="900" height="55" fill="url(#wr)" opacity=".12" filter="url(#sb)" transform="rotate(-15 500 300)"/>
<path d="M220,250 C350,110 560,110 660,200 C550,175 400,215 320,350 C265,435 195,365 220,250 Z" fill="url(#hm)" filter="url(#sb)" opacity=".85"/>
<ellipse cx="310" cy="225" rx="95" ry="38" transform="rotate(-38 310 225)" fill="#fff" filter="url(#hbF)" opacity=".9"/>
<ellipse cx="690" cy="690" rx="190" ry="65" transform="rotate(45 690 690)" fill="url(#hb)" filter="url(#hbF)" opacity=".65"/>
<path d="M765,615 A325,325 0 0,1 555,775 A345,345 0 0,0 785,645 Z" fill="url(#hb)" opacity=".55" filter="url(#sb)"/>
<g transform="translate(275,215)">
<circle cx="0" cy="0" r="9" fill="#fff" filter="url(#gw)" opacity=".9"/>
<circle cx="0" cy="0" r="2.5" fill="#fff"/>
<path d="M-20,0 L20,0 M0,-20 L0,20" stroke="#fff" stroke-width="1.2" opacity=".7" filter="url(#gw)"/>
<path d="M-11,0 L11,0 M0,-11 L0,11" stroke="#fff" stroke-width="2.2" opacity="1" stroke-linecap="round"/>
</g>
<circle cx="735" cy="665" r="3" fill="#fff" filter="url(#gw)" opacity=".8"/>
<circle cx="497" cy="450" r="348" fill="none" stroke="rgba(0,229,255,.45)" stroke-width="5" filter="url(#sb)"/>
<circle cx="503" cy="450" r="348" fill="none" stroke="rgba(255,0,85,.35)" stroke-width="5" filter="url(#sb)"/>
</g>
<circle cx="500" cy="450" r="350" fill="none" stroke="url(#rs)" stroke-width="2.5"/>
<circle cx="500" cy="450" r="348.5" fill="none" stroke="rgba(255,255,255,.35)" stroke-width="1"/>
<circle cx="500" cy="450" r="351" fill="none" stroke="rgba(0,0,0,.6)" stroke-width="2" filter="url(#sb)"/>
</svg>
</body>
</html>
2. Interactive procedural globe
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural 3D Globe</title>
<style>
body{margin:0;overflow:hidden;background:#02050a;font-family:system-ui,sans-serif}canvas{display:block}#ui{position:absolute;bottom:30px;left:50%;transform:translateX(-50%);background:rgba(10,20,35,.7);padding:12px 24px;border-radius:30px;border:1px solid rgba(136,204,255,.3);display:flex;align-items:center;gap:15px;color:#cce6ff;font-size:14px;backdrop-filter:blur(8px);z-index:10;user-select:none}input[type=range]{appearance:none;width:150px;height:4px;background:rgba(136,204,255,.2);border-radius:2px;outline:none}input[type=range]::-webkit-slider-thumb{appearance:none;width:16px;height:16px;border-radius:50%;background:#88ccff;box-shadow:0 0 10px #88ccff;cursor:pointer}
</style>
<script type="importmap">
{"imports":{"three":"https://unpkg.com/[email protected]/build/three.module.js","three/addons/":"https://unpkg.com/[email protected]/examples/jsm/"}}
</script>
</head>
<body>
<div id="ui"><span>Rotation Speed</span><input type="range" id="spd" min="0" max="0.05" step="0.001" value="0.005"></div>
<script type="module">
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x02050a);
const camera = new THREE.PerspectiveCamera(45, innerWidth/innerHeight, .1, 100);
camera.position.set(0, 2, 6);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
controls.minDistance = 2.5;
controls.maxDistance = 12;
scene.add(new THREE.AmbientLight(0xffffff, .2));
const sun = new THREE.DirectionalLight(0xffffff, 1.5);
sun.position.set(5, 3, 5);
scene.add(sun);
const rim = new THREE.DirectionalLight(0x4488ff, .5);
rim.position.set(-5, -2, -4);
scene.add(rim);
const cvs = document.createElement('canvas');
cvs.width = 1024; cvs.height = 512;
const ctx = cvs.getContext('2d', {willReadFrequently:true});
ctx.fillStyle = '#04101e'; ctx.fillRect(0,0,1024,512);
ctx.fillStyle = '#1a4d2e';
for(let i=0; i<20; i++){
const cx=Math.random()*1024, cy=60+Math.random()*392, r=30+Math.random()*70;
for(let j=0; j<40; j++){
const x=cx+(Math.random()-.5)*r*2.5, y=cy+(Math.random()-.5)*r*2.5, rad=8+Math.random()*r*.7;
for(const dx of [-1024,0,1024]){ctx.beginPath();ctx.arc(x+dx,y,rad,0,Math.PI*2);ctx.fill();}
}
}
const img = ctx.getImageData(0,0,1024,512), d = img.data;
const hash=(x,y)=>{const n=Math.sin(x*12.9898+y*78.233)*43758.5453;return n-Math.floor(n);};
for(let i=0; i<d.length; i+=4){
const px=(i/4)%1024|0, py=(i/4/1024)|0, n=hash(px>>1,py>>1);
if(d[i+1]>40){
d[i]=25+n*50; d[i+1]=55+n*75; d[i+2]=20+n*30;
if(py<45||py>467){const ice=Math.max(0,(45-py)/45+(py-467)/45);if(ice>n*.7){d[i]=220;d[i+1]=240;d[i+2]=255;}}
} else {
d[i]=4+n*6; d[i+1]=12+n*18; d[i+2]=26+n*38;
if(py<25||py>487){d[i]=195;d[i+1]=225;d[i+2]=252;}
}
}
ctx.putImageData(img,0,0);
const tex = new THREE.CanvasTexture(cvs);
tex.colorSpace = THREE.SRGBColorSpace;
tex.wrapS = THREE.RepeatWrapping;
const globe = new THREE.Group();
globe.rotation.z = 23.5 * Math.PI / 180;
scene.add(globe);
globe.add(new THREE.Mesh(
new THREE.SphereGeometry(1.5, 64, 64),
new THREE.MeshStandardMaterial({map:tex, bumpMap:tex, bumpScale:.03, roughness:.85})
));
globe.add(new THREE.Mesh(
new THREE.SphereGeometry(1.65, 64, 64),
new THREE.ShaderMaterial({
vertexShader:`varying vec3 vN;void main(){vN=normalize(normalMatrix*normal);gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`,
fragmentShader:`varying vec3 vN;void main(){float i=pow(max(0.,1.-dot(vN,vec3(0.,0.,1.))),3.);gl_FragColor=vec4(.3,.6,1.,1.)*i;}`,
blending:THREE.AdditiveBlending, transparent:true, depthWrite:false
})
));
const pCount=4000, pGeo=new THREE.BufferGeometry(), pPos=new Float32Array(pCount*3), pCol=new Float32Array(pCount*3);
for(let i=0;i<pCount;i++){
const a=Math.random()*Math.PI*2, r=2.1+(Math.random()-.5)*.4;
pPos[i*3]=Math.cos(a)*r; pPos[i*3+1]=(Math.random()-.5)*.12; pPos[i*3+2]=Math.sin(a)*r;
const b=.4+Math.random()*.6;
pCol[i*3]=.2*b; pCol[i*3+1]=.7*b; pCol[i*3+2]=1.*b;
}
pGeo.setAttribute('position',new THREE.BufferAttribute(pPos,3));
pGeo.setAttribute('color',new THREE.BufferAttribute(pCol,3));
const ring = new THREE.Points(pGeo,new THREE.PointsMaterial({size:.025,vertexColors:true,transparent:true,blending:THREE.AdditiveBlending,depthWrite:false}));
ring.rotation.x=Math.PI/5; ring.rotation.z=Math.PI/12;
scene.add(ring);
const spd = document.getElementById('spd'), clock = new THREE.Clock();
addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});
function anim(){
requestAnimationFrame(anim);
const dt=Math.min(clock.getDelta(),.1), v=parseFloat(spd.value);
globe.rotation.y += v*dt*60;
ring.rotation.y -= v*dt*90;
controls.update();
renderer.render(scene,camera);
}
anim();
</script>
</body>
</html>
3. Wizard game on floating islands
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Pastel Archipelago</title>
<style>
body{margin:0;overflow:hidden;background:#111;font-family:'Segoe UI',Tahoma,sans-serif;user-select:none;-webkit-user-select:none}
canvas{display:block;touch-action:none}
#hud{position:absolute;top:20px;left:20px;color:#fff;font-size:26px;font-weight:bold;text-shadow:0 2px 6px rgba(0,0,0,.8);pointer-events:none;z-index:2;display:flex;gap:20px}
#flash{position:absolute;inset:0;background:#ff3333;opacity:0;transition:opacity .15s ease-out;pointer-events:none;z-index:3}
.screen{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;background:rgba(20,10,40,.8);color:#fff;z-index:10;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
h1{font-size:52px;margin:0 0 10px;color:#aaddff;text-shadow:0 0 25px #88ccff;text-align:center}
p{font-size:18px;margin:5px 0 30px;color:#ccddff;text-align:center;max-width:600px;line-height:1.5}
button{padding:16px 45px;font-size:24px;font-weight:bold;color:#221144;background:#aaddff;border:none;border-radius:40px;cursor:pointer;box-shadow:0 0 20px #88ccff;transition:transform .2s,background .2s}
button:hover{transform:scale(1.08);background:#fff}
span{color:#88ffcc}
</style>
<script type="importmap">
{"imports":{"three":"https://unpkg.com/[email protected]/build/three.module.js"}}
</script>
</head>
<body>
<div id="hud"><div>❤️ <span id="hp">❤️❤️❤️</span></div><div>💎 <span id="sc">0</span></div></div>
<div id="flash"></div>
<div id="start" class="screen">
<h1>Pastel Archipelago</h1>
<p><b>WASD</b> to Move | <b>SPACE</b> to Jump | <b>DRAG</b> to Look<br><br>Collect glowing crystals and dodge the elemental turrets!<br>Watch out for the edges and the shifting sky.</p>
<button id="btnStart">START</button>
</div>
<div id="go" class="screen" style="display:none">
<h1>Game Over</h1>
<p>Final Score: <span id="fs">0</span></p>
<button id="btnRestart">RESTART</button>
</div>
<script type="module">
import * as THREE from 'three';
// --- Audio System ---
let actx;
function initAudio(){
if(!actx) actx = new (window.AudioContext || window.webkitAudioContext)();
if(actx.state === 'suspended') actx.resume();
}
function sfx(type){
if(!actx) return;
const o = actx.createOscillator(), g = actx.createGain();
o.connect(g); g.connect(actx.destination);
const t = actx.currentTime;
if(type==='jump'){ o.type='sine'; o.frequency.setValueAtTime(300,t); o.frequency.exponentialRampToValueAtTime(650,t+.12); g.gain.setValueAtTime(.2,t); g.gain.exponentialRampToValueAtTime(.01,t+.12); o.start(t); o.stop(t+.12); }
else if(type==='collect'){ o.type='square'; o.frequency.setValueAtTime(800,t); o.frequency.setValueAtTime(1200,t+.06); o.frequency.setValueAtTime(1800,t+.12); g.gain.setValueAtTime(.08,t); g.gain.exponentialRampToValueAtTime(.01,t+.2); o.start(t); o.stop(t+.2); }
else if(type==='hit'){ o.type='sawtooth'; o.frequency.setValueAtTime(180,t); o.frequency.exponentialRampToValueAtTime(40,t+.25); g.gain.setValueAtTime(.3,t); g.gain.exponentialRampToValueAtTime(.01,t+.25); o.start(t); o.stop(t+.25); }
else if(type==='shoot'){ o.type='triangle'; o.frequency.setValueAtTime(450,t); o.frequency.exponentialRampToValueAtTime(150,t+.1); g.gain.setValueAtTime(.08,t); g.gain.exponentialRampToValueAtTime(.01,t+.1); o.start(t); o.stop(t+.1); }
}
// --- Core Setup ---
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x111133, 25, 70);
const camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, .1, 150);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const amb = new THREE.AmbientLight(0xffffff, .4);
scene.add(amb);
const sun = new THREE.DirectionalLight(0xfff0dd, 1.5);
sun.castShadow = true;
sun.shadow.camera.left = -35; sun.shadow.camera.right = 35;
sun.shadow.camera.top = 35; sun.shadow.camera.bottom = -35;
sun.shadow.mapSize.width = 1024; sun.shadow.mapSize.height = 1024;
scene.add(sun);
// --- Environment ---
const islands = [
{x:0, z:0, r:9, y:0}, {x:14, z:-9, r:5.5, y:2}, {x:-12, z:-13, r:6.5, y:1},
{x:-16, z:8, r:4.5, y:3}, {x:12, z:14, r:5.5, y:1}, {x:0, z:23, r:7.5, y:-1}
];
const iMats = [
new THREE.MeshLambertMaterial({color:0xaa88cc, flatShading:true}),
new THREE.MeshLambertMaterial({color:0x99eeaa, flatShading:true}),
new THREE.MeshLambertMaterial({color:0x665599, flatShading:true})
];
const treeMat = new THREE.MeshLambertMaterial({color:0x44bb77, flatShading:true});
islands.forEach(i => {
const m = new THREE.Mesh(new THREE.CylinderGeometry(i.r, i.r*.65, 4.5, 7), iMats);
m.position.set(i.x, i.y - 2.25, i.z);
m.castShadow = true; m.receiveShadow = true;
scene.add(m);
for(let k=0; k<3; k++){
const a = Math.random()*6.28, r = Math.random()*i.r*.65;
const t = new THREE.Mesh(new THREE.ConeGeometry(.7, 2.2, 6), treeMat);
t.position.set(i.x + Math.cos(a)*r, i.y + 1.1, i.z + Math.sin(a)*r);
t.castShadow = true;
scene.add(t);
}
});
// Dust Particles
const pCount = 350, pGeo = new THREE.BufferGeometry(), pPos = new Float32Array(pCount*3);
for(let i=0; i<pCount; i++){
pPos[i*3] = (Math.random()-.5)*70;
pPos[i*3+1] = Math.random()*35 - 8;
pPos[i*3+2] = (Math.random()-.5)*70;
}
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
const dust = new THREE.Points(pGeo, new THREE.PointsMaterial({color:0xffffff, size:.22, transparent:true, opacity:.65, depthWrite:false}));
scene.add(dust);
// --- Wizard Player ---
const wizard = new THREE.Group();
const wMat = new THREE.MeshLambertMaterial({color:0x9977dd, flatShading:true});
const sMat = new THREE.MeshLambertMaterial({color:0xffddbb, flatShading:true});
const robe = new THREE.Mesh(new THREE.ConeGeometry(.55, 1.3, 7), wMat); robe.position.y = .65;
const head = new THREE.Mesh(new THREE.SphereGeometry(.26, 7, 7), sMat); head.position.y = 1.45;
const hat = new THREE.Mesh(new THREE.ConeGeometry(.48, .75, 7), wMat); hat.position.y = 1.9;
const staff = new THREE.Mesh(new THREE.CylinderGeometry(.045,.045,1.7,5), new THREE.MeshLambertMaterial({color:0x775533})); staff.position.set(.5, .95, .15); staff.rotation.z = -.18;
const gem = new THREE.Mesh(new THREE.OctahedronGeometry(.13), new THREE.MeshBasicMaterial({color:0x00ffff})); gem.position.set(.42, 1.82, .15);
wizard.add(robe, head, hat, staff, gem);
wizard.traverse(c => { if(c.isMesh) c.castShadow = true; });
scene.add(wizard);
// --- Entities ---
const crystals = [];
const crGeo = new THREE.OctahedronGeometry(.45);
const crMat = new THREE.MeshStandardMaterial({color:0x66ffff, emissive:0x00bbbb, emissiveIntensity:.8, flatShading:true});
for(let i=0; i<8; i++){
const m = new THREE.Mesh(crGeo, crMat);
m.castShadow = true;
scene.add(m);
crystals.push({mesh:m, baseY:0});
}
function respawnCrystal(c){
const i = islands[Math.floor(Math.random()*islands.length)];
const a = Math.random()*6.28, r = Math.random()*i.r*.7;
c.mesh.position.set(i.x + Math.cos(a)*r, i.y + 1.2, i.z + Math.sin(a)*r);
c.baseY = c.mesh.position.y;
c.mesh.visible = true;
}
const turrets = [];
const tMat = new THREE.MeshLambertMaterial({color:0xff8899, flatShading:true});
[1, 2, 4].forEach(idx => {
const i = islands[idx];
const m = new THREE.Mesh(new THREE.CylinderGeometry(.65, .85, 1.6, 6), tMat);
m.position.set(i.x, i.y + .8, i.z);
m.castShadow = true;
scene.add(m);
turrets.push({pos: m.position.clone(), timer: Math.random()*2});
});
const projs = [];
const prGeo = new THREE.SphereGeometry(.28, 6, 6);
const prMat = new THREE.MeshBasicMaterial({color:0xffaa22});
// --- Input & Camera ---
const keys = {};
addEventListener('keydown', e => keys[e.code] = true);
addEventListener('keyup', e => keys[e.code] = false);
let camAz = 0, camPol = Math.PI/3.2, camDist = 13, dragging = false, lastX = 0, lastY = 0;
const cv = renderer.domElement;
cv.addEventListener('mousedown', e => { dragging=true; lastX=e.clientX; lastY=e.clientY; });
addEventListener('mouseup', () => dragging=false);
addEventListener('mousemove', e => {
if(dragging){
camAz -= (e.clientX - lastX) * .008;
camPol = Math.max(.15, Math.min(Math.PI/2 - .1, camPol - (e.clientY - lastY) * .008));
lastX=e.clientX; lastY=e.clientY;
}
});
cv.addEventListener('touchstart', e => { dragging=true; lastX=e.touches[0].clientX; lastY=e.touches[0].clientY; }, {passive:false});
addEventListener('touchend', () => dragging=false);
addEventListener('touchmove', e => {
if(dragging){
camAz -= (e.touches[0].clientX - lastX) * .008;
camPol = Math.max(.15, Math.min(Math.PI/2 - .1, camPol - (e.touches[0].clientY - lastY) * .008));
lastX=e.touches[0].clientX; lastY=e.touches[0].clientY;
}
}, {passive:false});
addEventListener('resize', () => {
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
// --- Game State ---
let state = 'START', hp = 3, score = 0, time = 0, vy = 0, grounded = true, invuln = 0;
const elHp = document.getElementById('hp'), elSc = document.getElementById('sc'), elFlash = document.getElementById('flash');
window.startGame = () => {
initAudio();
state = 'PLAY'; hp = 3; score = 0; time = 0; vy = 0; invuln = 0;
wizard.position.set(0, 1, 0); wizard.rotation.y = 0; wizard.visible = true;
camAz = 0; camPol = Math.PI/3.2;
projs.forEach(p => scene.remove(p.mesh)); projs.length = 0;
crystals.forEach(respawnCrystal);
updateHUD();
document.getElementById('start').style.display = 'none';
document.getElementById('go').style.display = 'none';
};
document.getElementById('btnStart').addEventListener('click', startGame);
document.getElementById('btnRestart').addEventListener('click', startGame);
function updateHUD(){
elHp.innerText = '❤️'.repeat(Math.max(0, hp));
elSc.innerText = score;
}
function takeDamage(){
if(invuln > 0 || state !== 'PLAY') return;
hp--; invuln = 1.5; updateHUD(); sfx('hit');
elFlash.style.opacity = .45;
setTimeout(() => elFlash.style.opacity = 0, 150);
if(hp <= 0){
state = 'OVER';
document.getElementById('fs').innerText = score;
document.getElementById('go').style.display = 'flex';
}
}
// --- Main Loop ---
const clock = new THREE.Clock();
const skyDay = new THREE.Color(0xaaddff), skyNight = new THREE.Color(0x151133);
const tmpV = new THREE.Vector3();
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), .05);
// Day/Night Cycle
time += dt * .15;
const dayF = Math.max(0, Math.sin(time)*.5 + .5);
sun.position.set(Math.cos(time)*45, Math.max(8, Math.sin(time)*45), 25);
sun.intensity = dayF * 1.6;
amb.intensity = .15 + dayF * .45;
scene.background = skyNight.clone().lerp(skyDay, dayF);
scene.fog.color.copy(scene.background);
// Particles drift
dust.rotation.y += dt * .04;
const dpos = dust.geometry.attributes.position.array;
for(let i=1; i<pCount*3; i+=3){
dpos[i] += dt * .6;
if(dpos[i] > 27) dpos[i] = -8;
}
dust.geometry.attributes.position.needsUpdate = true;
if(state === 'PLAY'){
// Movement Input
let fwd = 0, rgt = 0;
if(keys['KeyW'] || keys['ArrowUp']) fwd = 1;
if(keys['KeyS'] || keys['ArrowDown']) fwd = -1;
if(keys['KeyA'] || keys['ArrowLeft']) rgt = -1;
if(keys['KeyD'] || keys['ArrowRight']) rgt = 1;
const len = Math.hypot(fwd, rgt);
if(len > 0){ fwd /= len; rgt /= len; }
const dx = (-Math.sin(camAz)*fwd + Math.cos(camAz)*rgt) * 9.5 * dt;
const dz = (-Math.cos(camAz)*fwd - Math.sin(camAz)*rgt) * 9.5 * dt;
wizard.position.x += dx;
wizard.position.z += dz;
if(len > 0){
const target = Math.atan2(dx, dz);
let diff = target - wizard.rotation.y;
while(diff > Math.PI) diff -= Math.PI*2;
while(diff < -Math.PI) diff += Math.PI*2;
wizard.rotation.y += diff * 12 * dt;
robe.rotation.x = Math.sin(time*15)*.08; // walk bob
} else {
robe.rotation.x *= .9;
}
// Jump & Gravity
if(keys['Space'] && grounded){ vy = 11.5; grounded = false; sfx('jump'); }
vy -= 22 * dt;
wizard.position.y += vy * dt;
// Island Collision
let targetY = -100;
for(const i of islands){
if(Math.hypot(wizard.position.x - i.x, wizard.position.z - i.z) < i.r - .25 && i.y > targetY){
targetY = i.y;
}
}
if(wizard.position.y <= targetY && vy <= 0){
wizard.position.y = targetY; vy = 0; grounded = true;
} else {
grounded = false;
}
// Fall Recovery
if(wizard.position.y < -18){
wizard.position.set(0, 2, 0); vy = 0; takeDamage();
}
// Invulnerability Blink
if(invuln > 0){
invuln -= dt;
wizard.visible = Math.floor(invuln * 16) % 2 === 0;
} else wizard.visible = true;
// Crystals
tmpV.copy(wizard.position).add(new THREE.Vector3(0, 1, 0));
crystals.forEach(c => {
c.mesh.rotation.y += dt * 2.5;
c.mesh.rotation.x += dt * 1.2;
c.mesh.position.y = c.baseY + Math.sin(time * 4 + c.mesh.position.x) * .22;
if(c.mesh.position.distanceTo(tmpV) < 1.3){
score += 100; updateHUD(); sfx('collect'); respawnCrystal(c);
}
});
// Turrets & Projectiles
turrets.forEach(t => {
t.timer -= dt;
if(t.timer <= 0){
t.timer = 1.4 + Math.random();
if(t.pos.distanceTo(wizard.position) < 28){
const m = new THREE.Mesh(prGeo, prMat);
m.position.copy(t.pos).add(new THREE.Vector3(0, 1, 0));
scene.add(m);
const dir = tmpV.clone().sub(m.position).normalize();
projs.push({mesh: m, vel: dir.multiplyScalar(13)});
sfx('shoot');
}
}
});
for(let i = projs.length - 1; i >= 0; i--){
const p = projs[i];
p.mesh.position.addScaledVector(p.vel, dt);
if(p.mesh.position.distanceTo(tmpV) < .85){
takeDamage(); scene.remove(p.mesh); projs.splice(i, 1); continue;
}
if(p.mesh.position.y < -20){ scene.remove(p.mesh); projs.splice(i, 1); }
}
}
// Dynamic Camera Follow
const camTarget = new THREE.Vector3(
wizard.position.x + camDist * Math.sin(camPol) * Math.sin(camAz),
wizard.position.y + camDist * Math.cos(camPol),
wizard.position.z + camDist * Math.sin(camPol) * Math.cos(camAz)
);
camera.position.lerp(camTarget, 8 * dt);
camera.lookAt(wizard.position.x, wizard.position.y + 1.1, wizard.position.z);
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
Hy4 details and scope
hy4 model with a 1,024k-token context window, up to 960k input tokens, and up to 64k output tokens. Tencent says the official Hy4 release improves on Hy4 Preview, especially for agentic and long-horizon productivity tasks, and adds visual understanding. The same guide documents OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages compatibility for TokenHub. This creative-coding run used the HY AI Studio interface; it was not a TokenHub API, latency, cost, token-usage, or vision test.hy4. It is separate from the earlier Hy4 Preview release and evaluation article. The three generated pages are useful examples of output structure and repair behavior, but one session cannot establish reliability, code quality across repeated runs, browser compatibility, or production readiness.Read Next
Crouton Alpha HTML Coding Check: Glass Crane, Globe & Wizard Game
A short creative-coding run with the Crouton Alpha label in OpenRouter: a glass-crane SVG, a procedural Three.js globe, and a wizard-island game, with original HTML and clearly scoped observations.
MiniMax M3.1 Flash: Four HTML Coding Benchmarks on Free OpenCode
MiniMax M3.1 Flash tested through free OpenCode: original crane, globe, wizard-game and task-board HTML, browser screenshots, timings, downloadable evidence, 16 passing board checks and a reproduced game physics failure.
Claude Sonnet 5.5 Browser Coding Check: Glass Crane, Globe & Wizard Game
A concise hands-on look at three Claude Sonnet 5.5 Medium HTML artifacts: a glass-and-origami SVG, an interactive procedural globe, and a floating-island wizard game.