AntDX316 commited on
Commit
e9e4cf4
·
1 Parent(s): 23b1b4e
Files changed (12) hide show
  1. .dockerignore +0 -23
  2. Dockerfile +3 -8
  3. README.md +1 -102
  4. app.py +550 -605
  5. docker-compose.yml +9 -10
  6. healthcheck.html +0 -23
  7. index.html +0 -27
  8. interface.py +0 -46
  9. nginx.conf +0 -40
  10. requirements.txt +4 -1
  11. script.js +0 -412
  12. styles.css +0 -95
.dockerignore DELETED
@@ -1,23 +0,0 @@
1
- # Git
2
- .git
3
- .gitignore
4
- .gitattributes
5
-
6
- # Docker
7
- Dockerfile
8
- docker-compose.yml
9
- .dockerignore
10
-
11
- # Node.js (if you later add npm packages)
12
- node_modules
13
- npm-debug.log
14
-
15
- # IDE files
16
- .idea
17
- .vscode
18
- *.swp
19
- *.swo
20
-
21
- # OS files
22
- .DS_Store
23
- Thumbs.db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -6,11 +6,9 @@ WORKDIR /app
6
  COPY requirements.txt /app/
7
  RUN pip install --no-cache-dir -r requirements.txt
8
 
9
- # Copy application files
10
- COPY index.html /app/
11
- COPY styles.css /app/
12
- COPY script.js /app/
13
  COPY app.py /app/
 
14
 
15
  # Set proper permissions
16
  RUN chmod -R 755 /app
@@ -25,8 +23,5 @@ ENV STREAMLIT_SERVER_HEADLESS=true
25
  ENV STREAMLIT_SERVER_ENABLE_CORS=false
26
  ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
27
 
28
- # Print environment for debugging
29
- RUN echo "Files in container:" && ls -la /app/
30
-
31
  # Run the Streamlit app
32
- CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
6
  COPY requirements.txt /app/
7
  RUN pip install --no-cache-dir -r requirements.txt
8
 
9
+ # Copy all application files
 
 
 
10
  COPY app.py /app/
11
+ COPY README.md /app/
12
 
13
  # Set proper permissions
14
  RUN chmod -R 755 /app
 
23
  ENV STREAMLIT_SERVER_ENABLE_CORS=false
24
  ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
25
 
 
 
 
26
  # Run the Streamlit app
27
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md CHANGED
@@ -3,107 +3,6 @@ title: HF Map
3
  emoji: 💻
4
  colorFrom: purple
5
  colorTo: green
6
- sdk: docker
7
  pinned: false
8
  ---
9
-
10
- # Battle Simulator
11
-
12
- An interactive battle simulation where red and blue forces fight on a dynamic battlefield. The application features different unit types, automatic combat AI, and an interactive interface that lets you inspect units during battle.
13
-
14
- ## Features
15
-
16
- - **Multiple Unit Types**: Infantry, Tank, and Artillery units with unique stats and abilities
17
- - **Dynamic Combat**: Units automatically seek targets, attack, and respond to battlefield conditions
18
- - **Interactive UI**: Click on any unit to see its detailed stats and status
19
- - **Auto-respawn**: Units respawn after being defeated, maintaining continuous battle
20
- - **Visual Effects**: Attack lines, health bars, and status indicators provide visual feedback
21
-
22
- ## Running the Application
23
-
24
- ### Standard Method
25
-
26
- Simply open the `index.html` file in any modern web browser:
27
-
28
- ```
29
- # Windows
30
- start index.html
31
-
32
- # macOS
33
- open index.html
34
-
35
- # Linux
36
- xdg-open index.html
37
- ```
38
-
39
- ### Using Docker
40
-
41
- This application can also be run using Docker, which ensures consistent behavior across different environments.
42
-
43
- #### Prerequisites
44
-
45
- - Docker
46
- - Docker Compose (optional, for easier management)
47
-
48
- #### Using Docker Directly
49
-
50
- 1. Build the Docker image:
51
- ```
52
- docker build -t battle-simulator .
53
- ```
54
-
55
- 2. Run the container:
56
- ```
57
- docker run -p 8080:8080 battle-simulator
58
- ```
59
-
60
- 3. Open your browser and navigate to:
61
- ```
62
- http://localhost:8080
63
- ```
64
-
65
- #### Using Docker Compose
66
-
67
- 1. Start the application:
68
- ```
69
- docker-compose up
70
- ```
71
-
72
- 2. Open your browser and navigate to:
73
- ```
74
- http://localhost:8080
75
- ```
76
-
77
- 3. To stop the application:
78
- ```
79
- docker-compose down
80
- ```
81
-
82
- ### Deploying to Hugging Face Spaces
83
-
84
- This application is fully compatible with Hugging Face Spaces and uses Streamlit for the deployment:
85
-
86
- 1. **Streamlit Integration**: Uses Streamlit to host the battle simulation in a web-friendly interface
87
- 2. **Embedded HTML/JS Application**: The original HTML/CSS/JS application is embedded directly in the Streamlit interface
88
- 3. **Port 8501**: The application runs on Streamlit's default port (8501)
89
- 4. **Simplified Deployment**: No need for complex web server configurations
90
- 5. **Hugging Face Spaces Optimized**: The Docker configuration is specifically designed to work seamlessly in the Hugging Face Spaces environment
91
-
92
- To deploy to Hugging Face Spaces:
93
-
94
- 1. Push your code to a GitHub repository
95
- 2. Create a new Space on Hugging Face with Docker SDK
96
- 3. Connect your GitHub repository to the Space
97
- 4. The Space will automatically build and deploy your application
98
-
99
- ## How to Use
100
-
101
- - **View Unit Info**: Click on any unit to see its details in the info panel
102
- - **Close Info Panel**: Click the "Close" button to dismiss the unit info panel
103
- - **Watch the Battle**: Observe as units automatically engage in combat across the map
104
-
105
- ## Technical Details
106
-
107
- - Built with pure JavaScript, HTML5, and CSS3
108
- - Uses HTML5 Canvas for rendering
109
- - No external dependencies required
 
3
  emoji: 💻
4
  colorFrom: purple
5
  colorTo: green
6
+ sdk: streamlit
7
  pinned: false
8
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,654 +1,599 @@
1
  import streamlit as st
2
- import os
 
 
 
 
 
 
 
 
3
 
4
- # Set page configuration
5
  st.set_page_config(
6
  page_title="Battle Simulator",
7
  page_icon="🎮",
8
- layout="wide"
 
9
  )
10
 
