Skip to content

Commit ecfeed1

Browse files
committed
- add a living rps clone with dynamic entity count, speed and playground size
1 parent 78960ac commit ecfeed1

File tree

5 files changed

+695
-294
lines changed

5 files changed

+695
-294
lines changed

games/living_rps.html

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Living RPS</title>
7+
<script src="https://cdn.tailwindcss.com"></script>
8+
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
9+
</head>
10+
<body class="bg-gray-900 text-white flex items-center justify-center min-h-screen">
11+
<div class="w-auto bg-gray-800 p-6 rounded-lg shadow-xl">
12+
<div id="playground" class="w-[420px] h-[420px] border border-gray-600 relative overflow-hidden mb-4 rounded-full"></div>
13+
<div class="flex justify-between mb-4">
14+
<button id="start-btn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Start</button>
15+
<button id="stop-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Stop</button>
16+
<button id="reset-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Reset</button>
17+
</div>
18+
<div id="stats" class="text-lg mb-4 flex justify-center space-x-4"></div>
19+
<div class="bg-gray-700 rounded p-4">
20+
<h2 class="text-lg font-bold mb-2">Game Options</h2>
21+
<div class="mb-4">
22+
<label for="entity-count" class="block mb-2">Entity Count: <span id="entity-count-value">100</span></label>
23+
<input type="range" min="5" max="500" value="100" class="w-full" id="entity-count">
24+
</div>
25+
<div class="mb-4">
26+
<label for="entity-speed" class="block mb-2">Entity Speed: <span id="entity-speed-value">2</span></label>
27+
<input type="range" min="1" max="20" value="2" class="w-full" id="entity-speed">
28+
</div>
29+
<div>
30+
<label for="playground-size" class="block mb-2">Playground Size: <span id="playground-size-value">420</span></label>
31+
<input type="range" min="200" max="1000" value="420" class="w-full" id="playground-size">
32+
</div>
33+
</div>
34+
</div>
35+
36+
<script>
37+
const playground = document.getElementById('playground');
38+
const startBtn = document.getElementById('start-btn');
39+
const stopBtn = document.getElementById('stop-btn');
40+
const resetBtn = document.getElementById('reset-btn');
41+
const statsDiv = document.getElementById('stats');
42+
const entityCountSlider = document.getElementById('entity-count');
43+
const entityCountValue = document.getElementById('entity-count-value');
44+
const entitySpeedSlider = document.getElementById('entity-speed');
45+
const entitySpeedValue = document.getElementById('entity-speed-value');
46+
const playgroundSizeSlider = document.getElementById('playground-size');
47+
const playgroundSizeValue = document.getElementById('playground-size-value');
48+
49+
const ENTITY_TYPES = ['rock', 'paper', 'scissors'];
50+
const ENTITY_SYMBOLS = {
51+
rock: '🗿',
52+
paper: '📄',
53+
scissors: '✂️'
54+
};
55+
56+
let entities = [];
57+
let animationId;
58+
let lastTime = 0;
59+
let entityCount = 100;
60+
let speedFactor = 2;
61+
62+
const ENTITY_SIZE = 20;
63+
const CENTER_FORCE = 0.00001;
64+
const SAME_TYPE_ATTRACTION = 0.00001;
65+
const REPULSION_DISTANCE = ENTITY_SIZE * 1.5;
66+
const REPULSION_FORCE = 0.1;
67+
let PLAYGROUND_SIZE = 420;
68+
let PLAYGROUND_RADIUS = PLAYGROUND_SIZE / 2;
69+
let CENTER_X = PLAYGROUND_RADIUS;
70+
let CENTER_Y = PLAYGROUND_RADIUS;
71+
72+
class Entity {
73+
constructor(type, x, y) {
74+
this.type = type;
75+
this.element = document.createElement('div');
76+
this.element.classList.add('absolute', 'text-2xl');
77+
this.element.textContent = ENTITY_SYMBOLS[type];
78+
this.x = x;
79+
this.y = y;
80+
this.vx = (Math.random() - 0.5) * speedFactor;
81+
this.vy = (Math.random() - 0.5) * speedFactor;
82+
this.ax = 0;
83+
this.ay = 0;
84+
playground.appendChild(this.element);
85+
this.updatePosition();
86+
}
87+
88+
updatePosition() {
89+
this.element.style.left = `${this.x - ENTITY_SIZE / 2}px`;
90+
this.element.style.top = `${this.y - ENTITY_SIZE / 2}px`;
91+
}
92+
93+
move(deltaTime) {
94+
this.vx += this.ax;
95+
this.vy += this.ay;
96+
this.vx += (Math.random() - 0.5) * 0.2;
97+
this.vy += (Math.random() - 0.5) * 0.2;
98+
99+
const dx = CENTER_X - this.x;
100+
const dy = CENTER_Y - this.y;
101+
const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
102+
this.vx += dx * CENTER_FORCE * distanceFromCenter;
103+
this.vy += dy * CENTER_FORCE * distanceFromCenter;
104+
105+
this.x += this.vx * deltaTime / 16;
106+
this.y += this.vy * deltaTime / 16;
107+
108+
if (distanceFromCenter > PLAYGROUND_RADIUS - ENTITY_SIZE / 2) {
109+
const angle = Math.atan2(dy, dx);
110+
111+
this.x = CENTER_X + (PLAYGROUND_RADIUS - ENTITY_SIZE / 2 - 1) * Math.cos(angle);
112+
this.y = CENTER_Y + (PLAYGROUND_RADIUS - ENTITY_SIZE / 2 - 1) * Math.sin(angle);
113+
114+
const normalX = Math.cos(angle);
115+
const normalY = Math.sin(angle);
116+
117+
const dotProduct = this.vx * normalX + this.vy * normalY;
118+
119+
this.vx = this.vx - 2 * dotProduct * normalX;
120+
this.vy = this.vy - 2 * dotProduct * normalY;
121+
122+
if (this.vx * dx + this.vy * dy > 0) {
123+
this.vx = -this.vx;
124+
this.vy = -this.vy;
125+
}
126+
127+
this.vx += (Math.random() - 0.5) * 0.5;
128+
this.vy += (Math.random() - 0.5) * 0.5;
129+
130+
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
131+
this.vx = (this.vx / speed) * speedFactor;
132+
this.vy = (this.vy / speed) * speedFactor;
133+
}
134+
135+
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
136+
if (speed > speedFactor) {
137+
this.vx = (this.vx / speed) * speedFactor;
138+
this.vy = (this.vy / speed) * speedFactor;
139+
}
140+
141+
this.updatePosition();
142+
this.ax = 0;
143+
this.ay = 0;
144+
}
145+
146+
flock(others) {
147+
let avgVx = 0, avgVy = 0;
148+
let centerX = 0, centerY = 0;
149+
let separationX = 0, separationY = 0;
150+
let count = 0;
151+
152+
others.forEach(other => {
153+
if (other !== this) {
154+
const dx = other.x - this.x;
155+
const dy = other.y - this.y;
156+
const distance = Math.sqrt(dx * dx + dy * dy);
157+
158+
if (distance < 70) {
159+
avgVx += other.vx;
160+
avgVy += other.vy;
161+
centerX += other.x;
162+
centerY += other.y;
163+
separationX -= dx / distance;
164+
separationY -= dy / distance;
165+
count++;
166+
}
167+
168+
// Apply attraction force for same type entities
169+
if (other.type === this.type) {
170+
this.vx += dx * SAME_TYPE_ATTRACTION;
171+
this.vy += dy * SAME_TYPE_ATTRACTION;
172+
}
173+
174+
// Apply repulsion force for close entities
175+
if (distance < REPULSION_DISTANCE) {
176+
const repulsionStrength = (REPULSION_DISTANCE - distance) / REPULSION_DISTANCE;
177+
this.vx -= dx / distance * REPULSION_FORCE * repulsionStrength;
178+
this.vy -= dy / distance * REPULSION_FORCE * repulsionStrength;
179+
}
180+
}
181+
});
182+
183+
if (count > 0) {
184+
this.ax += (avgVx / count - this.vx) * 0.05;
185+
this.ay += (avgVy / count - this.vy) * 0.05;
186+
this.ax += (centerX / count - this.x) * 0.0005;
187+
this.ay += (centerY / count - this.y) * 0.0005;
188+
this.ax += separationX * 0.05;
189+
this.ay += separationY * 0.05;
190+
}
191+
}
192+
193+
interact(others) {
194+
let nearestPrey = null;
195+
let nearestPredator = null;
196+
let minPreyDist = Infinity;
197+
let minPredatorDist = Infinity;
198+
199+
others.forEach(other => {
200+
if (other !== this) {
201+
const dx = other.x - this.x;
202+
const dy = other.y - this.y;
203+
const distance = Math.sqrt(dx * dx + dy * dy);
204+
205+
if (distance < ENTITY_SIZE) {
206+
if (
207+
(this.type === 'rock' && other.type === 'scissors') ||
208+
(this.type === 'paper' && other.type === 'rock') ||
209+
(this.type === 'scissors' && other.type === 'paper')
210+
) {
211+
other.transform(this.type);
212+
}
213+
} else if (distance < 100) {
214+
if (
215+
(this.type === 'rock' && other.type === 'scissors') ||
216+
(this.type === 'paper' && other.type === 'rock') ||
217+
(this.type === 'scissors' && other.type === 'paper')
218+
) {
219+
if (distance < minPreyDist) {
220+
minPreyDist = distance;
221+
nearestPrey = other;
222+
}
223+
} else if (
224+
(this.type === 'rock' && other.type === 'paper') ||
225+
(this.type === 'paper' && other.type === 'scissors') ||
226+
(this.type === 'scissors' && other.type === 'rock')
227+
) {
228+
if (distance < minPredatorDist) {
229+
minPredatorDist = distance;
230+
nearestPredator = other;
231+
}
232+
}
233+
}
234+
}
235+
});
236+
237+
const huntStrength = 0.004;
238+
const fleeStrength = 0.002;
239+
240+
if (nearestPrey) {
241+
this.ax += (nearestPrey.x - this.x) * huntStrength;
242+
this.ay += (nearestPrey.y - this.y) * huntStrength;
243+
}
244+
245+
if (nearestPredator) {
246+
this.ax -= (nearestPredator.x - this.x) * fleeStrength;
247+
this.ay -= (nearestPredator.y - this.y) * fleeStrength;
248+
}
249+
}
250+
251+
transform(newType) {
252+
this.type = newType;
253+
anime({
254+
targets: this.element,
255+
scale: [1.5, 1],
256+
duration: 300,
257+
easing: 'easeOutElastic(1, .5)'
258+
});
259+
this.element.textContent = ENTITY_SYMBOLS[newType];
260+
}
261+
}
262+
263+
function initGame() {
264+
entities = [];
265+
playground.innerHTML = '';
266+
const types = ['rock', 'paper', 'scissors'];
267+
let typeIndex = 0;
268+
for (let i = 0; i < entityCount; i++) {
269+
const type = types[typeIndex];
270+
const angle = Math.random() * 2 * Math.PI;
271+
const radius = Math.sqrt(Math.random()) * (PLAYGROUND_RADIUS - ENTITY_SIZE / 2);
272+
const x = CENTER_X + radius * Math.cos(angle);
273+
const y = CENTER_Y + radius * Math.sin(angle);
274+
entities.push(new Entity(type, x, y));
275+
typeIndex = (typeIndex + 1) % 3;
276+
}
277+
updatePlaygroundSize();
278+
}
279+
280+
function updateGame(currentTime) {
281+
const deltaTime = currentTime - lastTime;
282+
lastTime = currentTime;
283+
284+
entities.forEach(entity => {
285+
entity.flock(entities);
286+
entity.interact(entities);
287+
});
288+
289+
entities.forEach(entity => {
290+
entity.move(deltaTime);
291+
});
292+
293+
updateStats();
294+
295+
if (isGameOver()) {
296+
stopGame();
297+
startBtn.disabled = false;
298+
} else {
299+
animationId = requestAnimationFrame(updateGame);
300+
}
301+
}
302+
303+
function updateStats() {
304+
const stats = entities.reduce((acc, entity) => {
305+
acc[entity.type] = (acc[entity.type] || 0) + 1;
306+
return acc;
307+
}, {});
308+
309+
statsDiv.innerHTML = ENTITY_TYPES.map(type =>
310+
`<span>${ENTITY_SYMBOLS[type]} ${stats[type] || 0}</span>`
311+
).join('');
312+
}
313+
314+
function isGameOver() {
315+
return entities.every(entity => entity.type === entities[0].type);
316+
}
317+
318+
function startGame() {
319+
if (!animationId) {
320+
if (isGameOver()) {
321+
initGame();
322+
}
323+
lastTime = performance.now();
324+
animationId = requestAnimationFrame(updateGame);
325+
startBtn.disabled = true;
326+
stopBtn.disabled = false;
327+
}
328+
}
329+
330+
function stopGame() {
331+
if (animationId) {
332+
cancelAnimationFrame(animationId);
333+
animationId = null;
334+
startBtn.disabled = false;
335+
stopBtn.disabled = true;
336+
}
337+
}
338+
339+
function resetGame() {
340+
stopGame();
341+
initGame();
342+
updateStats();
343+
startBtn.disabled = false;
344+
}
345+
346+
function updatePlaygroundSize() {
347+
playground.style.width = `${PLAYGROUND_SIZE}px`;
348+
playground.style.height = `${PLAYGROUND_SIZE}px`;
349+
const container = playground.parentElement;
350+
container.style.width = `${PLAYGROUND_SIZE + 30}px`;
351+
}
352+
353+
startBtn.addEventListener('click', startGame);
354+
stopBtn.addEventListener('click', stopGame);
355+
resetBtn.addEventListener('click', resetGame);
356+
357+
entityCountSlider.addEventListener('input', function() {
358+
entityCount = parseInt(this.value);
359+
entityCountValue.textContent = entityCount;
360+
resetGame();
361+
});
362+
363+
entitySpeedSlider.addEventListener('input', function() {
364+
speedFactor = parseInt(this.value);
365+
entitySpeedValue.textContent = speedFactor;
366+
entities.forEach(entity => {
367+
const speed = Math.sqrt(entity.vx * entity.vx + entity.vy * entity.vy);
368+
entity.vx = (entity.vx / speed) * speedFactor;
369+
entity.vy = (entity.vy / speed) * speedFactor;
370+
});
371+
});
372+
373+
playgroundSizeSlider.addEventListener('input', function() {
374+
PLAYGROUND_SIZE = parseInt(this.value);
375+
PLAYGROUND_RADIUS = PLAYGROUND_SIZE / 2;
376+
CENTER_X = PLAYGROUND_RADIUS;
377+
CENTER_Y = PLAYGROUND_RADIUS;
378+
playgroundSizeValue.textContent = PLAYGROUND_SIZE;
379+
updatePlaygroundSize();
380+
resetGame();
381+
});
382+
383+
initGame();
384+
updateStats();
385+
updatePlaygroundSize();
386+
</script>
387+
</body>
388+
</html>

0 commit comments

Comments
 (0)