← Back to Articles Directory
AI Models • November 4, 2026 • 3 min read

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

Editorial illustration of a glass sphere containing an origami crane, a glowing procedural globe, and a wizard exploring pastel floating islands.
AI-generated editorial illustration

Three standalone HTML outputs from Hy4 New, including correction turns, full source, and what remains untested.

Co-Founder & Lead Programmer of AcceleratedLogic AI

On November 4, 2026, I used the Hy4 New model option in Tencent HY AI Studio to request three standalone browser projects: an SVG glass sphere with an origami crane, an interactive Three.js globe, and a third-person wizard game. The Studio details panel identified the API name as hy4. This is a hands-on artifact report, not a controlled benchmark or a model ranking.

What happened in the session

The first crane reply was design notes and separate SVG fragments, so I asked Hy4 to consolidate them into one complete HTML document. The first globe reply was a title, CSS fragment, and speed slider without the globe; I asked again for the complete source. Both follow-ups produced full documents. The wizard game arrived as a complete HTML document in one response.
I copied the final code blocks into 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

The first standalone page uses inline SVG gradients, layered highlights, shadows, and refracted-looking shapes to draw a glass sphere around a red folded-paper crane. It is a stylized vector illustration, not a physically simulated or interactive 3D scene. It has no external script dependency.
The final source followed a correction prompt after the initial response was split across fragments. The document itself was not edited after it was copied from the model response.
<!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

The globe page imports Three.js and OrbitControls through an import map from unpkg. It draws a procedurally generated canvas texture, adds an atmospheric glow shader and orbiting light particles, and wires pointer rotation, zoom, and the rotation-speed slider in the source. Chromium rendered its WebGL canvas and accepted drag, wheel and slider input. The slider retained the selected value. The page needs network access for its CDN imports.
<!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

The game source builds a pastel island scene with a low-poly wizard, crystals, elemental turrets, projectiles, a health-and-score HUD, day/night lighting, particles, keyboard movement, jumping, pointer camera controls, Web Audio sound effects, and restart/game-over UI. A source review found an important scope gap: collected crystals respawn and increase the score, but there is no crystal completion target or win state. That makes the listed objective an endless scoring loop rather than a completed collection quest. In Chromium, I started the game, sent jump and forward inputs, reached Game Over and restarted. Restart restored three hearts and zero score. Crystal collection, audio playback and performance still need dedicated checks.
<!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 &nbsp;|&nbsp; <b>SPACE</b> to Jump &nbsp;|&nbsp; <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

Tencent's current Hy model API guide lists the official 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.
This post covers the full Hy4 option shown as Hy4 New in the Studio and mapped to API name 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.