11
- # Main app content
12
- st.title("Battle Simulator")
13
- st.markdown("Click on units to see their details as they battle!")
14
-
15
- # HTML content to embed (directly including instead of reading from files)
16
- html_content = """
17
- <!DOCTYPE html>
18
- <html lang="en">
19
- <head>
20
- <meta charset="UTF-8">
21
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
- <title>Battle Simulator</title>
23
- <style>
24
- * {
25
- margin: 0;
26
- padding: 0;
27
- box-sizing: border-box;
28
- }
29
-
30
- body {
31
- font-family: Arial, sans-serif;
32
- background-color: transparent;
33
- padding: 0;
34
  }
35
-
36
- .container {
37
- max-width: 1000px;
38
- margin: 0 auto;
39
- background-color: white;
40
- padding: 20px;
41
- border-radius: 10px;
42
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
- h1 {
46
- text-align: center;
47
- margin-bottom: 20px;
48
- color: #333;
49
- }
50
 
51
- .battlefield {
52
- position: relative;
53
- }
54
 
55
- canvas {
56
- display: block;
57
- margin: 0 auto;
58
- border: 1px solid #ccc;
59
- background-color: #e8f4e5;
60
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- #info-panel {
63
- position: absolute;
64
- top: 20px;
65
- right: 20px;
66
- background-color: rgba(255, 255, 255, 0.95);
67
- padding: 20px;
68
- border-radius: 8px;
69
- border: 2px solid #4a6ea9;
70
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
71
- width: 300px;
72
- z-index: 10;
73
- transition: all 0.3s ease;
74
- animation: fadeIn 0.3s ease-out;
75
- }
76
 
77
- @keyframes fadeIn {
78
- from {
79
- opacity: 0;
80
- transform: translateY(-10px);
81
- }
82
- to {
83
- opacity: 1;
84
- transform: translateY(0);
85
- }
86
- }
87
 
88
- #info-panel h3 {
89
- margin-bottom: 15px;
90
- color: #333;
91
- border-bottom: 1px solid #ddd;
92
- padding-bottom: 10px;
93
- }
94
 
95
- #info-panel p {
96
- margin-bottom: 10px;
97
- display: flex;
98
- justify-content: space-between;
99
- }
100
 
101
- #close-info {
102
- display: block;
103
- margin: 20px auto 0;
104
- padding: 8px 16px;
105
- background-color: #4a6ea9;
106
- color: white;
107
- border: none;
108
- border-radius: 4px;
109
- cursor: pointer;
110
- }
111
 
112
- #close-info:hover {
113
- background-color: #3a5a89;
114
- }
 
115
 
116
- .hidden {
117
- display: none;
118
- }
119
- </style>
120
- </head>
121
- <body>
122
- <div class="container">
123
- <div class="battlefield">
124
- <canvas id="map" width="800" height="600"></canvas>
125
- <div id="info-panel" class="hidden">
126
- <h3 id="unit-name">Unit Name</h3>
127
- <p id="unit-type">Type: <span></span></p>
128
- <p id="unit-health">Health: <span></span></p>
129
- <p id="unit-attack">Attack: <span></span></p>
130
- <p id="unit-defense">Defense: <span></span></p>
131
- <p id="unit-status">Status: <span></span></p>
132
- <button id="close-info">Close</button>
133
- </div>
134
- </div>
135
- </div>
 
 
 
136
 
137
- <script>
138
- // Unit class definitions
139
- class Unit {
140
- constructor(id, team, type, x, y) {
141
- this.id = id;
142
- this.team = team; // 'red' or 'blue'
143
- this.type = type; // 'infantry', 'tank', or 'artillery'
144
- this.x = x;
145
- this.y = y;
146
- this.targetX = x;
147
- this.targetY = y;
148
- this.speed = 1;
149
- this.health = 100;
150
- this.maxHealth = 100;
151
- this.attackRange = 50;
152
- this.attackPower = 10;
153
- this.defense = 5;
154
- this.attackCooldown = 0;
155
- this.attackCooldownMax = 60;
156
- this.target = null;
157
- this.attackLine = null;
158
- this.isMoving = false;
159
- this.state = 'idle'; // 'idle', 'moving', 'attacking', 'dead'
160
- this.respawnTime = 0;
161
- this.respawnTimeMax = 180;
162
-
163
- // Set unit-specific properties
164
- this.setupUnitType();
165
- }
166
-
167
- setupUnitType() {
168
- switch (this.type) {
169
- case 'infantry':
170
- this.speed = 1.5;
171
- this.health = 80;
172
- this.maxHealth = 80;
173
- this.attackRange = 40;
174
- this.attackPower = Ω;
175
- this.defense = 3;
176
- this.attackCooldownMax = 45;
177
- this.respawnTimeMax = 120;
178
- break;
179
- case 'tank':
180
- this.speed = 0.8;
181
- this.health = 150;
182
- this.maxHealth = 150;
183
- this.attackRange = 60;
184
- this.attackPower = 15;
185
- this.defense = 10;
186
- this.attackCooldownMax = 75;
187
- this.respawnTimeMax = 240;
188
- break;
189
- case 'artillery':
190
- this.speed = 0.5;
191
- this.health = 60;
192
- this.maxHealth = 60;
193
- this.attackRange = 120;
194
- this.attackPower = 25;
195
- this.defense = 2;
196
- this.attackCooldownMax = 90;
197
- this.respawnTimeMax = 180;
198
- break;
199
- }
200
- }
201
-
202
- update(units) {
203
- if (this.state === 'dead') {
204
- this.respawnTime++;
205
- if (this.respawnTime >= this.respawnTimeMax) {
206
- this.respawn();
207
- }
208
- return;
209
- }
210
 
211
- // Check for targets if we don't have one or our target is dead
212
- if (!this.target || this.target.state === 'dead') {
213
- this.findTarget(units);
214
- this.state = 'idle';
215
- }
216
-
217
- // If we have a target, pursue it
218
- if (this.target) {
219
- const distance = Math.sqrt(
220
- Math.pow(this.target.x - this.x, 2) +
221
- Math.pow(this.target.y - this.y, 2)
222
- );
223
 
224
- // If in attack range, attack
225
- if (distance <= this.attackRange) {
226
- this.attack();
227
- this.state = 'attacking';
228
- }
229
- // Otherwise move toward target
230
- else {
231
- this.moveTowardTarget();
232
- this.state = 'moving';
233
- }
234
- } else {
235
- // Wander if no target
236
- this.wander();
237
- }
238
-
239
- // Handle movement
240
- this.move();
241
-
242
- // Update attack cooldown
243
- if (this.attackCooldown > 0) {
244
- this.attackCooldown--;
245
- }
246
- }
247
-
248
- findTarget(units) {
249
- let nearestDist = Infinity;
250
- this.target = null;
251
 
252
- for (const unit of units) {
253
- // Skip if same team or dead
254
- if (unit.team === this.team || unit.state === 'dead') continue;
255
 
256
- const dist = Math.sqrt(
257
- Math.pow(unit.x - this.x, 2) +
258
- Math.pow(unit.y - this.y, 2)
259
- );
 
 
 
 
 
 
 
 
 
 
260
 
261
- if (dist < nearestDist) {
262
- nearestDist = dist;
263
- this.target = unit;
 
 
 
 
264
  }
265
- }
266
- }
267
-
268
- attack() {
269
- if (this.attackCooldown === 0 && this.target && this.target.state !== 'dead') {
270
- // Calculate damage with some randomness
271
- const baseDamage = this.attackPower * (0.8 + Math.random() * 0.4);
272
- const damage = Math.max(1, baseDamage - this.target.defense / 2);
273
 
274
- // Apply damage to target
275
- this.target.takeDamage(damage);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
- // Create attack line for visual effect
278
- this.attackLine = {
279
- fromX: this.x,
280
- fromY: this.y,
281
- toX: this.target.x,
282
- toY: this.target.y,
283
- duration: 10, // frames the line will remain visible
284
- current: 0
285
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- // Reset cooldown
288
- this.attackCooldown = this.attackCooldownMax;
289
- }
290
- }
291
-
292
- takeDamage(amount) {
293
- this.health -= amount;
294
- if (this.health <= 0) {
295
- this.die();
296
- }
297
- }
298
-
299
- die() {
300
- this.state = 'dead';
301
- this.health = 0;
302
- this.respawnTime = 0;
303
- this.attackLine = null;
304
- }
305
-
306
- respawn() {
307
- // Respawn in a random position based on team
308
- const margin = 50;
309
- this.x = this.team === 'red'
310
- ? margin + Math.random() * 100
311
- : 800 - margin - Math.random() * 100;
312
- this.y = 100 + Math.random() * 400;
313
-
314
- this.health = this.maxHealth;
315
- this.state = 'idle';
316
- this.target = null;
317
- this.attackLine = null;
318
- this.respawnTime = 0;
319
- this.attackCooldown = 0;
320
- }
321
-
322
- moveTowardTarget() {
323
- if (!this.target) return;
324
-
325
- this.targetX = this.target.x;
326
- this.targetY = this.target.y;
327
-
328
- // Keep some distance based on unit type
329
- const angle = Math.atan2(this.targetY - this.y, this.targetX - this.x);
330
- let preferredDistance = this.attackRange * 0.8;
331
-
332
- if (this.type === 'artillery') {
333
- preferredDistance = this.attackRange * 0.9;
334
- } else if (this.type === 'infantry') {
335
- preferredDistance = this.attackRange * 0.6;
336
- }
337
-
338
- const distance = Math.sqrt(
339
- Math.pow(this.target.x - this.x, 2) +
340
- Math.pow(this.target.y - this.y, 2)
341
- );
342
-
343
- if (distance > preferredDistance) {
344
- this.targetX = this.target.x - Math.cos(angle) * preferredDistance;
345
- this.targetY = this.target.y - Math.sin(angle) * preferredDistance;
346
- } else {
347
- this.targetX = this.x;
348
- this.targetY = this.y;
349
- }
350
-
351
- this.isMoving = true;
352
- }
353
-
354
- wander() {
355
- // Only set a new random destination occasionally
356
- if (Math.random() < 0.01 || !this.isMoving) {
357
- const margin = 50;
358
- const teamZone = this.team === 'red' ? 300 : 500;
359
- const centerX = this.team === 'red' ? 200 : 600;
360
 
361
- this.targetX = Math.max(margin, Math.min(800 - margin,
362
- centerX + (Math.random() - 0.5) * teamZone));
363
- this.targetY = Math.max(margin, Math.min(600 - margin,
364
- 300 + (Math.random() - 0.5) * 400));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
- this.isMoving = true;
367
- }
368
- }
369
-
370
- move() {
371
- if (this.state === 'dead') return;
372
-
373
- const dx = this.targetX - this.x;
374
- const dy = this.targetY - this.y;
375
- const distance = Math.sqrt(dx * dx + dy * dy);
376
-
377
- if (distance > 5) {
378
- this.x += (dx / distance) * this.speed;
379
- this.y += (dy / distance) * this.speed;
380
- } else {
381
- this.isMoving = false;
382
- }
383
-
384
- // Keep units within bounds
385
- this.x = Math.max(10, Math.min(790, this.x));
386
- this.y = Math.max(10, Math.min(590, this.y));
387
- }
388
-
389
- draw(ctx) {
390
- if (this.state === 'dead') return;
391
-
392
- // Draw unit shape based on type
393
- ctx.fillStyle = this.team === 'red' ? '#e74c3c' : '#3498db';
394
-
395
- // Different shape for different unit types
396
- if (this.type === 'infantry') {
397
- // Circle for infantry
398
- ctx.beginPath();
399
- ctx.arc(this.x, this.y, 10, 0, Math.PI * 2);
400
- ctx.fill();
401
- } else if (this.type === 'tank') {
402
- // Square for tanks
403
- ctx.fillRect(this.x - 12, this.y - 12, 24, 24);
404
- } else if (this.type === 'artillery') {
405
- // Triangle for artillery
406
- ctx.beginPath();
407
- ctx.moveTo(this.x, this.y - 15);
408
- ctx.lineTo(this.x - 13, this.y + 10);
409
- ctx.lineTo(this.x + 13, this.y + 10);
410
- ctx.closePath();
411
- ctx.fill();
412
- }
413
-
414
- // Draw health bar
415
- const healthPercentage = this.health / this.maxHealth;
416
- const healthBarWidth = 20;
417
- const healthBarHeight = 3;
418
-
419
- // Health bar background
420
- ctx.fillStyle = '#e74c3c'; // Red for all health bars background
421
- ctx.fillRect(
422
- this.x - healthBarWidth / 2,
423
- this.y - 20,
424
- healthBarWidth,
425
- healthBarHeight
426
- );
427
-
428
- // Current health
429
- ctx.fillStyle = '#2ecc71'; // Green for health
430
- ctx.fillRect(
431
- this.x - healthBarWidth / 2,
432
- this.y - 20,
433
- healthBarWidth * healthPercentage,
434
- healthBarHeight
435
- );
436
-
437
- // Draw attack lines
438
- if (this.attackLine && this.attackLine.current < this.attackLine.duration) {
439
- ctx.strokeStyle = this.team === 'red' ? 'rgba(231, 76, 60, 0.7)' : 'rgba(52, 152, 219, 0.7)';
440
- ctx.lineWidth = 2;
441
- ctx.beginPath();
442
- ctx.moveTo(this.attackLine.fromX, this.attackLine.fromY);
443
- ctx.lineTo(this.attackLine.toX, this.attackLine.toY);
444
- ctx.stroke();
445
 
446
- this.attackLine.current++;
447
- }
448
-
449
- // Draw unit state indicator
450
- ctx.fillStyle = this.getStateColor();
451
- ctx.beginPath();
452
- ctx.arc(this.x, this.y - 25, 3, 0, Math.PI * 2);
453
- ctx.fill();
454
- }
455
-
456
- getStateColor() {
457
- switch (this.state) {
458
- case 'idle':
459
- return '#95a5a6'; // Gray
460
- case 'moving':
461
- return '#3498db'; // Blue
462
- case 'attacking':
463
- return '#e74c3c'; // Red
464
- default:
465
- return '#95a5a6';
466
- }
467
- }
468
-
469
- isPointInside(pointX, pointY) {
470
- if (this.state === 'dead') return false;
471
-
472
- const size = this.type === 'tank' ? 12 :
473
- this.type === 'artillery' ? 13 : 10;
474
-
475
- return Math.sqrt(
476
- Math.pow(this.x - pointX, 2) +
477
- Math.pow(this.y - pointY, 2)
478
- ) <= size;
479
- }
480
 
481
- getInfoText() {
482
- return {
483
- name: `${this.team.charAt(0).toUpperCase() + this.team.slice(1)} ${this.type.charAt(0).toUpperCase() + this.type.slice(1)} #${this.id}`,
484
- type: this.type.charAt(0).toUpperCase() + this.type.slice(1),
485
- health: `${Math.round(this.health)}/${this.maxHealth}`,
486
- attack: this.attackPower,
487
- defense: this.defense,
488
- status: this.state.charAt(0).toUpperCase() + this.state.slice(1)
489
- };
490
- }
491
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
- // Main app code
494
- document.addEventListener('DOMContentLoaded', function() {
495
- const canvas = document.getElementById('map');
496
- const ctx = canvas.getContext('2d');
497
- const infoPanel = document.getElementById('info-panel');
498
- const closeInfoBtn = document.getElementById('close-info');
 
 
 
 
499
 
500
- let units = [];
501
- let selectedUnit = null;
502
 
503
- // Create initial units
504
- createUnits();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
 
506
- // Set up game loop
507
- setInterval(updateGame, 1000 / 60); // 60 FPS
 
508
 
509
- // Add event listeners
510
- canvas.addEventListener('click', handleCanvasClick);
511
- closeInfoBtn.addEventListener('click', closeInfoPanel);
512
 
513
- function createUnits() {
514
- // Clear existing units
515
- units = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
 
517
- // Create red units (left side)
518
- for (let i = 0; i < 5; i++) {
519
- units.push(new Unit(i, 'red', 'infantry',
520
- 50 + Math.random() * 100, 100 + i * 100));
521
- }
522
- for (let i = 0; i < 3; i++) {
523
- units.push(new Unit(i + 5, 'red', 'tank',
524
- 100 + Math.random() * 100, 150 + i * 150));
525
- }
526
- for (let i = 0; i < 2; i++) {
527
- units.push(new Unit(i + 8, 'red', 'artillery',
528
- 50 + Math.random() * 80, 200 + i * 200));
529
- }
530
 
531
- // Create blue units (right side)
532
- for (let i = 0; i < 5; i++) {
533
- units.push(new Unit(i + 10, 'blue', 'infantry',
534
- 650 + Math.random() * 100, 100 + i * 100));
535
- }
536
- for (let i = 0; i < 3; i++) {
537
- units.push(new Unit(i + 15, 'blue', 'tank',
538
- 600 + Math.random() * 100, 150 + i * 150));
539
- }
540
- for (let i = 0; i < 2; i++) {
541
- units.push(new Unit(i + 18, 'blue', 'artillery',
542
- 670 + Math.random() * 80, 200 + i * 200));
543
- }
544
- }
545
-
546
- function updateGame() {
547
- // Update units
548
- for (const unit of units) {
549
- unit.update(units);
550
- }
551
 
552
- // Update display
553
- drawGame();
554
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
556
- function drawGame() {
557
- // Clear canvas
558
- ctx.clearRect(0, 0, canvas.width, canvas.height);
559
-
560
- // Draw background elements - territory markers
561
- drawTerritories();
562
-
563
- // Draw units
564
- for (const unit of units) {
565
- unit.draw(ctx);
566
- }
567
- }
568
 
569
- function drawTerritories() {
570
- // Draw red territory
571
- const redGradient = ctx.createLinearGradient(0, 0, 250, 0);
572
- redGradient.addColorStop(0, 'rgba(231, 76, 60, 0.2)');
573
- redGradient.addColorStop(1, 'rgba(231, 76, 60, 0.05)');
574
- ctx.fillStyle = redGradient;
575
- ctx.fillRect(0, 0, 250, canvas.height);
576
-
577
- // Draw blue territory
578
- const blueGradient = ctx.createLinearGradient(canvas.width - 250, 0, canvas.width, 0);
579
- blueGradient.addColorStop(0, 'rgba(52, 152, 219, 0.05)');
580
- blueGradient.addColorStop(1, 'rgba(52, 152, 219, 0.2)');
581
- ctx.fillStyle = blueGradient;
582
- ctx.fillRect(canvas.width - 250, 0, 250, canvas.height);
583
- }
 
584
 
585
- function handleCanvasClick(event) {
586
- const rect = canvas.getBoundingClientRect();
587
- const x = event.clientX - rect.left;
588
- const y = event.clientY - rect.top;
589
-
590
- // Check if a unit was clicked
591
- selectedUnit = null;
592
- for (const unit of units) {
593
- if (unit.isPointInside(x, y)) {
594
- selectedUnit = unit;
595
- break;
596
  }
597
- }
598
-
599
- if (selectedUnit) {
600
- showUnitInfo(selectedUnit);
601
- } else {
602
- closeInfoPanel();
603
- }
604
- }
605
-
606
- function showUnitInfo(unit) {
607
- const info = unit.getInfoText();
608
 
609
- document.getElementById('unit-name').textContent = info.name;
610
- document.getElementById('unit-type').querySelector('span').textContent = info.type;
611
- document.getElementById('unit-health').querySelector('span').textContent = info.health;
612
- document.getElementById('unit-attack').querySelector('span').textContent = info.attack;
613
- document.getElementById('unit-defense').querySelector('span').textContent = info.defense;
614
- document.getElementById('unit-status').querySelector('span').textContent = info.status;
615
-
616
- infoPanel.classList.remove('hidden');
617
- }
618
-
619
- function closeInfoPanel() {
620
- infoPanel.classList.add('hidden');
621
- selectedUnit = null;
622
- }
623
- });
624
-
625
- // Quick fix for the undefined variable in the code
626
- const Ω = 8; // Infantry attack power
627
  </script>
628
- </body>
629
- </html>
630
- """
631
 
