AntDX316 commited on
Commit
23b1b4e
·
1 Parent(s): 335d8f4
Files changed (4) hide show
  1. Dockerfile +8 -9
  2. README.md +6 -6
  3. app.py +653 -66
  4. requirements.txt +1 -1
Dockerfile CHANGED
@@ -11,23 +11,22 @@ COPY index.html /app/
11
  COPY styles.css /app/
12
  COPY script.js /app/
13
  COPY app.py /app/
14
- COPY healthcheck.html /app/
15
- COPY interface.py /app/
16
 
17
  # Set proper permissions
18
  RUN chmod -R 755 /app
19
 
20
- # Expose ports (Gradio uses 7860 by default)
21
- EXPOSE 7860
22
- EXPOSE 8080
23
 
24
  # Set environment variables
25
  ENV PYTHONUNBUFFERED=1
26
- ENV DOCKER_CONTAINER=true
27
- ENV HF_SPACE=true
 
 
28
 
29
  # Print environment for debugging
30
  RUN echo "Files in container:" && ls -la /app/
31
 
32
- # Run the Gradio interface
33
- CMD ["python", "-u", "interface.py"]
 
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
17
 
18
+ # Expose port 8501 (Streamlit's default port)
19
+ EXPOSE 8501
 
20
 
21
  # Set environment variables
22
  ENV PYTHONUNBUFFERED=1
23
+ ENV STREAMLIT_SERVER_PORT=8501
24
+ 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"]
README.md CHANGED
@@ -81,13 +81,13 @@ This application can also be run using Docker, which ensures consistent behavior
81
 
82
  ### Deploying to Hugging Face Spaces
83
 
84
- This application is compatible with Hugging Face Spaces Docker deployments. The Docker configuration has been specifically designed for the Hugging Face Spaces environment:
85
 
86
- 1. **Simple Python HTTP Server**: Instead of using Nginx (which has permission issues on Spaces), we use Python's built-in HTTP server
87
- 2. **Port 8080**: The web server listens on port 8080 (the default port expected by Hugging Face Spaces)
88
- 3. **Python-based Entry Point**: Using app.py as the main entry point improves compatibility with Hugging Face's environment
89
- 4. **Minimal Dependencies**: No complex web server configurations required, making deployment more reliable
90
- 5. **Environment Variable Integration**: The app detects when it's running in a Docker container on Hugging Face
91
 
92
  To deploy to Hugging Face Spaces:
93
 
 
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
 
app.py CHANGED
@@ -1,67 +1,654 @@
 
1
  import os
2
- import http.server
3
- import socketserver
4
- import threading
5
- import time
6
-
7
- # This file is for serving our static app on Hugging Face Spaces
8
-
9
- # Configuration
10
- PORT = int(os.environ.get("PORT", 8080))
11
- HOST = "0.0.0.0" # Listen on all interfaces
12
- DIRECTORY = os.path.dirname(os.path.abspath(__file__))
13
-
14
- # Change to the directory with our static files
15
- os.chdir(DIRECTORY)
16
-
17
- # Custom request handler with better error handling
18
- class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
19
- # Set correct MIME types
20
- extensions_map = {
21
- '.html': 'text/html',
22
- '.css': 'text/css',
23
- '.js': 'application/javascript',
24
- '.json': 'application/json',
25
- '.png': 'image/png',
26
- '.jpg': 'image/jpeg',
27
- '.jpeg': 'image/jpeg',
28
- '.gif': 'image/gif',
29
- '.svg': 'image/svg+xml',
30
- '': 'application/octet-stream',
31
- }
32
-
33
- def log_message(self, format, *args):
34
- # Enhanced logging
35
- print(f"[{self.log_date_time_string()}] {format % args}")
36
-
37
- def do_GET(self):
38
- # Handle specific paths
39
- if self.path == '/' or self.path == '':
40
- self.path = '/index.html'
41
-
42
- # Print detailed request info for debugging
43
- print(f"Handling request for: {self.path}")
44
-
45
- # Serve the file
46
- return http.server.SimpleHTTPRequestHandler.do_GET(self)
47
-
48
- # Print startup information
49
- print("=" * 60)
50
- print(f"Starting Battle Simulator Server")
51
- print(f"Current directory: {DIRECTORY}")
52
- print(f"Files in directory:")
53
- for file in os.listdir(DIRECTORY):
54
- print(f" - {file}")
55
- print(f"Listening on {HOST}:{PORT}")
56
- print("=" * 60)
57
-
58
- # Create and start the server
59
- handler = CustomHTTPRequestHandler
60
- httpd = socketserver.TCPServer((HOST, PORT), handler)
61
-
62
- # Print ready message
63
- print(f"Server started - Battle Simulator ready!")
64
- print(f"Open the 'App' tab to view the application")
65
-
66
- # Start serving
67
- httpd.serve_forever()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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('.'))}")
requirements.txt CHANGED
@@ -1 +1 @@
1
- gradio>=3.50.2
 
1
+ streamlit>=1.22.0