GPT-5.6 Sol × doom
8.7DDA raycaster + textures + door + minimap + Z-buffer — the signature challenge
correctness 8.7quality 9.1documentation 8.493970ms
$ cat doom.prompt — what the model was asked
Implement a first-person 3D raycasting engine in a single self-contained HTML file with no external libraries, no external images, and no CDN scripts. This is the hardest challenge in the benchmark. Partial credit is given per requirement met. ## Rendering - DDA (Digital Differential Analysis) raycasting — not a simplified ray-box approximation - Fish-eye correction applied to all wall distances - **Procedurally generated wall textures** using canvas math only (no image files, no data URIs): at least 3 distinct texture patterns (e.g. checkerboard, brick, stripe) assigned to different wall types in the map - Perspective-correct texture mapping onto wall columns - Distance-based shading: walls darken smoothly as they recede (multiply shade by 1/distance, clamped) - Ceiling rendered as a flat dark color; floor as a slightly lighter flat color - Target: 60fps at 640×480 internal resolution scaled to fill the browser window ## Map - Hard-coded map of at least 16×16 cells encoded as a 2D array - Non-trivial layout: at least 3 distinct rooms connected by corridors, one dead end, one secret area - At least 3 wall types (mapped to the 3 texture patterns) - One door cell (wall type 4) that opens when the player is within 1.5 cells and presses E; opened doors become passable and render as open archways - One exit cell — reaching it displays a 'LEVEL COMPLETE — [MM:SS]' overlay - Player spawn position defined in the map; facing toward the first corridor ## Player - WASD movement with smooth speed (moveSpeed: 3 cells/sec, rotSpeed: 2 rad/sec) - Mouse-look for horizontal rotation using the Pointer Lock API (click canvas to lock) - Collision detection: AABB against all solid walls (player radius: 0.2 cells) - Field of view: 66 degrees ## Z-buffer - Maintain a per-column Z-buffer (array of wall distances) for correct depth ordering - Use it to clip any future sprite/overlay rendering to avoid drawing behind walls ## HUD - Minimap in top-left corner: draw map cells as colored squares, player as a dot with a direction arrow; scale: 6px per cell - FPS counter (rolling average over last 30 frames) in top-right corner - 'Press E to open door' hint rendered in the center-bottom when a door is within 1.5 cells ## Code quality - Single HTML file, all JS inline in <script> tags, all CSS inline in <style> - requestAnimationFrame game loop with delta-time movement - Organized into clearly named functions: initMap(), castRay(), drawWallColumn(), drawHUD(), drawMinimap(), gameLoop(), handleInput() - No god-object — game state in a plain JS object, renderer functions take state as argument Scoring: the judge scores each major section (rendering, map, player, HUD, code) independently. A beautiful but mathematically wrong perspective loses on correctness. A correct raycaster with unreadable code loses on quality. Both must be right to score above 8.
run 1 of 1
8.7correctness 8.7quality 9.1documentation 8.493970ms
# judge: Strong real DDA raycasting, corrected textured walls, shading, doors, HUD, and delta-time controls, but the spawn faces east rather than toward its first southbound corridor, collision is circle-versus-cell rather than the requested AABB, and the claimed secret area is not meaningfully secret.
$ cat codex-cli/gpt-5.6-sol.doom.r1.response
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>DDA Raycaster</title>
<style>
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#000}
canvas{display:block;width:100vw;height:100vh;image-rendering:pixelated;cursor:crosshair}
</style>
</head>
<body>
<canvas id="game" width="640" height="480"></canvas>
<script>
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d", { alpha:false });
ctx.imageSmoothingEnabled = false;
const state = {
width:640,
height:480,
map:[],
mapWidth:0,
mapHeight:0,
player:{x:0,y:0,angle:0,radius:0.2},
keys:Object.create(null),
textures:{},
zBuffer:new Float32Array(640),
openDoors:new Set(),
nearbyDoor:null,
exit:null,
complete:false,
startTime:performance.now(),
completionTime:0,
lastTime:performance.now(),
fpsSamples:[],
fps:0,
moveSpeed:3,
rotSpeed:2,
fov:66*Math.PI/180
};
function initMap(s) {
/*
Upper-left and upper-right chambers are joined by a corridor and door.
The central and lower chambers connect through winding corridors.
The lower-left striped chamber is the secret area; the southeast spur
beyond the exit corridor includes a dead end.
*/
const encoded = [
"1111111111111111",
"1800001000000001",
"1000001000000001",
"1000002000000001",
"1110111111041101",
"1000100000000101",
"1000101111100101",
"1000001000100001",
"1333301000103331",
"1000000000100001",
"1011111110101101",
"1000000010000101",
"1000333011110101",
"1000300000000901",
"1000000010000001",
"1111111111111111"
];
s.map = encoded.map(row => Array.from(row, Number));
s.mapHeight = s.map.length;
s.mapWidth = s.map[0].length;
for (let y=0; y<s.mapHeight; y++) {
for (let x=0; x<s.mapWidth; x++) {
if (s.map[y][x] === 8) {
s.player.x=x+0.5;
s.player.y=y+0.5;
s.player.angle=0;
s.map[y][x]=0;
} else if (s.map[y][x] === 9) {
s.exit={x,y};
s.map[y][x]=0;
}
}
}
}
function makeTexture(draw) {
const c=document.createElement("canvas");
c.width=c.height=64;
const g=c.getContext("2d");
draw(g,64);
return c;
}
function createTextures(s) {
s.textures[1]=makeTexture((g,n)=>{
for(let y=0;y<n;y+=8) for(let x=0;x<n;x+=8){
g.fillStyle=((x+y)/8)%2===0?"#9b9b9b":"#52575b";
g.fillRect(x,y,8,8);
}
g.strokeStyle="#303438";
for(let i=0;i<=n;i+=8){
g.beginPath();g.moveTo(i,0);g.lineTo(i,n);g.stroke();
g.beginPath();g.moveTo(0,i);g.lineTo(n,i);g.stroke();
}
});
s.textures[2]=makeTexture((g,n)=>{
g.fillStyle="#702d24";g.fillRect(0,0,n,n);
for(let y=0;y<n;y+=12){
const offset=(y/12)%2 ? -8 : 0;
for(let x=offset;x<n;x+=16){
g.fillStyle=(x+y)%24===0?"#a34b38":"#873727";
g.fillRect(x+1,y+1,14,10);
}
}
g.fillStyle="#351d19";
for(let y=0;y<n;y+=12)g.fillRect(0,y,64,2);
for(let y=0;y<n;y+=12){
const offset=(y/12)%2 ? 8 : 0;
for(let x=offset;x<n;x+=16)g.fillRect(x,y,2,12);
}
});
s.textures[3]=makeTexture((g,n)=>{
g.fillStyle="#213842";g.fillRect(0,0,n,n);
for(let x=0;x<n;x+=8){
g.fillStyle=(x/8)%2?"#397788":"#285766";
g.fillRect(x,0,6,n);
g.fillStyle="#75a7aa";g.fillRect(x,0,1,n);
}
for(let y=3;y<n;y+=16){
g.fillStyle="rgba(255,255,255,.12)";
g.fillRect(0,y,n,2);
}
});
s.textures[4]=makeTexture((g,n)=>{
g.fillStyle="#5b351b";g.fillRect(0,0,n,n);
for(let x=0;x<n;x+=8){
g.fillStyle=x%16===0?"#754822":"#4a2a16";
g.fillRect(x+1,0,6,n);
g.fillStyle="#2a170d";g.fillRect(x,0,1,n);
}
g.fillStyle="#c99a38";
g.beginPath();g.arc(51,32,3,0,Math.PI*2);g.fill();
});
}
function cellAt(s,x,y) {
if(x<0||y<0||x>=s.mapWidth||y>=s.mapHeight)return 1;
return s.map[y][x];
}
function isSolid(s,x,y) {
const v=cellAt(s,x,y);
return v>=1 && v<=4;
}
function canOccupy(s,x,y) {
const r=s.player.radius;
const minX=Math.floor(x-r), maxX=Math.floor(x+r);
const minY=Math.floor(y-r), maxY=Math.floor(y+r);
for(let my=minY;my<=maxY;my++) for(let mx=minX;mx<=maxX;mx++){
if(!isSolid(s,mx,my))continue;
const nearestX=Math.max(mx,Math.min(x,mx+1));
const nearestY=Math.max(my,Math.min(y,my+1));
const dx=x-nearestX,dy=y-nearestY;
if(dx*dx+dy*dy<r*r)return false;
}
return true;
}
function nearestDoor(s) {
let best=null,bestD=1.5;
for(let y=0;y<s.mapHeight;y++) for(let x=0;x<s.mapWidth;x++){
if(s.map[y][x]!==4)continue;
const d=Math.hypot(s.player.x-(x+.5),s.player.y-(y+.5));
if(d<=bestD){bestD=d;best={x,y};}
}
return best;
}
function openNearbyDoor(s) {
const door=nearestDoor(s);
if(!door)return;
s.map[door.y][door.x]=5;
s.openDoors.add(door.x+","+door.y);
}
function castRay(s,rayAngle) {
const rayX=Math.cos(rayAngle),rayY=Math.sin(rayAngle);
let mapX=Math.floor(s.player.x),mapY=Math.floor(s.player.y);
const deltaX=Math.abs(1/(rayX||1e-12));
const deltaY=Math.abs(1/(rayY||1e-12));
const stepX=rayX<0?-1:1,stepY=rayY<0?-1:1;
let sideX=rayX<0?(s.player.x-mapX)*deltaX:(mapX+1-s.player.x)*deltaX;
let sideY=rayY<0?(s.player.y-mapY)*deltaY:(mapY+1-s.player.y)*deltaY;
let side=0,type=1,arch=null;
for(let i=0;i<64;i++){
let travel;
if(sideX<sideY){
travel=sideX;sideX+=deltaX;mapX+=stepX;side=0;
}else{
travel=sideY;sideY+=deltaY;mapY+=stepY;side=1;
}
type=cellAt(s,mapX,mapY);
if(type===5 && !arch) arch={distance:travel,side,mapX,mapY};
if(type>=1&&type<=4)break;
}
const rawDistance=side===0 ? sideX-deltaX : sideY-deltaY;
const correctedDistance=Math.max(.0001,
rawDistance*Math.cos(rayAngle-s.player.angle));
let hit=side===0
? s.player.y+rawDistance*rayY
: s.player.x+rawDistance*rayX;
let textureX=hit-Math.floor(hit);
if((side===0&&rayX>0)||(side===1&&rayY<0))textureX=1-textureX;
if(arch){
arch.correctedDistance=Math.max(.0001,
arch.distance*Math.cos(rayAngle-s.player.angle));
}
return {distance:correctedDistance,rawDistance,side,type,textureX,arch};
}
function drawWallColumn(s,x,ray) {
const h=s.height;
const wallHeight=Math.min(h*4,h/ray.distance);
const top=Math.floor(h/2-wallHeight/2);
const texture=s.textures[ray.type]||s.textures[1];
const tx=Math.max(0,Math.min(63,Math.floor(ray.textureX*64)));
ctx.drawImage(texture,tx,0,1,64,x,top,1,wallHeight);
let brightness=Math.max(.18,Math.min(1,1.25/ray.distance));
if(ray.side===1)brightness*=.78;
ctx.fillStyle=`rgba(0,0,0,${1-brightness})`;
ctx.fillRect(x,top,1,wallHeight);
/* A passable opened door is drawn as an overhead arch lintel. */
if(ray.arch && ray.arch.correctedDistance<ray.distance){
const d=ray.arch.correctedDistance;
const fullHeight=Math.min(h*4,h/d);
const archTop=h/2-fullHeight/2;
const lintelHeight=Math.max(2,fullHeight*.16);
if(d<s.zBuffer[x]){
ctx.drawImage(s.textures[4],0,0,64,10,x,archTop,1,lintelHeight);
const shade=Math.max(.2,Math.min(1,1.2/d));
ctx.fillStyle=`rgba(0,0,0,${1-shade})`;
ctx.fillRect(x,archTop,1,lintelHeight);
}
}
}
function renderWorld(s) {
ctx.fillStyle="#141820";
ctx.fillRect(0,0,s.width,s.height/2);
ctx.fillStyle="#292a28";
ctx.fillRect(0,s.height/2,s.width,s.height/2);
const halfFov=s.fov/2;
const tanHalf=Math.tan(halfFov);
for(let x=0;x<s.width;x++){
const cameraX=2*(x+.5)/s.width-1;
const rayAngle=s.player.angle+Math.atan(cameraX*tanHalf);
const ray=castRay(s,rayAngle);
s.zBuffer[x]=ray.distance;
drawWallColumn(s,x,ray);
}
}
/* Future sprites should project to a screen column, then draw only when
spriteDistance < state.zBuffer[column], preserving correct wall occlusion. */
function drawMinimap(s) {
const scale=6,ox=10,oy=10;
ctx.fillStyle="rgba(0,0,0,.65)";
ctx.fillRect(ox-4,oy-4,s.mapWidth*scale+8,s.mapHeight*scale+8);
const colors=["#20252a","#8e9296","#934434","#357487","#a76a2c","#4c6a45"];
for(let y=0;y<s.mapHeight;y++) for(let x=0;x<s.mapWidth;x++){
const v=s.map[y][x];
ctx.fillStyle=v===0?"#20252a":colors[v]||"#20252a";
ctx.fillRect(ox+x*scale,oy+y*scale,scale-1,scale-1);
}
if(s.exit){
ctx.fillStyle="#4cff6b";
ctx.fillRect(ox+s.exit.x*scale+1,oy+s.exit.y*scale+1,scale-2,scale-2);
}
const px=ox+s.player.x*scale,py=oy+s.player.y*scale;
ctx.fillStyle="#ffe45c";
ctx.beginPath();ctx.arc(px,py,2.5,0,Math.PI*2);ctx.fill();
ctx.strokeStyle="#ffe45c";ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(px,py);
ctx.lineTo(px+Math.cos(s.player.angle)*9,py+Math.sin(s.player.angle)*9);
ctx.stroke();
}
function formatTime(ms) {
const total=Math.floor(ms/1000);
return String(Math.floor(total/60)).padStart(2,"0")+":"+
String(total%60).padStart(2,"0");
}
function drawHUD(s) {
drawMinimap(s);
ctx.font="bold 14px monospace";
ctx.textAlign="right";
ctx.fillStyle="rgba(0,0,0,.65)";
ctx.fillRect(s.width-100,10,90,24);
ctx.fillStyle="#fff";
ctx.fillText(`${s.fps.toFixed(0)} FPS`,s.width-18,27);
ctx.textAlign="center";
if(s.nearbyDoor&&!s.complete){
const text="Press E to open door";
ctx.font="bold 16px sans-serif";
ctx.fillStyle="rgba(0,0,0,.7)";
ctx.fillRect(s.width/2-105,s.height-55,210,30);
ctx.fillStyle="#ffe8a3";
ctx.fillText(text,s.width/2,s.height-34);
}
if(document.pointerLockElement!==canvas&&!s.complete){
ctx.font="14px sans-serif";
ctx.fillStyle="rgba(0,0,0,.65)";
ctx.fillRect(s.width/2-105,16,210,28);
ctx.fillStyle="#fff";
ctx.fillText("Click to enable mouse-look",s.width/2,35);
}
if(s.complete){
ctx.fillStyle="rgba(0,0,0,.72)";
ctx.fillRect(0,0,s.width,s.height);
ctx.font="bold 32px sans-serif";
ctx.fillStyle="#7dff8d";
ctx.fillText("LEVEL COMPLETE — "+formatTime(s.completionTime),
s.width/2,s.height/2);
}
}
function handleInput(s,dt) {
if(s.complete)return;
let forward=0,strafe=0;
if(s.keys.KeyW)forward++;
if(s.keys.KeyS)forward--;
if(s.keys.KeyD)strafe++;
if(s.keys.KeyA)strafe--;
if(s.keys.ArrowLeft)s.player.angle-=s.rotSpeed*dt;
if(s.keys.ArrowRight)s.player.angle+=s.rotSpeed*dt;
const length=Math.hypot(forward,strafe)||1;
forward/=length;strafe/=length;
const ca=Math.cos(s.player.angle),sa=Math.sin(s.player.angle);
const dx=(ca*forward-sa*strafe)*s.moveSpeed*dt;
const dy=(sa*forward+ca*strafe)*s.moveSpeed*dt;
if(canOccupy(s,s.player.x+dx,s.player.y))s.player.x+=dx;
if(canOccupy(s,s.player.x,s.player.y+dy))s.player.y+=dy;
s.player.angle=(s.player.angle+Math.PI*2)%(Math.PI*2);
s.nearbyDoor=nearestDoor(s);
if(s.exit &&
Math.hypot(s.player.x-(s.exit.x+.5),s.player.y-(s.exit.y+.5))<.42){
s.complete=true;
s.completionTime=performance.now()-s.startTime;
}
}
function gameLoop(now) {
const dt=Math.min(.05,(now-state.lastTime)/1000);
state.lastTime=now;
handleInput(state,dt);
renderWorld(state);
drawHUD(state);
const instantaneous=dt>0?1/dt:60;
state.fpsSamples.push(instantaneous);
if(state.fpsSamples.length>30)state.fpsSamples.shift();
state.fps=state.fpsSamples.reduce((a,b)=>a+b,0)/state.fpsSamples.length;
requestAnimationFrame(gameLoop);
}
addEventListener("keydown",e=>{
state.keys[e.code]=true;
if(e.code==="KeyE"&&!e.repeat)openNearbyDoor(state);
if(["KeyW","KeyA","KeyS","KeyD","ArrowLeft","ArrowRight","Space"].includes(e.code))
e.preventDefault();
});
addEventListener("keyup",e=>state.keys[e.code]=false);
canvas.addEventListener("click",()=>{
if(!state.complete)canvas.requestPointerLock();
});
addEventListener("mousemove",e=>{
if(document.pointerLockElement===canvas&&!state.complete)
state.player.angle+=e.movementX*.0025;
});
initMap(state);
createTextures(state);
requestAnimationFrame(gameLoop);
</script>
</body>
</html>