AntDX316 commited on
Commit
6d54fec
·
1 Parent(s): e9e4cf4
Files changed (8) hide show
  1. Dockerfile +0 -27
  2. README.md +1 -1
  3. app.py +0 -599
  4. docker-compose.yml +0 -17
  5. index.html +45 -0
  6. requirements.txt +0 -4
  7. script.js +439 -0
  8. style.css +214 -0
Dockerfile DELETED
@@ -1,27 +0,0 @@
1
- FROM python:3.10-slim
2
-
3
- WORKDIR /app
4
-
5
- # Copy requirements first for better caching
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
15
-
16
- # Expose port 8501 (Streamlit's default port)
17
- EXPOSE 8501
18
-
19
- # Set environment variables
20
- ENV PYTHONUNBUFFERED=1
21
- ENV STREAMLIT_SERVER_PORT=8501
22
- ENV STREAMLIT_SERVER_HEADLESS=true
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,6 +3,6 @@ title: HF Map
3
  emoji: 💻
4
  colorFrom: purple
5
  colorTo: green
6
- sdk: streamlit
7
  pinned: false
8
  ---
 
3
  emoji: 💻
4
  colorFrom: purple
5
  colorTo: green
6
+ sdk: static
7
  pinned: false
8
  ---
app.py DELETED
@@ -1,599 +0,0 @@
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 DELETED
@@ -1,17 +0,0 @@
1
- version: '3'
2
-
3
- services:
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Battlefield Simulator</title>
7
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <header>
13
+ <h1>Battlefield Simulator</h1>
14
+ <div class="timer">
15
+ <span id="day-counter">Day 1 - 00:00 hours</span>
16
+ <button id="pause-button">Pause</button>
17
+ </div>
18
+ </header>
19
+ <main>
20
+ <div id="map"></div>
21
+ <div class="info-panel">
22
+ <h2>Unit Information</h2>
23
+ <div id="unit-info">
24
+ <p class="no-unit-selected">Click on a unit to see details</p>
25
+ </div>
26
+ </div>
27
+ </main>
28
+ <footer>
29
+ <div class="legend">
30
+ <div class="legend-item">
31
+ <div class="circle blue"></div>
32
+ <span>Blue Forces</span>
33
+ </div>
34
+ <div class="legend-item">
35
+ <div class="circle red"></div>
36
+ <span>Red Forces</span>
37
+ </div>
38
+ </div>
39
+ </footer>
40
+ </div>
41
+
42
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
43
+ <script src="script.js"></script>
44
+ </body>
45
+ </html>
requirements.txt DELETED
@@ -1,4 +0,0 @@
1
- streamlit>=1.22.0
2
- numpy>=1.20.0
3
- pandas>=1.3.0
4
- pillow>=8.0.0
 
 
 
 
 
