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