Skip to content

Instantly share code, notes, and snippets.

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

  • Save shricodev/6116661b71c027d990ad8e1b09495d64 to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/6116661b71c027d990ad8e1b09495d64 to your computer and use it in GitHub Desktop.
OpenAI o4-Mini - SimCity Simulation - Part 2
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Mini SimCity Demo – Economy & People</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: sans-serif;
}
#ui {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 12px;
border-radius: 4px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
#ui label {
display: block;
margin-bottom: 6px;
cursor: pointer;
}
#ui button {
margin-top: 8px;
padding: 4px 8px;
border: none;
background: #cc3333;
color: #fff;
border-radius: 3px;
cursor: pointer;
}
#ui button:hover {
background: #a02222;
}
#ui .stat {
margin-top: 8px;
font-weight: bold;
}
#info {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
padding: 6px 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
($100)</label
>
<label
><input type="radio" name="btype" value="com" /> Commercial
($200)</label
>
<label
><input type="radio" name="btype" value="ind" /> Industrial
($300)</label
>
<button id="resetBtn">🗑️ Reset City</button>
<div id="balance" class="stat">Money: $1000</div>
<div id="population" class="stat">Population: 0</div>
</div>
<div id="info">Click on a green block to place your building</div>
<!-- Three.js from a known stable release -->
<script src="https://unpkg.com/three@0.154.0/build/three.min.js"></script>
<script>
// ───────────────────────── SETTINGS ────────────────────────────
const GRID_SIZE = 16;
const CELL_SIZE = 4;
const ROAD_EVERY = 4;
const START_MONEY = 1000;
const BUILD_COST = { res: 100, com: 200, ind: 300 };
const BUILD_COLORS = { res: 0x66ccff, com: 0xffcc33, ind: 0x999999 };
const POP_PER_RES = 10; // initial pop on place
const GROWTH_PER_S = 1; // +1 pop/sec per res building
const NUM_CARS = 10;
const NUM_PEOPLE = 30; // roaming spheres
// ───────────────────────── GLOBALS ────────────────────────────
let scene, camera, renderer, raycaster, mouse;
let gridData = []; // occupancy map
let currentType = "res";
let money, population;
let cars = [];
let people = [];
// UI elements
const uiBalance = document.getElementById("balance");
const uiPop = document.getElementById("population");
const btnReset = document.getElementById("resetBtn");
init();
animate();
startPopulationGrowth();
// ─────────────────────── INITIALIZATION ────────────────────────
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);
// Lights
const hemi = new THREE.HemisphereLight(0xffffff, 0x444444);
hemi.position.set(0, 200, 0);
scene.add(hemi);
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
dir.position.set(-100, 100, -100);
scene.add(dir);
// Raycaster & mouse
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// Build terrain (ground + tiles)
buildGround();
// Initialize economy & population
resetStats();
// Event listeners
window.addEventListener("resize", onWindowResize);
renderer.domElement.addEventListener("pointerdown", onClick);
document
.querySelectorAll("[name=btype]")
.forEach((i) =>
i.addEventListener("change", (e) => (currentType = e.target.value)),
);
btnReset.addEventListener("click", resetGame);
// Cars and People
setupCars();
setupPeople();
}
// ──────────────────────── GROUND GRID ─────────────────────────
function buildGround() {
// ---------------- Ground plane (beneath tiles) ----------------
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(GRID_SIZE * CELL_SIZE, GRID_SIZE * CELL_SIZE),
new THREE.MeshPhongMaterial({ color: 0xdddddd }),
);
ground.rotation.x = -Math.PI / 2;
ground.position.set(
(GRID_SIZE * CELL_SIZE) / 2,
0,
(GRID_SIZE * CELL_SIZE) / 2,
);
scene.add(ground);
// ---------------- Tiles: roads vs blocks ---------------------
for (let i = 0; i < GRID_SIZE; i++) {
gridData[i] = [];
for (let j = 0; j < GRID_SIZE; j++) {
gridData[i][j] = null; // empty spot
const isRoadRow = i % ROAD_EVERY === ROAD_EVERY - 1;
const isRoadCol = j % ROAD_EVERY === ROAD_EVERY - 1;
const isRoad = isRoadRow || isRoadCol;
const tileMat = new THREE.MeshPhongMaterial({
color: isRoad ? 0x444444 : 0x88aa88,
side: THREE.DoubleSide,
});
const tile = new THREE.Mesh(
new THREE.PlaneGeometry(CELL_SIZE, CELL_SIZE),
tileMat,
);
tile.rotation.x = -Math.PI / 2;
tile.position.set(
i * CELL_SIZE + CELL_SIZE / 2,
0.01, // slight lift to avoid z-fight
j * CELL_SIZE + CELL_SIZE / 2,
);
scene.add(tile);
}
}
}
// ─────────────────── CLICK‐TO‐BUILD HANDLER ───────────────────
function onClick(ev) {
// raycast floor
mouse.x = (ev.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(scene.children, true);
if (!hits.length) return;
const p = hits[0].point;
// only accept near‐floor hits
if (Math.abs(p.y) > 0.1) return;
const i = Math.floor(p.x / CELL_SIZE),
j = Math.floor(p.z / CELL_SIZE);
// bounds check
if (i < 0 || j < 0 || i >= GRID_SIZE || j >= GRID_SIZE) return;
// roads are not buildable
if (
i % ROAD_EVERY == ROAD_EVERY - 1 ||
j % ROAD_EVERY == ROAD_EVERY - 1
) {
return;
}
// already occupied?
if (gridData[i][j]) return;
// cost check
const cost = BUILD_COST[currentType];
if (money < cost) {
alert("Not enough money!");
return;
}
// deduct money & update UI
money -= cost;
uiBalance.innerText = "Money: $" + money;
// instantiate building box
const mat = new THREE.MeshPhongMaterial({
color: BUILD_COLORS[currentType],
});
const geo = new THREE.BoxGeometry(
CELL_SIZE * 0.8,
CELL_SIZE * 0.8,
CELL_SIZE * 0.8,
);
const bld = new THREE.Mesh(geo, mat);
bld.position.set(
i * CELL_SIZE + CELL_SIZE / 2,
CELL_SIZE * 0.4,
j * CELL_SIZE + CELL_SIZE / 2,
);
scene.add(bld);
gridData[i][j] = { type: currentType, mesh: bld };
// immediate pop bump if residential
if (currentType === "res") {
population += POP_PER_RES;
uiPop.innerText = "Population: " + population;
}
}
// ───────────────────── RESET THE CITY ─────────────────────────
function resetGame() {
// remove all building meshes
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = 0; j < GRID_SIZE; j++) {
const cell = gridData[i][j];
if (cell) {
scene.remove(cell.mesh);
gridData[i][j] = null;
}
}
}
// remove people
people.forEach((p) => scene.remove(p));
people = [];
// reset stats & UI
resetStats();
// re‐spawn new people
setupPeople();
}
function resetStats() {
money = START_MONEY;
population = 0;
uiBalance.innerText = "Money: $" + money;
uiPop.innerText = "Population: " + population;
}
// ────────────────── POPULATION GROWTH LOOP ────────────────────
function startPopulationGrowth() {
setInterval(() => {
// count residential buildings
let resCount = 0;
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = 0; j < GRID_SIZE; j++) {
if (gridData[i][j]?.type === "res") resCount++;
}
}
if (resCount > 0) {
population += resCount * GROWTH_PER_S;
uiPop.innerText = "Population: " + Math.floor(population);
}
}, 1000);
}
// ───────────────────────── CARS ───────────────────────────────
function setupCars() {
// rectangular loop around 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),
];
for (let i = 0; i < NUM_CARS; i++) {
const car = new THREE.Mesh(
new THREE.BoxGeometry(
CELL_SIZE * 0.3,
CELL_SIZE * 0.2,
CELL_SIZE * 0.15,
),
new THREE.MeshPhongMaterial({ color: 0xff0000 }),
);
car.userData = { path, seg: 0, t: i / NUM_CARS, speed: 0.005 };
scene.add(car);
cars.push(car);
}
}
// ─────────────────────── PEOPLE ──────────────────────────────
function setupPeople() {
// roads run at indices where i%ROAD_EVERY==ROAD_EVERY-1 or j%ROAD_EVERY-1
const length = GRID_SIZE * CELL_SIZE;
for (let k = 0; k < NUM_PEOPLE; k++) {
// choose horizontal or vertical road
const horizontal = Math.random() < 0.5;
// pick a random road index
const idx = Math.floor(Math.random() * GRID_SIZE);
let fixedCoord;
if (horizontal) {
if (idx % ROAD_EVERY !== ROAD_EVERY - 1) continue;
fixedCoord = idx * CELL_SIZE + CELL_SIZE / 2; // z value
} else {
if (idx % ROAD_EVERY !== ROAD_EVERY - 1) continue;
fixedCoord = idx * CELL_SIZE + CELL_SIZE / 2; // x value
}
// sphere for person
const sph = new THREE.Mesh(
new THREE.SphereGeometry(CELL_SIZE * 0.1, 8, 8),
new THREE.MeshPhongMaterial({ color: 0x2222ff }),
);
sph.userData = {
horiz: horizontal,
fixed: fixedCoord,
t: Math.random(),
speed: 0.0008 + Math.random() * 0.0005,
};
scene.add(sph);
people.push(sph);
}
}
// ───────────────────────── ANIMATION ──────────────────────────
function animate() {
requestAnimationFrame(animate);
// move cars
cars.forEach((c) => {
const d = c.userData;
d.t += d.speed;
if (d.t > 1) {
d.t = 0;
d.seg = (d.seg + 1) % d.path.length;
}
const A = d.path[d.seg],
B = d.path[(d.seg + 1) % d.path.length];
c.position.lerpVectors(A, B, d.t);
// rotate toward travel direction
const dir = new THREE.Vector3().subVectors(B, A).normalize();
c.rotation.y = Math.atan2(dir.x, dir.z);
});
// move people
people.forEach((p) => {
const d = p.userData;
d.t = (d.t + d.speed) % 1;
if (d.horiz) {
// X moves 0→length, Z fixed
p.position.x = d.t * GRID_SIZE * CELL_SIZE;
p.position.z = d.fixed;
} else {
// Z moves, X fixed
p.position.z = d.t * GRID_SIZE * CELL_SIZE;
p.position.x = d.fixed;
}
p.position.y = 0.15;
});
renderer.render(scene, camera);
}
// ───────────────────── WINDOW RESIZE ──────────────────────────
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