-
-
Save shricodev/6116661b71c027d990ad8e1b09495d64 to your computer and use it in GitHub Desktop.
OpenAI o4-Mini - SimCity Simulation - Part 2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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