-
-
Save shricodev/0cfd4fe62c3bf4cc6844505a9de90e4a to your computer and use it in GitHub Desktop.
OpenAI o4-Mini - SimCity Simulation - Part 1
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</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