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> |