script.js ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Map initialization
2
+ let map;
3
+ let units = [];
4
+ let selectedUnit = null;
5
+ let isPaused = false;
6
+ let dayCounter = 1;
7
+ let hourCounter = 0;
8
+ let battleIntervals = [];
9
+ let timer;
10
+
11
+ // Unit types with their properties
12
+ const unitTypes = {
13
+ infantry: { speed: 0.001, attackPower: 10, range: 0.01, icon: '👤' },
14
+ armor: { speed: 0.002, attackPower: 25, range: 0.015, icon: '🛡️' },
15
+ artillery: { speed: 0.0005, attackPower: 30, range: 0.03, icon: '💥' },
16
+ recon: { speed: 0.003, attackPower: 5, range: 0.02, icon: '🔍' },
17
+ airDefense: { speed: 0.0008, attackPower: 20, range: 0.025, icon: '🚀' },
18
+ command: { speed: 0.0006, attackPower: 5, range: 0.015, icon: '⭐' },
19
+ medical: { speed: 0.001, attackPower: 0, range: 0.008, icon: '🩺' }
20
+ };
21
+
22
+ // Initialize the map when the DOM is fully loaded
23
+ document.addEventListener('DOMContentLoaded', function() {
24
+ initializeMap();
25
+ generateUnits();
26
+ placeUnitsOnMap();
27
+ startSimulation();
28
+
29
+ // Set up event listener for pause button
30
+ document.getElementById('pause-button').addEventListener('click', togglePause);
31
+ });
32
+
33
+ function initializeMap() {
34
+ // Create map centered on a fictional battle area
35
+ map = L.map('map').setView([40.7, -74.0], 13);
36
+
37
+ // Add the base map tiles (OpenStreetMap)
38
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
39
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
40
+ maxZoom: 19
41
+ }).addTo(map);
42
+ }
43
+
44
+ function generateUnits() {
45
+ // Generate blue forces
46
+ for (let i = 0; i < 15; i++) {
47
+ const type = getRandomUnitType();
48
+ units.push({
49
+ id: `blue-${i}`,
50
+ name: `Blue Force ${getUnitName(type)} ${i + 1}`,
51
+ type: type,
52
+ faction: 'blue',
53
+ strength: Math.floor(Math.random() * 50) + 50, // 50-99
54
+ health: 100,
55
+ status: getRandomStatus(),
56
+ lat: 40.7 + (Math.random() * 0.05 - 0.025), // Around the center
57
+ lng: -74.0 + (Math.random() * 0.05 - 0.025),
58
+ targetLat: null,
59
+ targetLng: null,
60
+ marker: null,
61
+ icon: unitTypes[type].icon,
62
+ speed: unitTypes[type].speed,
63
+ attackPower: unitTypes[type].attackPower,
64
+ range: unitTypes[type].range,
65
+ inBattle: false,
66
+ movingToTarget: false,
67
+ kills: 0,
68
+ lastMoveTimestamp: Date.now()
69
+ });
70
+ }
71
+
72
+ // Generate red forces
73
+ for (let i = 0; i < 15; i++) {
74
+ const type = getRandomUnitType();
75
+ units.push({
76
+ id: `red-${i}`,
77
+ name: `Red Force ${getUnitName(type)} ${i + 1}`,
78
+ type: type,
79
+ faction: 'red',
80
+ strength: Math.floor(Math.random() * 50) + 50, // 50-99
81
+ health: 100,
82
+ status: getRandomStatus(),
83
+ lat: 40.7 + (Math.random() * 0.05 - 0.025), // Around the center
84
+ lng: -74.0 + (Math.random() * 0.05 - 0.025),
85
+ targetLat: null,
86
+ targetLng: null,
87
+ marker: null,
88
+ icon: unitTypes[type].icon,
89
+ speed: unitTypes[type].speed,
90
+ attackPower: unitTypes[type].attackPower,
91
+ range: unitTypes[type].range,
92
+ inBattle: false,
93
+ movingToTarget: false,
94
+ kills: 0,
95
+ lastMoveTimestamp: Date.now()
96
+ });
97
+ }
98
+ }
99
+
100
+ function getRandomUnitType() {
101
+ const types = Object.keys(unitTypes);
102
+ return types[Math.floor(Math.random() * types.length)];
103
+ }
104
+
105
+ function getUnitName(type) {
106
+ const names = {
107
+ infantry: 'Battalion',
108
+ armor: 'Tank Division',
109
+ artillery: 'Artillery Battery',
110
+ recon: 'Reconnaissance Unit',
111
+ airDefense: 'Air Defense Unit',
112
+ command: 'Command Center',
113
+ medical: 'Medical Corps'
114
+ };
115
+ return names[type] || 'Unit';
116
+ }
117
+
118
+ function getRandomStatus() {
119
+ const statuses = ['Operational', 'Engaged', 'Advancing', 'Defending', 'Flanking', 'Retreating'];
120
+ return statuses[Math.floor(Math.random() * statuses.length)];
121
+ }
122
+
123
+ function placeUnitsOnMap() {
124
+ units.forEach(unit => {
125
+ const markerSize = unit.strength / 15 + 20; // Size based on strength
126
+
127
+ // Create custom HTML element for the marker
128
+ const markerHtml = `
129
+ <div class="unit-marker ${unit.faction}-unit" style="width: ${markerSize}px; height: ${markerSize}px; line-height: ${markerSize}px; font-size: ${markerSize/2}px;">
130
+ ${unit.icon}
131
+ </div>
132
+ `;
133
+
134
+ // Create the icon
135
+ const customIcon = L.divIcon({
136
+ html: markerHtml,
137
+ className: '',
138
+ iconSize: [markerSize, markerSize],
139
+ iconAnchor: [markerSize/2, markerSize/2]
140
+ });
141
+
142
+ // Create marker and add to map
143
+ unit.marker = L.marker([unit.lat, unit.lng], { icon: customIcon })
144
+ .addTo(map)
145
+ .on('click', function() {
146
+ selectUnit(unit);
147
+ });
148
+ });
149
+ }
150
+
151
+ function selectUnit(unit) {
152
+ // Reset previous selection
153
+ if (selectedUnit) {
154
+ const prevMarker = selectedUnit.marker;
155
+ const prevIcon = prevMarker.options.icon;
156
+ const prevHtml = prevIcon.options.html.replace(' selected', '');
157
+
158
+ const newIcon = L.divIcon({
159
+ html: prevHtml,
160
+ className: prevIcon.options.className,
161
+ iconSize: prevIcon.options.iconSize,
162
+ iconAnchor: prevIcon.options.iconAnchor
163
+ });
164
+
165
+ prevMarker.setIcon(newIcon);
166
+ }
167
+
168
+ // Set new selection
169
+ selectedUnit = unit;
170
+
171
+ // Update marker to show selection
172
+ const marker = unit.marker;
173
+ const icon = marker.options.icon;
174
+ const newHtml = icon.options.html.replace('unit-marker', 'unit-marker selected');
175
+
176
+ const newIcon = L.divIcon({
177
+ html: newHtml,
178
+ className: icon.options.className,
179
+ iconSize: icon.options.iconSize,
180
+ iconAnchor: icon.options.iconAnchor
181
+ });
182
+
183
+ marker.setIcon(newIcon);
184
+
185
+ // Update info panel
186
+ updateUnitInfo(unit);
187
+ }
188
+
189
+ function updateUnitInfo(unit) {
190
+ const unitInfoDiv = document.getElementById('unit-info');
191
+
192
+ let healthClass = unit.health > 70 ? '' : (unit.health > 30 ? 'health-warning' : 'health-critical');
193
+
194
+ unitInfoDiv.innerHTML = `
195
+ <div class="unit-name">${unit.name}</div>
196
+ <div class="unit-type">Type: ${getUnitName(unit.type)}</div>
197
+ <div class="unit-strength">Strength: ${unit.strength} personnel</div>
198
+ <div class="unit-status">Status: ${unit.status}</div>
199
+ <div class="unit-health">
200
+ Health: ${unit.health}%
201
+ <div class="health-bar ${healthClass}">
202
+ <div class="health-fill" style="width: ${unit.health}%"></div>
203
+ </div>
204
+ </div>
205
+ <div class="unit-stats">
206
+ <div>Attack Power: ${unit.attackPower}</div>
207
+ <div>Range: ${(unit.range * 1000).toFixed(1)} meters</div>
208
+ <div>Speed: ${(unit.speed * 1000).toFixed(1)}</div>
209
+ <div>Kills: ${unit.kills}</div>
210
+ </div>
211
+ <div class="unit-position">
212
+ Position: ${unit.lat.toFixed(4)}, ${unit.lng.toFixed(4)}
213
+ </div>
214
+ `;
215
+
216
+ // If the unit is in battle, show that information
217
+ if (unit.inBattle) {
218
+ unitInfoDiv.innerHTML += `
219
+ <div class="unit-battle-status">
220
+ <span style="color: red; font-weight: bold;">⚔️ ENGAGED IN BATTLE ⚔️</span>
221
+ </div>
222
+ `;
223
+ }
224
+ }
225
+
226
+ function startSimulation() {
227
+ // Update time every second
228
+ timer = setInterval(() => {
229
+ if (!isPaused) {
230
+ updateTime();
231
+
232
+ // Every 3 seconds, assign new targets to some units
233
+ if (hourCounter % 3 === 0) {
234
+ assignRandomTargets();
235
+ }
236
+
237
+ moveUnits();
238
+ checkForBattles();
239
+
240
+ // Update the selected unit's info if one is selected
241
+ if (selectedUnit) {
242
+ updateUnitInfo(selectedUnit);
243
+ }
244
+ }
245
+ }, 1000);
246
+ }
247
+
248
+ function updateTime() {
249
+ hourCounter++;
250
+ if (hourCounter >= 24) {
251
+ hourCounter = 0;
252
+ dayCounter++;
253
+ }
254
+
255
+ // Format hours with leading zero
256
+ const formattedHours = hourCounter.toString().padStart(2, '0');
257
+ document.getElementById('day-counter').textContent = `Day ${dayCounter} - ${formattedHours}:00 hours`;
258
+ }
259
+
260
+ function togglePause() {
261
+ isPaused = !isPaused;
262
+ const pauseButton = document.getElementById('pause-button');
263
+ pauseButton.textContent = isPaused ? 'Resume' : 'Pause';
264
+ }
265
+
266
+ function assignRandomTargets() {
267
+ const activeFactions = ['blue', 'red'];
268
+
269
+ // Randomly select units to assign new targets
270
+ units.forEach(unit => {
271
+ // Only assign new targets to units that aren't in battle and at random (30% chance)
272
+ if (!unit.inBattle && Math.random() < 0.3) {
273
+ // Find potential enemy targets
274
+ const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue';
275
+ const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0);
276
+
277
+ if (enemies.length > 0) {
278
+ // Choose random enemy as target
279
+ const target = enemies[Math.floor(Math.random() * enemies.length)];
280
+ unit.targetLat = target.lat;
281
+ unit.targetLng = target.lng;
282
+ unit.movingToTarget = true;
283
+ unit.status = 'Advancing';
284
+ }
285
+ }
286
+ });
287
+ }
288
+
289
+ function moveUnits() {
290
+ const now = Date.now();
291
+
292
+ units.forEach(unit => {
293
+ // Only move units that are alive and have a target
294
+ if (unit.health > 0 && unit.movingToTarget && unit.targetLat && unit.targetLng) {
295
+ const timeDelta = (now - unit.lastMoveTimestamp) / 1000; // Time in seconds since last move
296
+ unit.lastMoveTimestamp = now;
297
+
298
+ // Calculate direction vector
299
+ const dLat = unit.targetLat - unit.lat;
300
+ const dLng = unit.targetLng - unit.lng;
301
+
302
+ // Calculate distance to target
303
+ const distance = Math.sqrt(dLat * dLat + dLng * dLng);
304
+
305
+ // If we've reached the target (or close enough)
306
+ if (distance < 0.0005) {
307
+ unit.movingToTarget = false;
308
+ unit.status = 'Operational';
309
+ return;
310
+ }
311
+
312
+ // Normalize direction vector
313
+ const magnitude = Math.sqrt(dLat * dLat + dLng * dLng);
314
+ const dirLat = dLat / magnitude;
315
+ const dirLng = dLng / magnitude;
316
+
317
+ // Move unit towards target with speed influenced by unit type
318
+ const moveDistance = unit.speed * timeDelta;
319
+ unit.lat += dirLat * moveDistance;
320
+ unit.lng += dirLng * moveDistance;
321
+
322
+ // Update marker position
323
+ if (unit.marker) {
324
+ unit.marker.setLatLng([unit.lat, unit.lng]);
325
+ }
326
+ }
327
+ });
328
+ }
329
+
330
+ function checkForBattles() {
331
+ // Clear any existing battle animations
332
+ document.querySelectorAll('.battle-indicator').forEach(el => el.remove());
333
+
334
+ // Check each unit against potential enemies
335
+ units.forEach(unit => {
336
+ if (unit.health <= 0) return; // Skip dead units
337
+
338
+ const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue';
339
+ const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0);
340
+
341
+ // Reset battle state
342
+ unit.inBattle = false;
343
+
344
+ enemies.forEach(enemy => {
345
+ // Calculate distance between units
346
+ const dLat = unit.lat - enemy.lat;
347
+ const dLng = unit.lng - enemy.lng;
348
+ const distance = Math.sqrt(dLat * dLat + dLng * dLng);
349
+
350
+ // If within range, start battle
351
+ if (distance < unit.range) {
352
+ // Mark both units as in battle
353
+ unit.inBattle = true;
354
+ enemy.inBattle = true;
355
+
356
+ // Update statuses
357
+ unit.status = 'Engaged';
358
+ enemy.status = 'Engaged';
359
+
360
+ // Create battle animation at midpoint
361
+ const battleLat = (unit.lat + enemy.lat) / 2;
362
+ const battleLng = (unit.lng + enemy.lng) / 2;
363
+
364
+ createBattleAnimation(battleLat, battleLng);
365
+
366
+ // Calculate damage based on unit properties (simplified for this demo)
367
+ const unitDamage = Math.round(Math.random() * unit.attackPower * 0.2);
368
+ const enemyDamage = Math.round(Math.random() * enemy.attackPower * 0.2);
369
+
370
+ // Apply damage
371
+ enemy.health = Math.max(0, enemy.health - unitDamage);
372
+ unit.health = Math.max(0, unit.health - enemyDamage);
373
+
374
+ // Check if unit defeated enemy
375
+ if (enemy.health <= 0 && unitDamage > 0) {
376
+ unit.kills++;
377
+ checkUnitDeath(enemy);
378
+ }
379
+
380
+ // Check if enemy defeated unit
381
+ if (unit.health <= 0 && enemyDamage > 0) {
382
+ enemy.kills++;
383
+ checkUnitDeath(unit);
384
+ return; // Stop checking more enemies if this unit is dead
385
+ }
386
+ }
387
+ });
388
+ });
389
+ }
390
+
391
+ function createBattleAnimation(lat, lng) {
392
+ // Convert lat/lng to pixel coordinates
393
+ const point = map.latLngToLayerPoint([lat, lng]);
394
+
395
+ // Create battle indicator element
396
+ const battleEl = document.createElement('div');
397
+ battleEl.className = 'battle-indicator';
398
+ battleEl.style.left = point.x + 'px';
399
+ battleEl.style.top = point.y + 'px';
400
+
401
+ // Add to map container
402
+ document.querySelector('#map').appendChild(battleEl);
403
+
404
+ // Remove element after animation completes
405
+ setTimeout(() => {
406
+ if (battleEl.parentNode) {
407
+ battleEl.parentNode.removeChild(battleEl);
408
+ }
409
+ }, 2000);
410
+ }
411
+
412
+ function checkUnitDeath(unit) {
413
+ if (unit.health <= 0) {
414
+ // Update the unit's appearance to show it's destroyed
415
+ if (unit.marker) {
416
+ // Change the marker to show a destroyed unit (smaller and gray)
417
+ const markerSize = 15; // Smaller size for destroyed units
418
+
419
+ const markerHtml = `
420
+ <div class="unit-marker ${unit.faction}-unit" style="width: ${markerSize}px; height: ${markerSize}px; line-height: ${markerSize}px; font-size: ${markerSize/2}px; opacity: 0.5; background-color: #888;">
421
+ ☠️
422
+ </div>
423
+ `;
424
+
425
+ const destroyedIcon = L.divIcon({
426
+ html: markerHtml,
427
+ className: '',
428
+ iconSize: [markerSize, markerSize],
429
+ iconAnchor: [markerSize/2, markerSize/2]
430
+ });
431
+
432
+ unit.marker.setIcon(destroyedIcon);
433
+ }
434
+
435
+ unit.status = 'Destroyed';
436
+ unit.movingToTarget = false;
437
+ unit.inBattle = false;
438
+ }
439
+ }
style.css ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ color: #333;
11
+ }
12
+
13
+ .container {
14
+ display: flex;
15
+ flex-direction: column;
16
+ height: 100vh;
17
+ max-width: 1400px;
18
+ margin: 0 auto;
19
+ padding: 1rem;
20
+ }
21
+
22
+ header {
23
+ display: flex;
24
+ justify-content: space-between;
25
+ align-items: center;
26
+ padding: 0.5rem 0;
27
+ margin-bottom: 1rem;
28
+ }
29
+
30
+ header h1 {
31
+ font-size: 1.8rem;
32
+ color: #2c3e50;
33
+ }
34
+
35
+ .timer {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 10px;
39
+ }
40
+
41
+ #pause-button {
42
+ padding: 0.4rem 0.8rem;
43
+ background-color: #3498db;
44
+ color: white;
45
+ border: none;
46
+ border-radius: 4px;
47
+ cursor: pointer;
48
+ transition: background-color 0.3s;
49
+ }
50
+
51
+ #pause-button:hover {
52
+ background-color: #2980b9;
53
+ }
54
+
55
+ main {
56
+ display: flex;
57
+ flex: 1;
58
+ gap: 1rem;
59
+ margin-bottom: 1rem;
60
+ }
61
+
62
+ #map {
63
+ flex: 3;
64
+ height: 100%;
65
+ border-radius: 5px;
66
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
67
+ }
68
+
69
+ .info-panel {
70
+ flex: 1;
71
+ background-color: white;
72
+ padding: 1rem;
73
+ border-radius: 5px;
74
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
75
+ overflow-y: auto;
76
+ }
77
+
78
+ .info-panel h2 {
79
+ margin-bottom: 1rem;
80
+ padding-bottom: 0.5rem;
81
+ border-bottom: 1px solid #ddd;
82
+ color: #2c3e50;
83
+ }
84
+
85
+ #unit-info {
86
+ line-height: 1.6;
87
+ }
88
+
89
+ .no-unit-selected {
90
+ color: #7f8c8d;
91
+ font-style: italic;
92
+ }
93
+
94
+ .unit-name {
95
+ font-weight: bold;
96
+ font-size: 1.2rem;
97
+ margin-bottom: 0.5rem;
98
+ color: #2c3e50;
99
+ }
100
+
101
+ .unit-type, .unit-strength, .unit-status {
102
+ margin-bottom: 0.5rem;
103
+ }
104
+
105
+ .unit-health {
106
+ margin: 1rem 0;
107
+ }
108
+
109
+ .health-bar {
110
+ height: 10px;
111
+ background-color: #ecf0f1;
112
+ border-radius: 5px;
113
+ overflow: hidden;
114
+ margin-top: 0.3rem;
115
+ }
116
+
117
+ .health-fill {
118
+ height: 100%;
119
+ background-color: #2ecc71;
120
+ border-radius: 5px;
121
+ transition: width 0.3s;
122
+ }
123
+
124
+ .health-critical .health-fill {
125
+ background-color: #e74c3c;
126
+ }
127
+
128
+ .health-warning .health-fill {
129
+ background-color: #f39c12;
130
+ }
131
+
132
+ .unit-actions {
133
+ margin-top: 1rem;
134
+ }
135
+
136
+ footer {
137
+ padding: 0.5rem 0;
138
+ }
139
+
140
+ .legend {
141
+ display: flex;
142
+ gap: 1.5rem;
143
+ justify-content: center;
144
+ }
145
+
146
+ .legend-item {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.5rem;
150
+ }
151
+
152
+ .circle {
153
+ width: 15px;
154
+ height: 15px;
155
+ border-radius: 50%;
156
+ }
157
+
158
+ .blue {
159
+ background-color: #3498db;
160
+ }
161
+
162
+ .red {
163
+ background-color: #e74c3c;
164
+ }
165
+
166
+ /* Map marker styles */
167
+ .unit-marker {
168
+ border-radius: 50%;
169
+ display: flex;
170
+ justify-content: center;
171
+ align-items: center;
172
+ color: white;
173
+ font-weight: bold;
174
+ border: 2px solid white;
175
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
176
+ transition: transform 0.3s, box-shadow 0.3s;
177
+ }
178
+
179
+ .unit-marker:hover {
180
+ transform: scale(1.1);
181
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
182
+ z-index: 1000 !important;
183
+ }
184
+
185
+ .unit-marker.blue-unit {
186
+ background-color: #3498db;
187
+ }
188
+
189
+ .unit-marker.red-unit {
190
+ background-color: #e74c3c;
191
+ }
192
+
193
+ .unit-marker.selected {
194
+ transform: scale(1.2);
195
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
196
+ z-index: 1000 !important;
197
+ }
198
+
199
+ /* Battle animation */
200
+ @keyframes battle-pulse {
201
+ 0% { transform: scale(1); opacity: 1; }
202
+ 50% { transform: scale(1.5); opacity: 0.7; }
203
+ 100% { transform: scale(2); opacity: 0; }
204
+ }
205
+
206
+ .battle-indicator {
207
+ position: absolute;
208
+ width: 20px;
209
+ height: 20px;
210
+ background-color: yellow;
211
+ border-radius: 50%;
212
+ pointer-events: none;
213
+ animation: battle-pulse 1s infinite;
214
+ }