632
- # Display the app in a fullscreen iframe
633
- st.components.v1.html(html_content, height=750, scrolling=False)
634
-
635
- # Add some additional info in sidebar
636
- with st.sidebar:
637
- st.header("Battle Simulator")
638
- st.markdown("""
639
- This simulation shows red and blue forces battling on a map.
640
-
641
- **Unit Types:**
642
- - ⭕ Infantry (Circles): Fast but weaker units
643
- - ⬛ Tanks (Squares): Strong with high defense
644
- - △ Artillery (Triangles): Powerful long-range attacks
645
-
646
- **Instructions:**
647
- - Click on any unit to see its stats
648
- - Watch as units automatically engage in combat
649
- """)
650
-
651
- # Debug information
652
- if st.checkbox("Show Debug Info"):
653
- st.write(f"Working directory: {os.getcwd()}")
654
- st.write(f"Files available: {', '.join(os.listdir('.'))}")
 
1
  import streamlit as st
2
+ import numpy as np
3
+ import pandas as pd
4
+ import time
5
+ import random
6
+ from PIL import Image, ImageDraw
7
+ import math
8
+ import uuid
9
+ import base64
10
+ from io import BytesIO
11
 
12
+ # Set page config
13
  st.set_page_config(
14
  page_title="Battle Simulator",
15
  page_icon="🎮",
16
+ layout="wide",
17
+ initial_sidebar_state="expanded"
18
  )
19
 
20
+ # Constants
21
+ MAP_WIDTH = 800
22
+ MAP_HEIGHT = 600
23
+ TEAM_RED = "red"
24
+ TEAM_BLUE = "blue"
25
+ UNIT_TYPES = ["infantry", "tank", "artillery"]
26
+ TEAM_COLORS = {
27
+ TEAM_RED: "#e74c3c",
28
+ TEAM_BLUE: "#3498db"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
+ UNIT_STATS = {
31
+ "infantry": {
32
+ "max_health": 80,
33
+ "attack": 8,
34
+ "defense": 3,
35
+ "range": 40,
36
+ "speed": 1.5,
37
+ "size": 10,
38
+ "shape": "circle",
39
+ "respawn_time": 120
40
+ },
41
+ "tank": {
42
+ "max_health": 150,
43
+ "attack": 15,
44
+ "defense": 10,
45
+ "range": 60,
46
+ "speed": 0.8,
47
+ "size": 12,
48
+ "shape": "square",
49
+ "respawn_time": 240
50
+ },
51
+ "artillery": {
52
+ "max_health": 60,
53
+ "attack": 25,
54
+ "defense": 2,
55
+ "range": 120,
56
+ "speed": 0.5,
57
+ "size": 13,
58
+ "shape": "triangle",
59
+ "respawn_time": 180
60
+ }
61
  }
62
 
63
+ # Utility functions
64
+ def calculate_distance(x1, y1, x2, y2):
65
+ """Calculate Euclidean distance between two points"""
66
+ return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
 
67
 
68
+ def angle_between(x1, y1, x2, y2):
69
+ """Calculate angle between two points in radians"""
70
+ return math.atan2(y2 - y1, x2 - x1)
71
 
72
+ class Unit:
73
+ def __init__(self, id, team, unit_type, x, y):
74
+ self.id = id
75
+ self.team = team
76
+ self.type = unit_type
77
+ self.x = x
78
+ self.y = y
79
+ self.target_x = x
80
+ self.target_y = y
81
+
82
+ # Get stats from the unit type
83
+ stats = UNIT_STATS[unit_type]
84
+ self.max_health = stats["max_health"]
85
+ self.health = self.max_health
86
+ self.attack = stats["attack"]
87
+ self.defense = stats["defense"]
88
+ self.range = stats["range"]
89
+ self.speed = stats["speed"]
90
+ self.size = stats["size"]
91
+ self.shape = stats["shape"]
92
+ self.respawn_time = stats["respawn_time"]
93
+
94
+ # Battle state
95
+ self.state = "idle" # idle, moving, attacking, dead
96
+ self.target = None
97
+ self.dead_timer = 0
98
+ self.attack_cooldown = 0
99
+ self.attack_cooldown_max = 60
100
+ self.attack_line = None
101
 
102
+ def find_target(self, units):
103
+ """Find nearest enemy unit"""
104
+ if self.state == "dead":
105
+ return
 
 
 
 
 
 
 
 
 
 
106
 
107
+ nearest_dist = float("inf")
108
+ nearest_enemy = None
 
 
 
 
 
 
 
 
109
 
110
+ for unit in units:
111
+ if unit.team != self.team and unit.state != "dead":
112
+ dist = calculate_distance(self.x, self.y, unit.x, unit.y)
113
+ if dist < nearest_dist:
114
+ nearest_dist = dist
115
+ nearest_enemy = unit
116
 
117
+ self.target = nearest_enemy
 
 
 
 
118
 
119
+ def update(self, units):
120
+ """Update unit state and position"""
121
+ if self.state == "dead":
122
+ self.dead_timer += 1
123
+ if self.dead_timer >= self.respawn_time:
124
+ self.respawn()
125
+ return
 
 
 
126
 
127
+ # Find target if no current target or target is dead
128
+ if not self.target or self.target.state == "dead":
129
+ self.find_target(units)
130
+ self.state = "idle"
131
 
132
+ # If we have a target, pursue it
133
+ if self.target:
134
+ dist_to_target = calculate_distance(self.x, self.y, self.target.x, self.target.y)
135
+
136
+ # If in attack range, attack
137
+ if dist_to_target <= self.range:
138
+ self.attack_target()
139
+ self.state = "attacking"
140
+ else:
141
+ # Move toward target
142
+ self.target_x = self.target.x
143
+ self.target_y = self.target.y
144
+ self.state = "moving"
145
+ else:
146
+ # Wander if no target
147
+ if random.random() < 0.02:
148
+ team_zone = 300 if self.team == TEAM_RED else 500
149
+ center_x = 200 if self.team == TEAM_RED else 600
150
+
151
+ self.target_x = max(50, min(MAP_WIDTH - 50,
152
+ center_x + (random.random() - 0.5) * team_zone))
153
+ self.target_y = max(50, min(MAP_HEIGHT - 50,
154
+ 300 + (random.random() - 0.5) * 400))
155
 
156
+ # Move toward target position
157
+ self.move()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ # Update attack cooldown
160
+ if self.attack_cooldown > 0:
161
+ self.attack_cooldown -= 1
 
 
 
 
 
 
 
 
 
162
 
163
+ # Update attack line
164
+ if self.attack_line:
165
+ self.attack_line["duration"] -= 1
166
+ if self.attack_line["duration"] <= 0:
167
+ self.attack_line = None
168
+
169
+ def move(self):
170
+ """Move unit toward target position"""
171
+ if self.state == "dead":
172
+ return
173
+
174
+ dx = self.target_x - self.x
175
+ dy = self.target_y - self.y
176
+ dist = math.sqrt(dx*dx + dy*dy)
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ if dist > 5:
179
+ self.x += (dx / dist) * self.speed
180
+ self.y += (dy / dist) * self.speed
181
 
182
+ # Keep units within map bounds
183
+ self.x = max(self.size, min(MAP_WIDTH - self.size, self.x))
184
+ self.y = max(self.size, min(MAP_HEIGHT - self.size, self.y))
185
+
186
+ def attack_target(self):
187
+ """Attack the current target"""
188
+ if self.attack_cooldown == 0 and self.target and self.target.state != "dead":
189
+ # Apply random variance to attack damage
190
+ damage_multiplier = 0.8 + random.random() * 0.4
191
+ base_damage = self.attack * damage_multiplier
192
+ final_damage = max(1, base_damage - self.target.defense / 2)
193
+
194
+ # Apply damage to target
195
+ self.target.take_damage(final_damage)
196
 
197
+ # Create attack line for visualization
198
+ self.attack_line = {
199
+ "from_x": self.x,
200
+ "from_y": self.y,
201
+ "to_x": self.target.x,
202
+ "to_y": self.target.y,
203
+ "duration": 10
204
  }
 
 
 
 
 
 
 
 
205
 
206
+ # Reset cooldown
207
+ self.attack_cooldown = self.attack_cooldown_max
208
+
209
+ def take_damage(self, amount):
210
+ """Take damage from an attack"""
211
+ self.health -= amount
212
+ if self.health <= 0:
213
+ self.die()
214
+
215
+ def die(self):
216
+ """Unit death"""
217
+ self.state = "dead"
218
+ self.health = 0
219
+ self.dead_timer = 0
220
+ self.attack_line = None
221
+
222
+ def respawn(self):
223
+ """Respawn the unit"""
224
+ margin = 50
225
+ if self.team == TEAM_RED:
226
+ self.x = margin + random.random() * 100
227
+ else:
228
+ self.x = MAP_WIDTH - margin - random.random() * 100
229
+
230
+ self.y = 100 + random.random() * 400
231
+ self.health = self.max_health
232
+ self.state = "idle"
233
+ self.target = None
234
+ self.attack_line = None
235
+ self.dead_timer = 0
236
+ self.attack_cooldown = 0
237
+
238
+ def is_point_inside(self, point_x, point_y):
239
+ """Check if a point is inside the unit"""
240
+ if self.state == "dead":
241
+ return False
242
 
243
+ return calculate_distance(self.x, self.y, point_x, point_y) <= self.size
244
+
245
+ class BattleSimulation:
246
+ def __init__(self):
247
+ self.units = []
248
+ self.selected_unit = None
249
+ self.frame_count = 0
250
+
251
+ # Create initial units
252
+ self.create_units()
253
+
254
+ def create_units(self):
255
+ """Create initial units for both teams"""
256
+ # Create red team (left side)
257
+ for i in range(5):
258
+ self.units.append(Unit(
259
+ id=f"red-infantry-{i}",
260
+ team=TEAM_RED,
261
+ unit_type="infantry",
262
+ x=50 + random.random() * 100,
263
+ y=100 + i * 100
264
+ ))
265
 
266
+ for i in range(3):
267
+ self.units.append(Unit(
268
+ id=f"red-tank-{i}",
269
+ team=TEAM_RED,
270
+ unit_type="tank",
271
+ x=100 + random.random() * 100,
272
+ y=150 + i * 150
273
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ for i in range(2):
276
+ self.units.append(Unit(
277
+ id=f"red-artillery-{i}",
278
+ team=TEAM_RED,
279
+ unit_type="artillery",
280
+ x=50 + random.random() * 80,
281
+ y=200 + i * 200
282
+ ))
283
+
284
+ # Create blue team (right side)
285
+ for i in range(5):
286
+ self.units.append(Unit(
287
+ id=f"blue-infantry-{i}",
288
+ team=TEAM_BLUE,
289
+ unit_type="infantry",
290
+ x=650 + random.random() * 100,
291
+ y=100 + i * 100
292
+ ))
293
 
294
+ for i in range(3):
295
+ self.units.append(Unit(
296
+ id=f"blue-tank-{i}",
297
+ team=TEAM_BLUE,
298
+ unit_type="tank",
299
+ x=600 + random.random() * 100,
300
+ y=150 + i * 150
301
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
+ for i in range(2):
304
+ self.units.append(Unit(
305
+ id=f"blue-artillery-{i}",
306
+ team=TEAM_BLUE,
307
+ unit_type="artillery",
308
+ x=670 + random.random() * 80,
309
+ y=200 + i * 200
310
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ def update(self):
313
+ """Update all units in the simulation"""
314
+ for unit in self.units:
315
+ unit.update(self.units)
316
+
317
+ self.frame_count += 1
318
+
319
+ def render(self):
320
+ """Render the current state of the battle"""
321
+ # Create a new image
322
+ img = Image.new('RGBA', (MAP_WIDTH, MAP_HEIGHT), (232, 244, 229, 255))
323
+ draw = ImageDraw.Draw(img)
324
+
325
+ # Draw territory gradients
326
+ # (This is approximate as PIL doesn't have easy gradients)
327
+ # Red territory
328
+ for x in range(250):
329
+ alpha = int(50 * (1 - x / 250))
330
+ draw.line([(x, 0), (x, MAP_HEIGHT)], fill=(231, 76, 60, alpha), width=1)
331
+
332
+ # Blue territory
333
+ for x in range(MAP_WIDTH - 250, MAP_WIDTH):
334
+ alpha = int(50 * (x - (MAP_WIDTH - 250)) / 250)
335
+ draw.line([(x, 0), (x, MAP_HEIGHT)], fill=(52, 152, 219, alpha), width=1)
336
+
337
+ # Draw attack lines first (so they appear behind units)
338
+ for unit in self.units:
339
+ if unit.attack_line:
340
+ line_color = TEAM_COLORS[unit.team]
341
+ draw.line(
342
+ [(unit.attack_line["from_x"], unit.attack_line["from_y"]),
343
+ (unit.attack_line["to_x"], unit.attack_line["to_y"])],
344
+ fill=line_color,
345
+ width=2
346
+ )
347
+
348
+ # Draw units
349
+ for unit in self.units:
350
+ if unit.state == "dead":
351
+ continue
352
+
353
+ # Draw the unit shape
354
+ fill_color = TEAM_COLORS[unit.team]
355
+
356
+ if unit.shape == "circle": # Infantry
357
+ draw.ellipse(
358
+ [(unit.x - unit.size, unit.y - unit.size),
359
+ (unit.x + unit.size, unit.y + unit.size)],
360
+ fill=fill_color
361
+ )
362
+ elif unit.shape == "square": # Tank
363
+ draw.rectangle(
364
+ [(unit.x - unit.size, unit.y - unit.size),
365
+ (unit.x + unit.size, unit.y + unit.size)],
366
+ fill=fill_color
367
+ )
368
+ elif unit.shape == "triangle": # Artillery
369
+ draw.polygon(
370
+ [(unit.x, unit.y - unit.size),
371
+ (unit.x - unit.size, unit.y + unit.size),
372
+ (unit.x + unit.size, unit.y + unit.size)],
373
+ fill=fill_color
374
+ )
375
+
376
+ # Draw health bar
377
+ health_ratio = unit.health / unit.max_health
378
+ health_bar_width = 20
379
+ health_bar_height = 3
380
+
381
+ # Health bar background (red)
382
+ draw.rectangle(
383
+ [(unit.x - health_bar_width/2, unit.y - 20),
384
+ (unit.x + health_bar_width/2, unit.y - 20 + health_bar_height)],
385
+ fill="#e74c3c"
386
+ )
387
+
388
+ # Health bar foreground (green)
389
+ draw.rectangle(
390
+ [(unit.x - health_bar_width/2, unit.y - 20),
391
+ (unit.x - health_bar_width/2 + health_bar_width * health_ratio, unit.y - 20 + health_bar_height)],
392
+ fill="#2ecc71"
393
+ )
394
+
395
+ # Draw state indicator
396
+ state_colors = {
397
+ "idle": "#95a5a6", # Gray
398
+ "moving": "#3498db", # Blue
399
+ "attacking": "#e74c3c" # Red
400
+ }
401
+
402
+ draw.ellipse(
403
+ [(unit.x - 3, unit.y - 25 - 3),
404
+ (unit.x + 3, unit.y - 25 + 3)],
405
+ fill=state_colors.get(unit.state, "#95a5a6")
406
+ )
407
+
408
+ # Highlight selected unit
409
+ if self.selected_unit and unit.id == self.selected_unit.id:
410
+ draw.ellipse(
411
+ [(unit.x - unit.size - 5, unit.y - unit.size - 5),
412
+ (unit.x + unit.size + 5, unit.y + unit.size + 5)],
413
+ outline="#f1c40f",
414
+ width=2
415
+ )
416
+
417
+ # Convert the image to base64 for displaying in Streamlit
418
+ buffered = BytesIO()
419
+ img.save(buffered, format="PNG")
420
+ img_str = base64.b64encode(buffered.getvalue()).decode()
421
+
422
+ return img_str
423
+
424
+ def handle_click(self, x, y):
425
+ """Handle click on the battle map"""
426
+ self.selected_unit = None
427
+ for unit in self.units:
428
+ if unit.is_point_inside(x, y) and unit.state != "dead":
429
+ self.selected_unit = unit
430
+ break
431
+
432
+ return self.selected_unit
433
 
434
+ def get_unit_info_html(unit):
435
+ """Generate HTML for unit info panel"""
436
+ if not unit:
437
+ return ""
438
+
439
+ type_icon = {
440
+ "infantry": "⭕",
441
+ "tank": "⬛",
442
+ "artillery": "△"
443
+ }.get(unit.type, "")
444
 
445
+ team_color = TEAM_COLORS[unit.team]
 
446
 
447
+ html = f"""
448
+ <div style="background-color: rgba(255, 255, 255, 0.95); padding: 20px;
449
+ border-radius: 8px; border: 2px solid {team_color}; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);">
450
+ <h3 style="margin-bottom: 15px; color: #333; border-bottom: 1px solid #ddd; padding-bottom: 10px;">
451
+ {unit.team.capitalize()} {unit.type.capitalize()} {type_icon}
452
+ </h3>
453
+ <p style="margin-bottom: 10px; display: flex; justify-content: space-between;">
454
+ <span>Health:</span> <span>{int(unit.health)}/{unit.max_health}</span>
455
+ </p>
456
+ <p style="margin-bottom: 10px; display: flex; justify-content: space-between;">
457
+ <span>Attack:</span> <span>{unit.attack}</span>
458
+ </p>
459
+ <p style="margin-bottom: 10px; display: flex; justify-content: space-between;">
460
+ <span>Defense:</span> <span>{unit.defense}</span>
461
+ </p>
462
+ <p style="margin-bottom: 10px; display: flex; justify-content: space-between;">
463
+ <span>Range:</span> <span>{unit.range}</span>
464
+ </p>
465
+ <p style="margin-bottom: 10px; display: flex; justify-content: space-between;">
466
+ <span>Speed:</span> <span>{unit.speed}</span>
467
+ </p>
468
+ <p style="margin-bottom: 10px; display: flex; justify-content: space-between;">
469
+ <span>Status:</span> <span>{unit.state.capitalize()}</span>
470
+ </p>
471
+ </div>
472
+ """
473
+ return html
474
+
475
+ def main():
476
+ # Initialize session state
477
+ if 'battle_sim' not in st.session_state:
478
+ st.session_state.battle_sim = BattleSimulation()
479
+ st.session_state.last_click = None
480
 
481
+ # Title and description
482
+ st.title("Battle Simulator")
483
+ st.markdown("An interactive battle simulation where red and blue forces fight on a dynamic battlefield.")
484
 
485
+ # Create a layout with columns
486
+ col1, col2 = st.columns([3, 1])
 
487
 
488
+ with col1:
489
+ # Display battle map with click handler
490
+ battle_sim = st.session_state.battle_sim
491
+ battle_sim.update() # Update simulation
492
+ img_str = battle_sim.render() # Render the current state
493
+
494
+ # Display the battle map with click handling
495
+ st.markdown(f"""
496
+ <div style="position: relative; width: {MAP_WIDTH}px; margin: 0 auto;">
497
+ <img src="data:image/png;base64,{img_str}" width="{MAP_WIDTH}" id="battlemap"
498
+ style="border: 1px solid #ccc; cursor: pointer;" onclick="handleMapClick(event)">
499
+ </div>
500
+ <script>
501
+ function handleMapClick(e) {
502
+ const rect = e.target.getBoundingClientRect();
503
+ const x = e.clientX - rect.left;
504
+ const y = e.clientY - rect.top;
505
+ // Pass click coordinates to Streamlit
506
+ window.parent.postMessage({{
507
+ type: "streamlit:setComponentValue",
508
+ value: [x, y]
509
+ }}, "*");
510
+ }
511
+ </script>
512
+ """, unsafe_allow_html=True)
513
 
514
+ # Use a streamlit empty element to trigger rerun on map click
515
+ map_click = st.empty()
 
 
 
 
 
 
 
 
 
 
 
516
 
517
+ with col2:
518
+ # Show information about selected unit
519
+ st.markdown("### Unit Information")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
 
521
+ unit_info = st.empty()
522
+ if battle_sim.selected_unit:
523
+ unit_info.markdown(get_unit_info_html(battle_sim.selected_unit), unsafe_allow_html=True)
524
+ else:
525
+ unit_info.markdown("Click on a unit to see its details")
526
+
527
+ # Unit legend
528
+ st.markdown("### Unit Types")
529
+ st.markdown("""
530
+ - ⭕ **Infantry**: Fast but weaker units
531
+ - ⬛ **Tank**: Slow but powerful with strong defense
532
+ - △ **Artillery**: Long-range attacks but vulnerable
533
+ """)
534
+
535
+ # Battle statistics
536
+ st.markdown("### Battle Statistics")
537
+
538
+ # Count units by team and status
539
+ red_active = sum(1 for unit in battle_sim.units if unit.team == TEAM_RED and unit.state != "dead")
540
+ blue_active = sum(1 for unit in battle_sim.units if unit.team == TEAM_BLUE and unit.state != "dead")
541
+
542
+ # Display counters
543
+ st.markdown(f"""
544
+ <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
545
+ <div style="text-align: center; padding: 10px; background-color: rgba(231, 76, 60, 0.2); border-radius: 5px; width: 45%;">
546
+ <div style="font-size: 24px; font-weight: bold;">{red_active}</div>
547
+ <div>Red Forces</div>
548
+ </div>
549
+ <div style="text-align: center; padding: 10px; background-color: rgba(52, 152, 219, 0.2); border-radius: 5px; width: 45%;">
550
+ <div style="font-size: 24px; font-weight: bold;">{blue_active}</div>
551
+ <div>Blue Forces</div>
552
+ </div>
553
+ </div>
554
+ """, unsafe_allow_html=True)
555
 
556
+ # Handle map clicks
557
+ clicked = st.button('Refresh Battle State', key='refresh_battle')
 
 
 
 
 
 
 
 
 
 
558
 
559
+ # Create a container for receiving click data and auto-refreshing
560
+ click_container = st.container()
561
+ with click_container:
562
+ click_data = st.text_input("Click coordinates",
563
+ value="",
564
+ label_visibility="collapsed",
565
+ key="click_data")
566
+
567
+ if click_data:
568
+ try:
569
+ x, y = map(float, click_data.strip('[]').split(','))
570
+ selected = battle_sim.handle_click(x, y)
571
+ st.session_state.last_click = (x, y)
572
+ st.experimental_rerun()
573
+ except:
574
+ pass
575
 
576
+ # Auto-refresh the page
577
+ st.markdown("""
578
+ <script>
579
+ // Set up to receive messages from the iframe
580
+ window.addEventListener('message', function(e) {
581
+ if (e.data.type === 'streamlit:setComponentValue') {
582
+ const coordinates = e.data.value;
583
+ document.querySelector('input[key="click_data"]').value = coordinates;
584
+ document.querySelector('input[key="click_data"]').dispatchEvent(new Event('input'));
 
 
585
  }
586
+ });
 
 
 
 
 
 
 
 
 
 
587
 
588
+ // Auto-refresh the simulation every 1 second
589
+ const refreshInterval = setInterval(function() {
590
+ const refreshButton = document.querySelector('button[key="refresh_battle"]');
591
+ if (refreshButton) {
592
+ refreshButton.click();
593
+ }
594
+ }, 1000);
 
 
 
 
 
 
 
 
 
 
 
595
  </script>
596
+ """, unsafe_allow_html=True)
 
 
597
 
598
+ if __name__ == "__main__":
599
+ main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docker-compose.yml CHANGED
@@ -4,15 +4,14 @@ services:
4
  battle-simulator:
5
  build: .
6
  ports:
7
- - "8080:8080"
8
  volumes:
9
- - ./index.html:/usr/share/nginx/html/index.html
10
- - ./styles.css:/usr/share/nginx/html/styles.css
11
- - ./script.js:/usr/share/nginx/html/script.js
12
- - ./nginx.conf:/etc/nginx/nginx.conf
13
- # Add read-only flag to prevent write attempts to read-only filesystem in HF Spaces
14
- read_only: false
15
- # Specific settings for Hugging Face Spaces or similar restricted environments
16
  environment:
17
- - NGINX_ENTRYPOINT_QUIET_LOGS=1
18
- - NGINX_WORKER_PROCESSES=1
 
 
 
 
 
4
  battle-simulator:
5
  build: .
6
  ports:
7
+ - "8501:8501"
8
  volumes:
9
+ - ./app.py:/app/app.py
10
+ - ./requirements.txt:/app/requirements.txt
 
 
 
 
 
11
  environment:
12
+ - STREAMLIT_SERVER_PORT=8501
13
+ - STREAMLIT_SERVER_HEADLESS=true
14
+ - STREAMLIT_SERVER_ENABLE_CORS=false
15
+ - PYTHONUNBUFFERED=1
16
+ # Restart policy
17
+ restart: unless-stopped
healthcheck.html DELETED
@@ -1,23 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Battle Simulator Health Check</title>
5
- <meta charset="UTF-8">
6
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
- <!-- Special meta tags for Hugging Face Spaces -->
9
- <meta property="og:title" content="Battle Simulator">
10
- <meta property="og:description" content="Interactive battle simulation with units on a map">
11
- </head>
12
- <body>
13
- <h1>Battle Simulator is running!</h1>
14
- <p>Status: OK</p>
15
- <p>If you're seeing this page directly, please click on the "App" tab to view the simulation.</p>
16
- <script>
17
- // Redirect to index.html if accessed directly
18
- if (window.location.pathname === '/healthcheck.html') {
19
- window.location.href = '/';
20
- }
21
- </script>
22
- </body>
23
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html DELETED
@@ -1,27 +0,0 @@
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>Battle Simulator</title>
7
- <link rel="stylesheet" href="styles.css">
8
- </head>
9
- <body>
10
- <div class="container">
11
- <h1>Battle Simulator</h1>
12
- <div class="battlefield">
13
- <canvas id="map" width="800" height="600"></canvas>
14
- <div id="info-panel" class="hidden">
15
- <h3 id="unit-name">Unit Name</h3>
16
- <p id="unit-type">Type: <span></span></p>
17
- <p id="unit-health">Health: <span></span></p>
18
- <p id="unit-attack">Attack: <span></span></p>
19
- <p id="unit-defense">Defense: <span></span></p>
20
- <p id="unit-status">Status: <span></span></p>
21
- <button id="close-info">Close</button>
22
- </div>
23
- </div>
24
- </div>
25
- <script src="script.js"></script>
26
- </body>
27
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
interface.py DELETED
@@ -1,46 +0,0 @@
1
- import gradio as gr
2
- import os
3
- import http.server
4
- import socketserver
5
- import threading
6
- import webbrowser
7
-
8
- # Define constants
9
- PORT = int(os.environ.get("PORT", 8080))
10
- HOST = "0.0.0.0" # Listen on all interfaces
11
- DIRECTORY = os.path.dirname(os.path.abspath(__file__))
12
-
13
- # Start HTTP server in a separate thread
14
- def start_http_server():
15
- os.chdir(DIRECTORY)
16
- handler = http.server.SimpleHTTPRequestHandler
17
- httpd = socketserver.TCPServer((HOST, PORT), handler)
18
- print(f"Server started at http://{HOST}:{PORT}")
19
- print(f"Current directory: {DIRECTORY}")
20
- httpd.serve_forever()
21
-
22
- # Start server in a background thread
23
- server_thread = threading.Thread(target=start_http_server, daemon=True)
24
- server_thread.start()
25
-
26
- # Create a simple Gradio interface for Hugging Face
27
- with gr.Blocks() as demo:
28
- gr.HTML(f"""
29
- <iframe
30
- src="http://{HOST}:{PORT}"
31
- width="100%"
32
- height="800px"
33
- frameborder="0"
34
- style="border: 1px solid #ddd; border-radius: 8px;"
35
- ></iframe>
36
- <div style="text-align: center; margin-top: 10px; padding: 10px; background-color: #f8f8f8; border-radius: 5px;">
37
- <h3>Battle Simulator</h3>
38
- <p>If the simulation doesn't appear above, you can access it directly at:
39
- <a href="http://{HOST}:{PORT}" target="_blank">Battle Simulator</a>
40
- </p>
41
- </div>
42
- """)
43
-
44
- # Launch the interface
45
- if __name__ == "__main__":
46
- demo.launch(server_name=HOST, server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
nginx.conf DELETED
@@ -1,40 +0,0 @@
1
- worker_processes auto;
2
-
3
- error_log /dev/stderr warn;
4
- pid /tmp/nginx.pid;
5
-
6
- events {
7
- worker_connections 1024;
8
- }
9
-
10
- http {
11
- include /etc/nginx/mime.types;
12
- default_type application/octet-stream;
13
-
14
- # Use /tmp for all temp files (it's writable in Spaces)
15
- client_body_temp_path /tmp/client_temp;
16
- proxy_temp_path /tmp/proxy_temp;
17
- fastcgi_temp_path /tmp/fastcgi_temp;
18
- scgi_temp_path /tmp/scgi_temp;
19
- uwsgi_temp_path /tmp/uwsgi_temp;
20
-
21
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
22
- '$status $body_bytes_sent "$http_referer" '
23
- '"$http_user_agent" "$http_x_forwarded_for"';
24
-
25
- access_log /dev/stdout main;
26
-
27
- sendfile on;
28
- keepalive_timeout 65;
29
-
30
- # Basic server block on port 8080
31
- server {
32
- listen 8080;
33
- server_name localhost;
34
-
35
- location / {
36
- root /usr/share/nginx/html;
37
- index index.html;
38
- }
39
- }
40
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1 +1,4 @@
1
- streamlit>=1.22.0
 
 
 
 
1
+ streamlit>=1.22.0
2
+ numpy>=1.20.0
3
+ pandas>=1.3.0
4
+ pillow>=8.0.0
script.js DELETED
@@ -1,412 +0,0 @@
1
- // Initialize elements when the page loads
2
- let canvas, ctx;
3
- window.addEventListener('DOMContentLoaded', () => {
4
- canvas = document.getElementById('map');
5
- ctx = canvas.getContext('2d');
6
- initializeGame();
7
- });
8
-
9
- // Constants for unit types and teams
10
- const UNIT_TYPES = {
11
- INFANTRY: {
12
- name: 'Infantry',
13
- baseHealth: 100,
14
- baseAttack: 10,
15
- baseDefense: 5,
16
- speed: 1,
17
- attackRange: 50,
18
- radius: 12,
19
- color: '#3498db'
20
- },
21
- TANK: {
22
- name: 'Tank',
23
- baseHealth: 200,
24
- baseAttack: 20,
25
- baseDefense: 15,
26
- speed: 0.5,
27
- attackRange: 100,
28
- radius: 18,
29
- color: '#e74c3c'
30
- },
31
- ARTILLERY: {
32
- name: 'Artillery',
33
- baseHealth: 80,
34
- baseAttack: 30,
35
- baseDefense: 3,
36
- speed: 0.3,
37
- attackRange: 150,
38
- radius: 15,
39
- color: '#f39c12'
40
- }
41
- };
42
-
43
- const TEAMS = {
44
- RED: {
45
- name: 'Red Forces',
46
- primaryColor: '#e74c3c',
47
- secondaryColor: '#c0392b'
48
- },
49
- BLUE: {
50
- name: 'Blue Forces',
51
- primaryColor: '#3498db',
52
- secondaryColor: '#2980b9'
53
- }
54
- };
55
-
56
- // Class to represent a military unit
57
- class Unit {
58
- constructor(id, type, team, x, y) {
59
- this.id = id;
60
- this.type = type;
61
- this.team = team;
62
- this.x = x;
63
- this.y = y;
64
- this.targetX = x;
65
- this.targetY = y;
66
- this.health = type.baseHealth;
67
- this.maxHealth = type.baseHealth;
68
- this.attack = type.baseAttack;
69
- this.defense = type.baseDefense;
70
- this.radius = type.radius;
71
- this.speed = type.speed;
72
- this.attackRange = type.attackRange;
73
- this.attackCooldown = 0;
74
- this.target = null;
75
- this.selected = false;
76
- this.status = 'Idle';
77
- this.deadTimer = 0;
78
- this.isAlive = true;
79
- }
80
-
81
- // Update the unit's position and state
82
- update(units, deltaTime) {
83
- if (!this.isAlive) {
84
- this.deadTimer += deltaTime;
85
- if (this.deadTimer > 3000) {
86
- this.respawn();
87
- }
88
- return;
89
- }
90
-
91
- if (!this.target || !this.target.isAlive) {
92
- this.findNewTarget(units);
93
- }
94
-
95
- if (this.attackCooldown > 0) {
96
- this.attackCooldown -= deltaTime;
97
- }
98
-
99
- if (this.target) {
100
- // Calculate distance to target
101
- const dx = this.target.x - this.x;
102
- const dy = this.target.y - this.y;
103
- const distance = Math.sqrt(dx * dx + dy * dy);
104
-
105
- if (distance <= this.attackRange) {
106
- // Attack if in range
107
- this.status = 'Attacking';
108
- if (this.attackCooldown <= 0) {
109
- this.attackTarget();
110
- }
111
- } else {
112
- // Move towards target
113
- this.status = 'Moving';
114
- const angle = Math.atan2(dy, dx);
115
- const moveDistance = this.speed * deltaTime / 16;
116
-
117
- this.x += Math.cos(angle) * moveDistance;
118
- this.y += Math.sin(angle) * moveDistance;
119
- }
120
- } else {
121
- this.status = 'Idle';
122
- // Random wandering
123
- if (Math.random() < 0.01) {
124
- this.targetX = Math.random() * canvas.width;
125
- this.targetY = Math.random() * canvas.height;
126
- }
127
-
128
- // Move towards random target
129
- const dx = this.targetX - this.x;
130
- const dy = this.targetY - this.y;
131
- const distance = Math.sqrt(dx * dx + dy * dy);
132
-
133
- if (distance > 5) {
134
- const angle = Math.atan2(dy, dx);
135
- const moveDistance = this.speed * deltaTime / 32;
136
-
137
- this.x += Math.cos(angle) * moveDistance;
138
- this.y += Math.sin(angle) * moveDistance;
139
- }
140
- }
141
- }
142
-
143
- // Find a new target from the opposing team
144
- findNewTarget(units) {
145
- let closestDistance = Infinity;
146
- let closestTarget = null;
147
-
148
- for (const unit of units) {
149
- if (unit.team !== this.team && unit.isAlive) {
150
- const dx = unit.x - this.x;
151
- const dy = unit.y - this.y;
152
- const distance = Math.sqrt(dx * dx + dy * dy);
153
-
154
- if (distance < closestDistance) {
155
- closestDistance = distance;
156
- closestTarget = unit;
157
- }
158
- }
159
- }
160
-
161
- this.target = closestTarget;
162
- }
163
-
164
- // Attack the current target
165
- attackTarget() {
166
- if (!this.target || !this.target.isAlive) return;
167
-
168
- const damage = Math.max(1, this.attack - this.target.defense / 2);
169
- this.target.takeDamage(damage);
170
- this.attackCooldown = 1000; // 1 second cooldown
171
-
172
- // Visual feedback (particle effect could be added here)
173
- }
174
-
175
- // Take damage from an attack
176
- takeDamage(amount) {
177
- this.health -= amount;
178
- if (this.health <= 0) {
179
- this.die();
180
- }
181
- }
182
-
183
- // Die and set a timer for respawn
184
- die() {
185
- this.health = 0;
186
- this.isAlive = false;
187
- this.deadTimer = 0;
188
- this.status = 'Dead';
189
- }
190
-
191
- // Respawn the unit at a random position on its team's side
192
- respawn() {
193
- this.health = this.maxHealth;
194
- this.isAlive = true;
195
- this.status = 'Idle';
196
-
197
- // Respawn on team's side
198
- if (this.team === TEAMS.RED) {
199
- this.x = Math.random() * (canvas.width / 3);
200
- } else {
201
- this.x = canvas.width - Math.random() * (canvas.width / 3);
202
- }
203
- this.y = Math.random() * canvas.height;
204
-
205
- this.targetX = this.x;
206
- this.targetY = this.y;
207
- }
208
-
209
- // Draw the unit on the canvas
210
- draw(ctx) {
211
- const alpha = this.isAlive ? 1 : 0.3;
212
-
213
- // Draw health bar
214
- if (this.isAlive) {
215
- const healthPercent = this.health / this.maxHealth;
216
- ctx.fillStyle = `rgba(0, 0, 0, ${alpha * 0.2})`;
217
- ctx.fillRect(this.x - this.radius, this.y - this.radius - 10, this.radius * 2, 5);
218
-
219
- ctx.fillStyle = healthPercent > 0.5 ? `rgba(46, 204, 113, ${alpha})` :
220
- healthPercent > 0.2 ? `rgba(241, 196, 15, ${alpha})` :
221
- `rgba(231, 76, 60, ${alpha})`;
222
- ctx.fillRect(this.x - this.radius, this.y - this.radius - 10, this.radius * 2 * healthPercent, 5);
223
- }
224
-
225
- // Draw unit body
226
- ctx.beginPath();
227
- ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
228
- ctx.fillStyle = `rgba(${this.team === TEAMS.RED ? '231, 76, 60' : '52, 152, 219'}, ${alpha})`;
229
- ctx.fill();
230
-
231
- // Draw outline if selected
232
- if (this.selected) {
233
- ctx.strokeStyle = '#2ecc71';
234
- ctx.lineWidth = 2;
235
- ctx.stroke();
236
- }
237
-
238
- // Draw unit type indicator
239
- ctx.beginPath();
240
- if (this.type === UNIT_TYPES.INFANTRY) {
241
- ctx.moveTo(this.x - 5, this.y - 5);
242
- ctx.lineTo(this.x + 5, this.y - 5);
243
- ctx.lineTo(this.x, this.y + 5);
244
- ctx.closePath();
245
- } else if (this.type === UNIT_TYPES.TANK) {
246
- ctx.rect(this.x - 6, this.y - 6, 12, 12);
247
- } else if (this.type === UNIT_TYPES.ARTILLERY) {
248
- ctx.arc(this.x, this.y, 5, 0, Math.PI * 2);
249
- }
250
- ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.8})`;
251
- ctx.fill();
252
-
253
- // Draw attack line if attacking
254
- if (this.target && this.isAlive && this.target.isAlive && this.status === 'Attacking') {
255
- ctx.beginPath();
256
- ctx.moveTo(this.x, this.y);
257
- ctx.lineTo(this.target.x, this.target.y);
258
- ctx.strokeStyle = `rgba(255, 0, 0, ${alpha * 0.5})`;
259
- ctx.lineWidth = 1;
260
- ctx.stroke();
261
- }
262
- }
263
-
264
- // Check if a point is inside the unit (for click detection)
265
- containsPoint(x, y) {
266
- const dx = this.x - x;
267
- const dy = this.y - y;
268
- return dx * dx + dy * dy <= this.radius * this.radius;
269
- }
270
- }
271
-
272
- // Create units for both teams
273
- let units = [];
274
- let selectedUnit = null;
275
- let lastTime = 0;
276
-
277
- function initializeUnits() {
278
- units = [];
279
-
280
- // Create Red team units
281
- createUnits(10, UNIT_TYPES.INFANTRY, TEAMS.RED, 0, canvas.width / 3, 0, canvas.height);
282
- createUnits(5, UNIT_TYPES.TANK, TEAMS.RED, 0, canvas.width / 3, 0, canvas.height);
283
- createUnits(3, UNIT_TYPES.ARTILLERY, TEAMS.RED, 0, canvas.width / 3, 0, canvas.height);
284
-
285
- // Create Blue team units
286
- createUnits(10, UNIT_TYPES.INFANTRY, TEAMS.BLUE, canvas.width * 2/3, canvas.width, 0, canvas.height);
287
- createUnits(5, UNIT_TYPES.TANK, TEAMS.BLUE, canvas.width * 2/3, canvas.width, 0, canvas.height);
288
- createUnits(3, UNIT_TYPES.ARTILLERY, TEAMS.BLUE, canvas.width * 2/3, canvas.width, 0, canvas.height);
289
- }
290
-
291
- function createUnits(count, type, team, minX, maxX, minY, maxY) {
292
- for (let i = 0; i < count; i++) {
293
- const x = minX + Math.random() * (maxX - minX);
294
- const y = minY + Math.random() * (maxY - minY);
295
- const unit = new Unit(units.length, type, team, x, y);
296
- units.push(unit);
297
- }
298
- }
299
-
300
- // Main game loop
301
- function gameLoop(currentTime) {
302
- if (!lastTime) lastTime = currentTime;
303
- const deltaTime = currentTime - lastTime;
304
- lastTime = currentTime;
305
-
306
- // Clear the canvas
307
- ctx.clearRect(0, 0, canvas.width, canvas.height);
308
-
309
- // Update and draw all units
310
- for (const unit of units) {
311
- unit.update(units, deltaTime);
312
- }
313
-
314
- // Draw in two passes to ensure selected units appear on top
315
- // First draw non-selected units
316
- for (const unit of units) {
317
- if (!unit.selected) {
318
- unit.draw(ctx);
319
- }
320
- }
321
-
322
- // Then draw selected units
323
- for (const unit of units) {
324
- if (unit.selected) {
325
- unit.draw(ctx);
326
- }
327
- }
328
-
329
- // Continue the game loop
330
- requestAnimationFrame(gameLoop);
331
- }
332
-
333
- // Handle clicks on units
334
- function handleCanvasClick(event) {
335
- const rect = canvas.getBoundingClientRect();
336
- const x = event.clientX - rect.left;
337
- const y = event.clientY - rect.top;
338
-
339
- console.log(`Click at coordinates: (${x}, ${y})`);
340
-
341
- // Deselect any previously selected unit
342
- if (selectedUnit) {
343
- selectedUnit.selected = false;
344
- selectedUnit = null;
345
-
346
- // Make sure the info panel is hidden when deselecting
347
- document.getElementById('info-panel').classList.add('hidden');
348
- }
349
-
350
- // Check if a unit was clicked
351
- let unitClicked = false;
352
-
353
- for (let i = units.length - 1; i >= 0; i--) {
354
- const unit = units[i];
355
- if (unit.containsPoint(x, y)) {
356
- console.log(`Unit clicked: ${unit.team.name} ${unit.type.name} (${unit.id})`);
357
- selectedUnit = unit;
358
- unit.selected = true;
359
- unitClicked = true;
360
-
361
- // Update info panel with unit details
362
- const nameElement = document.getElementById('unit-name');
363
- const typeElement = document.getElementById('unit-type').querySelector('span');
364
- const healthElement = document.getElementById('unit-health').querySelector('span');
365
- const attackElement = document.getElementById('unit-attack').querySelector('span');
366
- const defenseElement = document.getElementById('unit-defense').querySelector('span');
367
- const statusElement = document.getElementById('unit-status').querySelector('span');
368
-
369
- nameElement.textContent = `${unit.team.name} ${unit.type.name}`;
370
- typeElement.textContent = unit.type.name;
371
- healthElement.textContent = `${Math.max(0, Math.floor(unit.health))} / ${unit.maxHealth}`;
372
- attackElement.textContent = unit.attack;
373
- defenseElement.textContent = unit.defense;
374
- statusElement.textContent = unit.status;
375
-
376
- // Set team color for the unit name
377
- nameElement.style.color = unit.team.primaryColor;
378
-
379
- // Show the info panel
380
- const infoPanel = document.getElementById('info-panel');
381
- infoPanel.classList.remove('hidden');
382
-
383
- break;
384
- }
385
- }
386
-
387
- if (!unitClicked) {
388
- console.log("No unit clicked");
389
- }
390
- }
391
-
392
- // Initialize the game
393
- function initializeGame() {
394
- // Initialize game components
395
- initializeUnits();
396
-
397
- // Add event listeners
398
- canvas.addEventListener('click', handleCanvasClick);
399
-
400
- document.getElementById('close-info').addEventListener('click', () => {
401
- document.getElementById('info-panel').classList.add('hidden');
402
- if (selectedUnit) {
403
- selectedUnit.selected = false;
404
- selectedUnit = null;
405
- }
406
- });
407
-
408
- // Start the game loop
409
- requestAnimationFrame(gameLoop);
410
-
411
- console.log("Battle simulator initialized successfully");
412
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
styles.css DELETED
@@ -1,95 +0,0 @@
1
- * {
2
- margin: 0;
3
- padding: 0;
4
- box-sizing: border-box;
5
- }
6
-
7
- body {
8
- font-family: Arial, sans-serif;
9
- background-color: #f0f0f0;
10
- padding: 20px;
11
- }
12
-
13
- .container {
14
- max-width: 1000px;
15
- margin: 0 auto;
16
- background-color: white;
17
- padding: 20px;
18
- border-radius: 10px;
19
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
20
- }
21
-
22
- h1 {
23
- text-align: center;
24
- margin-bottom: 20px;
25
- color: #333;
26
- }
27
-
28
- .battlefield {
29
- position: relative;
30
- }
31
-
32
- canvas {
33
- display: block;
34
- margin: 0 auto;
35
- border: 1px solid #ccc;
36
- background-color: #e8f4e5;
37
- }
38
-
39
- #info-panel {
40
- position: absolute;
41
- top: 20px;
42
- right: 20px;
43
- background-color: rgba(255, 255, 255, 0.95);
44
- padding: 20px;
45
- border-radius: 8px;
46
- border: 2px solid #4a6ea9;
47
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
48
- width: 300px;
49
- z-index: 10;
50
- transition: all 0.3s ease;
51
- animation: fadeIn 0.3s ease-out;
52
- }
53
-
54
- @keyframes fadeIn {
55
- from {
56
- opacity: 0;
57
- transform: translateY(-10px);
58
- }
59
- to {
60
- opacity: 1;
61
- transform: translateY(0);
62
- }
63
- }
64
-
65
- #info-panel h3 {
66
- margin-bottom: 15px;
67
- color: #333;
68
- border-bottom: 1px solid #ddd;
69
- padding-bottom: 10px;
70
- }
71
-
72
- #info-panel p {
73
- margin-bottom: 10px;
74
- display: flex;
75
- justify-content: space-between;
76
- }
77
-
78
- #close-info {
79
- display: block;
80
- margin: 20px auto 0;
81
- padding: 8px 16px;
82
- background-color: #4a6ea9;
83
- color: white;
84
- border: none;
85
- border-radius: 4px;
86
- cursor: pointer;
87
- }
88
-
89
- #close-info:hover {
90
- background-color: #3a5a89;
91
- }
92
-
93
- .hidden {
94
- display: none;
95
- }