Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created April 22, 2025 08:36
Show Gist options
  • Select an option

  • Save shricodev/0cfd4fe62c3bf4cc6844505a9de90e4a to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/0cfd4fe62c3bf4cc6844505a9de90e4a to your computer and use it in GitHub Desktop.
OpenAI o4-Mini - SimCity Simulation - Part 1
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Mini SimCity Demo</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: sans-serif;
}
#ui {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.85);
padding: 10px;
border-radius: 4px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
#ui label {
display: block;
margin-bottom: 5px;
cursor: pointer;
}
#population {
margin-top: 8px;
font-weight: bold;
}
#info {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
padding: 5px 10px;
border-radius: 4px;
}
</style>
</head>
<body>
<div id="ui">
<strong>Select Building:</strong><br />
<label
><input type="radio" name="btype" value="res" checked />
Residential</label
>
<label><input type="radio" name="btype" value="com" /> Commercial</label>
<label><input type="radio" name="btype" value="ind" /> Industrial</label>
<div id="population">Population: 0</div>
</div>
<div id="info">Click on a green block to place your building</div>
<script src="https://unpkg.com/three@0.154.0/build/three.min.js"></script>
<script>
// === SETTINGS ===
const GRID_SIZE = 16; // # of blocks per side
const CELL_SIZE = 4; // world units per block
const ROAD_EVERY = 4; // every Nth line is a road
const BUILDING_COLORS = {
res: 0x66ccff,
com: 0xffcc33,
ind: 0x999999,
};
const POP_PER_RES = 10; // population per residential
// === GLOBALS ===
let scene, camera, renderer, raycaster, mouse;
let gridData = []; // keep track of occupancy
let currentType = "res";
let population = 0;
let cars = []; // car objects to animate
init();
animate();
function init() {
// scene + camera
scene = new THREE.Scene();
scene.background = new THREE.Color(0xbfd1e5);
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
1,
2000,
);
camera.position.set(
(GRID_SIZE * CELL_SIZE) / 2,
(GRID_SIZE * CELL_SIZE) / 1.5,
(GRID_SIZE * CELL_SIZE) / 2,
);
camera.lookAt(
(GRID_SIZE * CELL_SIZE) / 2,
0,
(GRID_SIZE * CELL_SIZE) / 2,
);
// renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// lighting
const hemi = new THREE.HemisphereLight(0xffffff, 0x444444);
hemi.position.set(0, 200, 0);
scene.add(hemi);
const dir = new THREE.DirectionalLight(0xffffff);
dir.position.set(-100, 100, -100);
scene.add(dir);
// raycaster
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// ground + grid + roads
buildGround();
// event listeners
window.addEventListener("resize", onWindowResize);
renderer.domElement.addEventListener("pointerdown", onClick);
// UI radio buttons
document.querySelectorAll("[name=btype]").forEach((input) => {
input.addEventListener(
"change",
(e) => (currentType = e.target.value),
);
});
// create some cars
setupCars();
}
function buildGround() {
// flat ground
const groundMat = new THREE.MeshPhongMaterial({ color: 0xdddddd });
const groundGeo = new THREE.PlaneGeometry(
GRID_SIZE * CELL_SIZE,
GRID_SIZE * CELL_SIZE,
);
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.set(
(GRID_SIZE * CELL_SIZE) / 2,
0,
(GRID_SIZE * CELL_SIZE) / 2,
);
scene.add(ground);
// build the blocks + roads
for (let i = 0; i < GRID_SIZE; i++) {
gridData[i] = [];
for (let j = 0; j < GRID_SIZE; j++) {
gridData[i][j] = null; // empty
const isRoadRow = i % ROAD_EVERY === ROAD_EVERY - 1;
const isRoadCol = j % ROAD_EVERY === ROAD_EVERY - 1;
const cellSize = CELL_SIZE;
const half = cellSize / 2;
const x = i * cellSize + half;
const z = j * cellSize + half;
// draw a flat tile
const tileGeo = new THREE.PlaneGeometry(cellSize, cellSize);
const tileMat = new THREE.MeshPhongMaterial({
color: isRoadRow || isRoadCol ? 0x444444 : 0x88aa88,
});
const tile = new THREE.Mesh(tileGeo, tileMat);
tile.rotation.x = -Math.PI / 2;
tile.position.set(x, 0.01, z);
scene.add(tile);
// if it's a road intersection, optionally draw crosswalks etc.
}
}
}
function onClick(ev) {
ev.preventDefault();
// convert to normalized device coords
mouse.x = (ev.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// intersect with all tiles (we gave them plane geometry)
const hits = raycaster.intersectObjects(scene.children, true);
if (!hits.length) return;
const hit = hits[0];
// we only want clicks on floor tiles (y ~ 0.01)
if (Math.abs(hit.point.y) > 0.1) return;
const px = hit.point.x;
const pz = hit.point.z;
// figure grid coords
const i = Math.floor(px / CELL_SIZE);
const j = Math.floor(pz / CELL_SIZE);
if (i < 0 || j < 0 || i >= GRID_SIZE || j >= GRID_SIZE) return;
// road cells are not buildable
if (
i % ROAD_EVERY == ROAD_EVERY - 1 ||
j % ROAD_EVERY == ROAD_EVERY - 1
) {
return;
}
// empty?
if (gridData[i][j] !== null) return;
// place building
const bcolor = BUILDING_COLORS[currentType];
const bgeo = new THREE.BoxGeometry(
CELL_SIZE * 0.8,
CELL_SIZE * 0.8,
CELL_SIZE * 0.8,
);
const bmat = new THREE.MeshPhongMaterial({ color: bcolor });
const building = new THREE.Mesh(bgeo, bmat);
building.position.set(
i * CELL_SIZE + CELL_SIZE / 2,
CELL_SIZE * 0.4,
j * CELL_SIZE + CELL_SIZE / 2,
);
scene.add(building);
gridData[i][j] = { type: currentType, mesh: building };
// update population
if (currentType === "res") {
population += POP_PER_RES;
document.getElementById("population").innerText =
"Population: " + population;
}
}
function setupCars() {
// Let's make a simple rectangular loop just inside the outer roads
const startX = CELL_SIZE / 2;
const startZ = CELL_SIZE / 2;
const endX = (GRID_SIZE - 1) * CELL_SIZE + CELL_SIZE / 2;
const endZ = (GRID_SIZE - 1) * CELL_SIZE + CELL_SIZE / 2;
const path = [
new THREE.Vector3(startX, 0.2, startZ),
new THREE.Vector3(endX, 0.2, startZ),
new THREE.Vector3(endX, 0.2, endZ),
new THREE.Vector3(startX, 0.2, endZ),
];
// make 10 cars
for (let c = 0; c < 10; c++) {
const cgeo = new THREE.BoxGeometry(
CELL_SIZE * 0.3,
CELL_SIZE * 0.2,
CELL_SIZE * 0.15,
);
const cmat = new THREE.MeshPhongMaterial({ color: 0xff0000 });
const car = new THREE.Mesh(cgeo, cmat);
car.userData = { path, seg: 0, t: c / 10 }; // stagger starts
scene.add(car);
cars.push(car);
}
}
function animate() {
requestAnimationFrame(animate);
const dt = 0.005;
// move cars
cars.forEach((car) => {
const pd = car.userData;
pd.t += dt;
if (pd.t > 1) {
pd.t = 0;
pd.seg = (pd.seg + 1) % pd.path.length;
}
// interpolate between seg and seg+1
const a = pd.path[pd.seg];
const b = pd.path[(pd.seg + 1) % pd.path.length];
car.position.lerpVectors(a, b, pd.t);
// rotate to face direction
const dir = new THREE.Vector3().subVectors(b, a).normalize();
const ang = Math.atan2(dir.x, dir.z);
car.rotation.y = ang;
});
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment