Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hexagon Ecosystem Evolution</title> | |
| <style> | |
| body { margin: 0; background-color: #282c34; color: white; font-family: Arial; overflow: hidden; } | |
| #gameContainer { display: flex; height: 100vh; } | |
| #sidebar { width: 300px; padding: 10px; text-align: left; background-color: #1e1e1e; overflow-y: auto; } | |
| canvas { flex-grow: 1; background-color: #1e1e1e; cursor: pointer; height: 100%; } | |
| #timer, #counts, #selection { font-size: 18px; margin: 10px 0; } | |
| button { margin: 3px; padding: 8px; font-size: 14px; cursor: pointer; width: 50px; } | |
| .category { margin-top: 15px; font-weight: bold; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="gameContainer"> | |
| <div id="sidebar"> | |
| <div id="timer">Time Left: 5:00</div> | |
| <div id="counts">Counts: Loading...</div> | |
| <div id="selection"> | |
| <div class="category">Plants</div> | |
| <div id="plantSelection"></div> | |
| <div class="category">Insects</div> | |
| <div id="insectSelection"></div> | |
| <div class="category">Animals</div> | |
| <div id="animalSelection"></div> | |
| </div> | |
| </div> | |
| <canvas id="gameCanvas"></canvas> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById("gameCanvas"); | |
| const ctx = canvas.getContext("2d"); | |
| canvas.width = window.innerWidth - 300; // Full width minus sidebar | |
| canvas.height = window.innerHeight; // Full height | |
| const hexSize = 30; | |
| const hexWidth = Math.sqrt(3) * hexSize; | |
| const hexHeight = 2 * hexSize; | |
| const offsetX = hexWidth; | |
| const offsetY = hexHeight * 0.75; | |
| const rows = Math.ceil(canvas.height / offsetY) + 1; | |
| const cols = Math.ceil(canvas.width / offsetX) + 1; | |
| let hexGrid = []; | |
| let timer = 300; | |
| let selectedEntity = null; | |
| const entities = { | |
| plants: ["π±", "πΏ", "π", "πΊ", "πΈ", "πΌ", "π»", "πΎ", "π³", "π΄", "π", "π", "π", "π", "π", "π", "π", "π", "π", "π₯"], | |
| insects: ["π", "π¦", "π"], | |
| animals: ["π", "π¦", "π¦", "π¦", "πΏοΈ"] | |
| }; | |
| let resources = {}; | |
| let counts = {}; | |
| let animalPairs = new Map(); | |
| function initializeEntities() { | |
| Object.keys(entities).forEach(category => { | |
| entities[category].forEach(entity => { | |
| resources[entity] = 5; | |
| counts[entity] = 0; | |
| }); | |
| }); | |
| } | |
| function generateHexGrid() { | |
| hexGrid = []; | |
| for (let row = 0; row < rows; row++) { | |
| for (let col = 0; col < cols; col++) { | |
| let x = col * offsetX; | |
| let y = row * offsetY + (col % 2 ? offsetY / 2 : 0); | |
| hexGrid.push({ x, y, type: "empty", entities: [], nourishment: 0 }); | |
| } | |
| } | |
| } | |
| function selectEntity(entity) { | |
| selectedEntity = entity; | |
| } | |
| function drawHex(hex) { | |
| const { x, y, type, entities } = hex; | |
| ctx.beginPath(); | |
| for (let i = 0; i < 6; i++) { | |
| let angle = (Math.PI / 3) * i; | |
| let px = x + hexSize * Math.cos(angle); | |
| let py = y + hexSize * Math.sin(angle); | |
| ctx[i === 0 ? "moveTo" : "lineTo"](px, py); | |
| } | |
| ctx.closePath(); | |
| ctx.fillStyle = type === "empty" ? "#C2B280" : "#90EE90"; | |
| ctx.fill(); | |
| ctx.stroke(); | |
| ctx.fillStyle = "white"; | |
| ctx.font = "18px Arial"; | |
| entities.forEach((entity, index) => { | |
| let randX = x - 8 + (Math.random() * 16 - 8); | |
| let randY = y + 4 + (Math.random() * 16 - 8); | |
| ctx.fillText(entity, randX, randY); | |
| }); | |
| } | |
| function getNeighbors(hexIndex) { | |
| const neighbors = []; | |
| const row = Math.floor(hexIndex / cols); | |
| const col = hexIndex % cols; | |
| const directions = [ | |
| [-1, 0], [1, 0], [0, -1], [0, 1], | |
| col % 2 ? [-1, -1] : [-1, 1], | |
| col % 2 ? [1, -1] : [1, 1] | |
| ]; | |
| directions.forEach(([dr, dc]) => { | |
| const newRow = row + dr; | |
| const newCol = col + dc; | |
| if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) { | |
| neighbors.push(newRow * cols + newCol); | |
| } | |
| }); | |
| return neighbors; | |
| } | |
| function aStar(startIdx, goalIdx) { | |
| const openSet = new Set([startIdx]); | |
| const cameFrom = {}; | |
| const gScore = { [startIdx]: 0 }; | |
| const fScore = { [startIdx]: heuristic(startIdx, goalIdx) }; | |
| while (openSet.size > 0) { | |
| let current = Array.from(openSet).reduce((a, b) => fScore[a] < fScore[b] ? a : b); | |
| if (current === goalIdx) return reconstructPath(cameFrom, current); | |
| openSet.delete(current); | |
| getNeighbors(current).forEach(neighbor => { | |
| const tentativeGScore = gScore[current] + 1; | |
| if (!gScore[neighbor] || tentativeGScore < gScore[neighbor]) { | |
| cameFrom[neighbor] = current; | |
| gScore[neighbor] = tentativeGScore; | |
| fScore[neighbor] = gScore[neighbor] + heuristic(neighbor, goalIdx); | |
| openSet.add(neighbor); | |
| } | |
| }); | |
| } | |
| return []; | |
| } | |
| function heuristic(a, b) { | |
| const rowA = Math.floor(a / cols), colA = a % cols; | |
| const rowB = Math.floor(b / cols), colB = b % cols; | |
| return Math.abs(rowA - rowB) + Math.abs(colA - colB); | |
| } | |
| function reconstructPath(cameFrom, current) { | |
| const path = [current]; | |
| while (cameFrom[current] !== undefined) { | |
| current = cameFrom[current]; | |
| path.unshift(current); | |
| } | |
| return path; | |
| } | |
| function pollinate() { | |
| hexGrid.forEach(hex => { | |
| if (hex.entities.some(e => entities.insects.includes(e))) { | |
| hex.entities.forEach(entity => { | |
| if (entities.plants.includes(entity) && Math.random() < 0.2) { | |
| hex.entities.push(entity); // Plant baby in same hex | |
| counts[entity]++; | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| function moveAnimals() { | |
| hexGrid.forEach((hex, index) => { | |
| let animalsToMove = []; | |
| hex.entities.forEach((entity, i) => { | |
| if (entities.animals.includes(entity)) { | |
| let nourishment = hex.nourishment; | |
| const hasPlants = hex.entities.some(e => entities.plants.includes(e)); | |
| const hasInsects = hex.entities.some(e => entities.insects.includes(e)); | |
| // Eating logic | |
| if (hasPlants && Math.random() < 0.3) { | |
| const plantIdx = hex.entities.findIndex(e => entities.plants.includes(e)); | |
| hex.entities.splice(plantIdx, 1); | |
| nourishment += 2; | |
| counts[hex.entities[plantIdx]]--; | |
| } else if (hasInsects && Math.random() < 0.5) { | |
| const insectIdx = hex.entities.findIndex(e => entities.insects.includes(e)); | |
| hex.entities.splice(insectIdx, 1); | |
| nourishment += 1; | |
| counts[hex.entities[insectIdx]]--; | |
| } | |
| // Reproduction | |
| const mateCount = hex.entities.filter(e => e === entity).length; | |
| if (mateCount >= 2 && nourishment >= 2 && Math.random() < 0.1) { | |
| hex.entities.push(entity); // Baby in same hex | |
| counts[entity]++; | |
| nourishment -= 2; | |
| if (!animalPairs.has(entity + index)) { | |
| animalPairs.set(entity + index, true); | |
| } | |
| } | |
| // Movement toward food | |
| const neighbors = getNeighbors(index); | |
| let goalIdx = neighbors.find(n => hexGrid[n].entities.some(e => entities.plants.includes(e))); | |
| if (!goalIdx) goalIdx = neighbors.find(n => hexGrid[n].entities.some(e => entities.insects.includes(e))); | |
| if (!goalIdx) goalIdx = neighbors[Math.floor(Math.random() * neighbors.length)]; | |
| const path = aStar(index, goalIdx); | |
| if (path.length > 1) { | |
| animalsToMove.push({ entity, fromIdx: index, toIdx: path[1], nourishment }); | |
| if (animalPairs.has(entity + index)) { | |
| const pairIdx = hex.entities.indexOf(entity, i + 1); | |
| if (pairIdx !== -1) { | |
| animalsToMove.push({ entity, fromIdx: index, toIdx: path[1], nourishment }); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| animalsToMove.forEach(move => { | |
| const fromHex = hexGrid[move.fromIdx]; | |
| const toHex = hexGrid[move.toIdx]; | |
| const entityIdx = fromHex.entities.indexOf(move.entity); | |
| if (entityIdx !== -1) { | |
| fromHex.entities.splice(entityIdx, 1); | |
| toHex.entities.push(move.entity); | |
| toHex.nourishment += move.nourishment; | |
| fromHex.nourishment = Math.max(0, fromHex.nourishment - move.nourishment); | |
| if (fromHex.entities.length === 0) fromHex.type = "empty"; | |
| toHex.type = "occupied"; | |
| if (animalPairs.has(move.entity + move.fromIdx)) { | |
| animalPairs.delete(move.entity + move.fromIdx); | |
| animalPairs.set(move.entity + move.toIdx, true); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| function renderMap() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| hexGrid.forEach(hex => drawHex(hex)); | |
| } | |
| function updateGame() { | |
| timer--; | |
| document.getElementById("timer").innerText = `Time Left: ${Math.floor(timer / 60)}:${(timer % 60).toString().padStart(2, '0')}`; | |
| if (timer % 5 === 0) { | |
| pollinate(); | |
| moveAnimals(); | |
| updateCounts(); | |
| } | |
| renderMap(); | |
| if (timer <= 0) { | |
| clearInterval(gameLoop); | |
| alert("Game Over!"); | |
| } | |
| } | |
| function updateCounts() { | |
| let countText = "Counts: "; | |
| Object.keys(counts).forEach(entity => { | |
| countText += `${entity}:${counts[entity]} `; | |
| }); | |
| document.getElementById("counts").innerText = countText; | |
| } | |
| function populateSidebar() { | |
| ["plantSelection", "insectSelection", "animalSelection"].forEach((id, idx) => { | |
| const category = ["plants", "insects", "animals"][idx]; | |
| const div = document.getElementById(id); | |
| entities[category].forEach(entity => { | |
| const btn = document.createElement("button"); | |
| btn.innerText = entity; | |
| btn.onclick = () => selectEntity(entity); | |
| div.appendChild(btn); | |
| }); | |
| }); | |
| } | |
| canvas.addEventListener("click", (event) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const mouseX = event.clientX - rect.left; | |
| const mouseY = event.clientY - rect.top; | |
| hexGrid.forEach(hex => { | |
| if (Math.hypot(hex.x - mouseX, hex.y - mouseY) < hexSize) { | |
| if (selectedEntity && resources[selectedEntity] > 0 && hex.type === "empty") { | |
| hex.type = "occupied"; | |
| hex.entities.push(selectedEntity); | |
| resources[selectedEntity]--; | |
| counts[selectedEntity]++; | |
| updateCounts(); | |
| } | |
| } | |
| }); | |
| renderMap(); | |
| }); | |
| window.addEventListener("resize", () => { | |
| canvas.width = window.innerWidth - 300; | |
| canvas.height = window.innerHeight; | |
| generateHexGrid(); | |
| renderMap(); | |
| }); | |
| initializeEntities(); | |
| generateHexGrid(); | |
| populateSidebar(); | |
| renderMap(); | |
| const gameLoop = setInterval(updateGame, 1000); | |
| </script> | |
| </body> | |
| </html> |