pyscreeps-arena 0.5.7b0__py3-none-any.whl → 0.5.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,705 @@
1
+ import json
2
+ import os
3
+ import math
4
+ import importlib
5
+ from pathlib import Path
6
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps
7
+
8
+
9
+ class MapRender:
10
+ def __init__(self, map_path, cell_size=32):
11
+ self._map_path = map_path
12
+ self._cell_size = cell_size
13
+ self._map_data = None
14
+
15
+ # Updated terrain colors
16
+ self._terrain_colors = {
17
+ 'X': (139, 126, 102), # terrain-wall #8B7E66
18
+ '2': (144, 238, 144), # plain #90EE90
19
+ 'A': (205, 190, 112), # swamp #CDBE70
20
+ }
21
+
22
+ # Object rendering configurations with custom scale ratios
23
+ self._object_styles = {
24
+ 'StructureSpawn': {
25
+ 'shape': 'circle',
26
+ 'fill': (255, 255, 0), # yellow
27
+ 'outline': (200, 200, 0),
28
+ 'size': 1.1 # 1.1x scale as requested
29
+ },
30
+ 'StructureWall': {
31
+ 'shape': 'rounded_square',
32
+ 'fill': (128, 128, 128), # gray
33
+ 'outline': (100, 100, 100),
34
+ 'size': 0.8,
35
+ 'radius': 0.2
36
+ },
37
+ 'StructureContainer': {
38
+ 'shape': 'square',
39
+ 'fill': (254, 238, 0), # #FEEE00
40
+ 'outline': (255, 255, 255), # white border
41
+ 'outline_width': 2,
42
+ 'size': 0.8
43
+ },
44
+ 'StructureTower': {
45
+ 'shape': 'tower', # circle + triangle
46
+ 'circle_fill': (100, 100, 100),
47
+ 'triangle_fill': (200, 200, 200),
48
+ 'size': 1.1 # 1.1x scale as requested
49
+ },
50
+ 'StructureRampart': {
51
+ 'shape': 'ring',
52
+ 'fill': None,
53
+ 'outline': (150, 150, 150),
54
+ 'outline_width': 3,
55
+ 'size': 0.85
56
+ },
57
+ 'StructureExtension': {
58
+ 'shape': 'circle',
59
+ 'fill': (200, 200, 200),
60
+ 'outline': (150, 150, 150),
61
+ 'size': 0.6
62
+ },
63
+ 'StructureRoad': {
64
+ 'shape': 'line',
65
+ 'fill': (100, 100, 100),
66
+ 'width': 3,
67
+ 'size': 0.8
68
+ },
69
+ 'Flag': {
70
+ 'shape': 'flag', # pole + flag shape
71
+ 'pole_color': (139, 69, 19), # brown
72
+ 'flag_color': (255, 0, 0), # red
73
+ 'size': 0.9
74
+ },
75
+ 'Source': {
76
+ 'shape': 'circle',
77
+ 'fill': (255, 215, 0), # gold
78
+ 'outline': (200, 170, 0),
79
+ 'size': 0.7
80
+ },
81
+ 'Creep': {
82
+ 'shape': 'circle',
83
+ 'fill': (0, 255, 255), # cyan
84
+ 'outline': (0, 200, 200),
85
+ 'size': 0.6
86
+ },
87
+ 'Resource': {
88
+ 'shape': 'diamond',
89
+ 'fill': (255, 165, 0), # orange
90
+ 'outline': (200, 130, 0),
91
+ 'size': 0.5
92
+ },
93
+ 'ConstructionSite': {
94
+ 'shape': 'square',
95
+ 'fill': (255, 165, 0), # orange
96
+ 'outline': (200, 130, 0),
97
+ 'size': 0.7
98
+ },
99
+ 'Portal': {
100
+ 'shape': 'ring',
101
+ 'fill': None,
102
+ 'outline': (128, 0, 128), # purple
103
+ 'outline_width': 4,
104
+ 'size': 0.8
105
+ }
106
+ }
107
+
108
+ # Resource management: Load local images
109
+ self._resources = {}
110
+ self._load_resources()
111
+
112
+ def _load_resources(self):
113
+ """Load local resources from resources directory."""
114
+ # Define resources directory path
115
+ resources_dir = Path(__file__).parent.parent / 'resources'
116
+
117
+ if not resources_dir.exists():
118
+ print(f"[DEBUG] Resources directory not found: {resources_dir}")
119
+ return
120
+
121
+ # Scan all Python files in resources directory
122
+ for resource_file in resources_dir.glob('*.py'):
123
+ if resource_file.name.startswith('_'):
124
+ continue # Skip __init__.py and other hidden files
125
+
126
+ # Get resource name from filename (remove .py extension)
127
+ resource_name = resource_file.stem
128
+
129
+ try:
130
+ # Import the module dynamically
131
+ module_name = f"pyscreeps_arena.resources.{resource_name}"
132
+ module = importlib.import_module(module_name)
133
+
134
+ # Check if toImage method exists
135
+ if hasattr(module, 'toImage'):
136
+ # Load the toImage method
137
+ self._resources[resource_name] = module.toImage
138
+ print(f"[DEBUG] Loaded resource: {resource_name}")
139
+ except Exception as e:
140
+ print(f"[DEBUG] Failed to load resource {resource_name}: {e}")
141
+
142
+ def _load_map(self):
143
+ """Load map data from JSON file."""
144
+ try:
145
+ with open(self._map_path, 'r', encoding='utf-8') as f:
146
+ self._map_data = json.load(f)
147
+ print(f"[DEBUG] Loaded map data with {len(self._map_data['map'])} rows") # 调试输出:查看地图行数
148
+ except FileNotFoundError:
149
+ raise FileNotFoundError(f"Map file not found: {self._map_path}")
150
+ except json.JSONDecodeError as e:
151
+ raise ValueError(f"Invalid JSON in map file: {e}")
152
+
153
+ def _get_map_dimensions(self):
154
+ """Get map width and height."""
155
+ if not self._map_data:
156
+ return 0, 0
157
+
158
+ height = len(self._map_data['map'])
159
+ width = len(self._map_data['map'][0]) if height > 0 else 0
160
+ print(f"[DEBUG] Map dimensions: {width}x{height}") # 调试输出:查看地图尺寸
161
+ return width, height
162
+
163
+ def _draw_terrain(self, draw, img_width, img_height):
164
+ """Draw terrain tiles with inner borders for continuous regions."""
165
+ map_rows = self._map_data['map']
166
+ height = len(map_rows)
167
+ width = len(map_rows[0]) if height > 0 else 0
168
+
169
+ # First draw all terrain tiles
170
+ for y, row in enumerate(map_rows):
171
+ for x, char in enumerate(row):
172
+ if char in self._terrain_colors:
173
+ color = self._terrain_colors[char]
174
+ left = x * self._cell_size
175
+ top = y * self._cell_size
176
+ right = left + self._cell_size
177
+ bottom = top + self._cell_size
178
+
179
+ draw.rectangle([left, top, right, bottom], fill=color)
180
+
181
+ # Then draw inner borders for continuous terrain regions
182
+ self._draw_terrain_inner_borders(draw, map_rows, width, height)
183
+
184
+ def _draw_terrain_inner_borders(self, draw, map_rows, width, height):
185
+ """Draw inner borders for continuous terrain regions."""
186
+ # Mark visited cells to avoid redundant processing
187
+ visited = [[False for _ in range(width)] for _ in range(height)]
188
+
189
+ # Iterate through all cells
190
+ for y in range(height):
191
+ for x in range(width):
192
+ if not visited[y][x]:
193
+ # Find continuous region
194
+ region = self._find_continuous_region(map_rows, x, y, visited)
195
+ if len(region) > 1: # Only draw borders for regions larger than 1 cell
196
+ self._draw_region_inner_borders(draw, region, map_rows[y][x])
197
+
198
+ def _find_continuous_region(self, map_rows, start_x, start_y, visited):
199
+ """Find continuous terrain region starting from (start_x, start_y)."""
200
+ terrain_type = map_rows[start_y][start_x]
201
+ region = []
202
+ queue = [(start_x, start_y)]
203
+ visited[start_y][start_x] = True
204
+
205
+ # Directions: up, down, left, right (no diagonals)
206
+ directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
207
+
208
+ while queue:
209
+ x, y = queue.pop(0)
210
+ region.append((x, y))
211
+
212
+ for dx, dy in directions:
213
+ nx, ny = x + dx, y + dy
214
+ # Check boundaries and if cell is same terrain type and not visited
215
+ if 0 <= nx < len(map_rows[0]) and 0 <= ny < len(map_rows):
216
+ if not visited[ny][nx] and map_rows[ny][nx] == terrain_type:
217
+ visited[ny][nx] = True
218
+ queue.append((nx, ny))
219
+
220
+ return region
221
+
222
+ def _draw_region_inner_borders(self, draw, region, terrain_type):
223
+ """Draw inner borders for a continuous region."""
224
+ if terrain_type not in self._terrain_colors:
225
+ return
226
+
227
+ # Calculate border color (50% opacity, 70% self color + 30% black)
228
+ base_color = self._terrain_colors[terrain_type]
229
+ # Mix 70% self color with 30% black
230
+ r = max(0, int(base_color[0] * 0.7))
231
+ g = max(0, int(base_color[1] * 0.7))
232
+ b = max(0, int(base_color[2] * 0.7))
233
+ border_color = (r, g, b, 128) # 128 = 50% opacity
234
+
235
+ # Fixed border width of 1 as requested
236
+ border_width = 1
237
+
238
+ # Create a set for quick lookup
239
+ region_set = set(region)
240
+ directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
241
+
242
+ # Iterate through each cell in region and check adjacent cells
243
+ for x, y in region:
244
+ # Check all four directions
245
+ for dx, dy in directions:
246
+ nx, ny = x + dx, y + dy
247
+ # If neighbor is not in region, draw border on that side
248
+ if (nx, ny) not in region_set:
249
+ # Calculate border coordinates
250
+ left = x * self._cell_size
251
+ top = y * self._cell_size
252
+ right = left + self._cell_size
253
+ bottom = top + self._cell_size
254
+
255
+ # Draw border based on direction
256
+ if dx == 0 and dy == -1: # Top border
257
+ draw.rectangle([left, top, right, top + border_width], fill=border_color)
258
+ elif dx == 0 and dy == 1: # Bottom border
259
+ draw.rectangle([left, bottom - border_width, right, bottom], fill=border_color)
260
+ elif dx == -1 and dy == 0: # Left border
261
+ draw.rectangle([left, top, left + border_width, bottom], fill=border_color)
262
+ elif dx == 1 and dy == 0: # Right border
263
+ draw.rectangle([right - border_width, top, right, bottom], fill=border_color)
264
+
265
+ def _draw_shape(self, draw, x, y, style, overlay_color=None):
266
+ """Draw a specific shape based on style configuration."""
267
+ center_x = x * self._cell_size + self._cell_size // 2
268
+ center_y = y * self._cell_size + self._cell_size // 2
269
+ size = int(self._cell_size * style.get('size', 0.7))
270
+
271
+ shape = style.get('shape', 'circle')
272
+
273
+ if shape == 'circle':
274
+ radius = size // 2
275
+ fill_color = style.get('fill')
276
+ outline_color = style.get('outline')
277
+
278
+ # Draw main circle
279
+ draw.ellipse([
280
+ center_x - radius, center_y - radius,
281
+ center_x + radius, center_y + radius
282
+ ], fill=fill_color, outline=outline_color)
283
+
284
+ # Add highlight and shadow for质感效果
285
+ if fill_color:
286
+ # Top-left highlight
287
+ highlight_radius = radius // 2
288
+ highlight_offset = radius // 3
289
+ highlight_color = tuple(min(255, c + 60) for c in fill_color)
290
+ draw.ellipse([
291
+ center_x - radius + highlight_offset, center_y - radius + highlight_offset,
292
+ center_x - radius + highlight_offset + highlight_radius,
293
+ center_y - radius + highlight_offset + highlight_radius
294
+ ], fill=highlight_color)
295
+
296
+ # Bottom-right shadow
297
+ shadow_radius = radius // 2
298
+ shadow_offset = radius // 3
299
+ shadow_color = tuple(max(0, c - 40) for c in fill_color)
300
+ draw.ellipse([
301
+ center_x + radius - shadow_offset - shadow_radius,
302
+ center_y + radius - shadow_offset - shadow_radius,
303
+ center_x + radius - shadow_offset,
304
+ center_y + radius - shadow_offset
305
+ ], fill=shadow_color)
306
+
307
+ elif shape == 'ring':
308
+ radius = size // 2
309
+ draw.ellipse([
310
+ center_x - radius, center_y - radius,
311
+ center_x + radius, center_y + radius
312
+ ], outline=style.get('outline'), width=style.get('outline_width', 2))
313
+
314
+ elif shape == 'square':
315
+ half_size = size // 2
316
+ left = center_x - half_size
317
+ top = center_y - half_size
318
+ right = center_x + half_size
319
+ bottom = center_y + half_size
320
+
321
+ fill_color = style.get('fill')
322
+ outline_color = style.get('outline')
323
+ outline_width = style.get('outline_width', 1)
324
+
325
+ # Create gradient fill for质感效果
326
+ if fill_color:
327
+ # Create a small gradient image
328
+ gradient_img = Image.new('RGBA', (size, size))
329
+ gradient_draw = ImageDraw.Draw(gradient_img)
330
+
331
+ # Linear gradient from top-left to bottom-right
332
+ for i in range(size):
333
+ for j in range(size):
334
+ # Calculate gradient factor (0.0 to 1.0)
335
+ factor = (i + j) / (2 * size)
336
+ # Interpolate color between highlight and shadow
337
+ highlight = tuple(min(255, c + 40) for c in fill_color)
338
+ shadow = tuple(max(0, c - 30) for c in fill_color)
339
+ # Calculate interpolated color
340
+ r = int(highlight[0] * (1 - factor) + shadow[0] * factor)
341
+ g = int(highlight[1] * (1 - factor) + shadow[1] * factor)
342
+ b = int(highlight[2] * (1 - factor) + shadow[2] * factor)
343
+ gradient_draw.point((i, j), fill=(r, g, b, 255))
344
+
345
+ # Paste gradient onto main image
346
+ main_img = draw._image
347
+ main_img.paste(gradient_img, (left, top), gradient_img)
348
+
349
+ # Draw outline if needed
350
+ if outline_color:
351
+ draw.rectangle([left, top, right, bottom],
352
+ outline=outline_color, width=outline_width)
353
+ else:
354
+ # Fallback to basic square if no fill color
355
+ draw.rectangle([left, top, right, bottom],
356
+ fill=fill_color,
357
+ outline=outline_color,
358
+ width=outline_width)
359
+
360
+ elif shape == 'rounded_square':
361
+ half_size = size // 2
362
+ left = center_x - half_size
363
+ top = center_y - half_size
364
+ right = center_x + half_size
365
+ bottom = center_y + half_size
366
+ radius = int(size * style.get('radius', 0.2))
367
+
368
+ fill_color = style.get('fill')
369
+ outline_color = style.get('outline')
370
+ outline_width = style.get('outline_width', 1)
371
+
372
+ # Create gradient fill for质感效果
373
+ if fill_color:
374
+ # Create a small gradient image
375
+ gradient_img = Image.new('RGBA', (size, size))
376
+ gradient_draw = ImageDraw.Draw(gradient_img)
377
+
378
+ # Linear gradient from top-left to bottom-right
379
+ for i in range(size):
380
+ for j in range(size):
381
+ # Calculate gradient factor (0.0 to 1.0)
382
+ factor = (i + j) / (2 * size)
383
+ # Interpolate color between highlight and shadow
384
+ highlight = tuple(min(255, c + 40) for c in fill_color)
385
+ shadow = tuple(max(0, c - 30) for c in fill_color)
386
+ # Calculate interpolated color
387
+ r = int(highlight[0] * (1 - factor) + shadow[0] * factor)
388
+ g = int(highlight[1] * (1 - factor) + shadow[1] * factor)
389
+ b = int(highlight[2] * (1 - factor) + shadow[2] * factor)
390
+ gradient_draw.point((i, j), fill=(r, g, b, 255))
391
+
392
+ # Paste gradient onto main image
393
+ main_img = draw._image
394
+ main_img.paste(gradient_img, (left, top), gradient_img)
395
+
396
+ # Draw rounded rectangle outline
397
+ if outline_color:
398
+ draw.rounded_rectangle([left, top, right, bottom], radius,
399
+ outline=outline_color,
400
+ width=outline_width)
401
+ else:
402
+ # Fallback to basic rounded square if no fill color
403
+ draw.rounded_rectangle([left, top, right, bottom], radius,
404
+ fill=fill_color,
405
+ outline=outline_color,
406
+ width=outline_width)
407
+
408
+ elif shape == 'diamond':
409
+ radius = size // 2
410
+ points = [
411
+ (center_x, center_y - radius), # top
412
+ (center_x + radius, center_y), # right
413
+ (center_x, center_y + radius), # bottom
414
+ (center_x - radius, center_y) # left
415
+ ]
416
+ draw.polygon(points, fill=style.get('fill'), outline=style.get('outline'))
417
+
418
+ elif shape == 'tower':
419
+ # Circle background
420
+ radius = size // 2
421
+ circle_fill = style.get('circle_fill')
422
+ triangle_fill = style.get('triangle_fill')
423
+
424
+ # Draw main circle
425
+ draw.ellipse([
426
+ center_x - radius, center_y - radius,
427
+ center_x + radius, center_y + radius
428
+ ], fill=circle_fill)
429
+
430
+ # Add metal reflection to circle
431
+ if circle_fill:
432
+ reflection_width = radius // 3
433
+ reflection_color = tuple(min(255, c + 80) for c in circle_fill)
434
+ draw.rectangle([
435
+ center_x - reflection_width,
436
+ center_y - radius,
437
+ center_x + reflection_width,
438
+ center_y + radius
439
+ ], fill=reflection_color)
440
+
441
+ # Triangle foreground with metal texture
442
+ tri_size = size // 3
443
+ triangle_points = [
444
+ (center_x, center_y - tri_size), # top
445
+ (center_x - tri_size, center_y + tri_size), # bottom left
446
+ (center_x + tri_size, center_y + tri_size) # bottom right
447
+ ]
448
+
449
+ # Draw main triangle
450
+ draw.polygon(triangle_points, fill=triangle_fill)
451
+
452
+ # Add metal reflection to triangle
453
+ if triangle_fill:
454
+ # Vertical reflection line
455
+ reflection_color = tuple(min(255, c + 60) for c in triangle_fill)
456
+ draw.line([
457
+ center_x, center_y - tri_size,
458
+ center_x, center_y + tri_size
459
+ ], fill=reflection_color, width=2)
460
+
461
+ # Horizontal reflection line
462
+ draw.line([
463
+ center_x - tri_size, center_y,
464
+ center_x + tri_size, center_y
465
+ ], fill=reflection_color, width=2)
466
+
467
+ elif shape == 'flag':
468
+ # Pole
469
+ pole_x = center_x - size // 4
470
+ pole_top = center_y - size // 2
471
+ pole_bottom = center_y + size // 2
472
+ draw.line([pole_x, pole_top, pole_x, pole_bottom],
473
+ fill=style.get('pole_color'), width=2)
474
+
475
+ # Flag shape
476
+ flag_left = pole_x
477
+ flag_top = pole_top
478
+ flag_right = center_x + size // 2
479
+ flag_bottom = center_y - size // 4
480
+ flag_middle = center_x
481
+
482
+ flag_points = [
483
+ (flag_left, flag_top),
484
+ (flag_right, flag_top + (flag_bottom - flag_top) // 3),
485
+ (flag_middle, center_y),
486
+ (flag_right, flag_bottom - (flag_bottom - flag_top) // 3),
487
+ (flag_left, flag_bottom)
488
+ ]
489
+ draw.polygon(flag_points, fill=style.get('flag_color'))
490
+
491
+ # Add flag texture
492
+ flag_color = style.get('flag_color')
493
+ if flag_color:
494
+ # Add diagonal stripes for texture
495
+ stripe_color = tuple(max(0, c - 30) for c in flag_color)
496
+ for i in range(5):
497
+ offset = i * 5
498
+ draw.line([
499
+ flag_left + offset, flag_top,
500
+ flag_right, flag_bottom - offset
501
+ ], fill=stripe_color, width=1)
502
+
503
+ elif shape == 'line':
504
+ # For roads, we'll draw a simple line, but in a real implementation
505
+ # you might want to connect adjacent road pieces
506
+ left = center_x - size // 2
507
+ right = center_x + size // 2
508
+ top = center_y - style.get('width', 3) // 2
509
+ bottom = center_y + style.get('width', 3) // 2
510
+ draw.rectangle([left, top, right, bottom], fill=style.get('fill'))
511
+
512
+ # Apply overlay color if provided
513
+ if overlay_color:
514
+ # Calculate bounding box for overlay application
515
+ half_size = size // 2
516
+ bbox = [center_x - half_size, center_y - half_size, center_x + half_size, center_y + half_size]
517
+
518
+ # Get main image reference
519
+ main_img = draw._image
520
+
521
+ # Extract the region containing the shape
522
+ shape_region = main_img.crop(bbox)
523
+
524
+ # Convert to RGBA if not already
525
+ if shape_region.mode != 'RGBA':
526
+ shape_region = shape_region.convert('RGBA')
527
+
528
+ # Apply overlay using the provided blend method
529
+ r, g, b, a = shape_region.split()
530
+ rgb_img = Image.merge('RGB', (r, g, b))
531
+ color_overlay = Image.new('RGB', shape_region.size, overlay_color)
532
+
533
+ # Blend RGB parts (50% transparency)
534
+ blended_rgb = Image.blend(rgb_img, color_overlay, alpha=0.5)
535
+
536
+ # Recombine with original alpha channel
537
+ new_r, new_g, new_b = blended_rgb.split()
538
+ blended_region = Image.merge('RGBA', (new_r, new_g, new_b, a))
539
+
540
+ # Paste blended region back to main image
541
+ main_img.paste(blended_region, bbox, blended_region)
542
+
543
+ def _draw_objects(self, draw):
544
+ """Draw game objects."""
545
+ objects = self._map_data.get('objects', {})
546
+
547
+ for obj_type, obj_list in objects.items():
548
+ if obj_type in self._object_styles and obj_list:
549
+ style = self._object_styles[obj_type]
550
+
551
+ for obj in obj_list:
552
+ x, y = obj['x'], obj['y']
553
+
554
+ # Check for 'my' attribute and determine overlay color
555
+ overlay_color = None
556
+ if 'my' in obj:
557
+ if obj['my']:
558
+ overlay_color = '#76EE00' # Green for friendly
559
+ else:
560
+ overlay_color = '#EE6363' # Red for enemy
561
+
562
+ # Try to use local resource image first
563
+ resource_name = obj_type
564
+ if resource_name in self._resources:
565
+ # Use local image
566
+ try:
567
+ self._draw_resource_image(draw, x, y, resource_name, style, overlay_color)
568
+ continue
569
+ except Exception as e:
570
+ print(f"[DEBUG] Failed to draw resource image for {obj_type}: {e}")
571
+
572
+ # Fall back to质感绘图
573
+ self._draw_shape(draw, x, y, style, overlay_color)
574
+
575
+ def _draw_resource_image(self, draw, x, y, resource_name, style, overlay_color=None):
576
+ """Draw local resource image."""
577
+ # Get the image from resource
578
+ img = self._resources[resource_name]()
579
+
580
+ # Calculate size based on style
581
+ size = int(self._cell_size * style.get('size', 0.7))
582
+
583
+ # Resize the image to fit the cell
584
+ img = img.resize((size, size), Image.Resampling.LANCZOS)
585
+
586
+ # Apply overlay color if needed
587
+ if overlay_color:
588
+ # Convert to RGBA if not already
589
+ if img.mode != 'RGBA':
590
+ img = img.convert('RGBA')
591
+
592
+ # Apply color overlay as per the provided blend method
593
+ r, g, b, a = img.split()
594
+ rgb_img = Image.merge('RGB', (r, g, b))
595
+ color_overlay = Image.new('RGB', img.size, overlay_color)
596
+ blended_rgb = Image.blend(rgb_img, color_overlay, alpha=0.5)
597
+ new_r, new_g, new_b = blended_rgb.split()
598
+ img = Image.merge('RGBA', (new_r, new_g, new_b, a))
599
+
600
+ # Calculate position to center the image
601
+ cell_x = x * self._cell_size
602
+ cell_y = y * self._cell_size
603
+ offset_x = (self._cell_size - size) // 2
604
+ offset_y = (self._cell_size - size) // 2
605
+
606
+ # Paste the image onto the main image
607
+ # Note: draw is a ImageDraw object, but we need to access the underlying image
608
+ # So we'll use the img attribute from the draw object
609
+ main_img = draw._image
610
+ main_img.paste(img, (cell_x + offset_x, cell_y + offset_y), img)
611
+
612
+ def _draw_grid(self, draw, img_width, img_height):
613
+ """Draw grid lines with transparency."""
614
+ # Create a semi-transparent overlay for grid
615
+ grid_img = Image.new('RGBA', (img_width, img_height), (255, 248, 220, 0)) # #FFF8DC with 0 alpha
616
+ grid_draw = ImageDraw.Draw(grid_img)
617
+
618
+ # Vertical lines
619
+ for x in range(0, img_width + 1, self._cell_size):
620
+ grid_draw.line([x, 0, x, img_height], fill=(255, 248, 220, 128), width=1) # 50% transparent
621
+
622
+ # Horizontal lines
623
+ for y in range(0, img_height + 1, self._cell_size):
624
+ grid_draw.line([0, y, img_width, y], fill=(255, 248, 220, 128), width=1) # 50% transparent
625
+
626
+ # Composite the grid onto the main image
627
+ return grid_img
628
+
629
+ def render(self, output_path=None, show_grid=True):
630
+ """
631
+ Render map to image file.
632
+
633
+ :param output_path: Output image path (default: map_render.png in same directory as map)
634
+ :param show_grid: Whether to show grid lines
635
+
636
+ :raises: FileNotFoundError: If map file doesn't exist
637
+ :raises: ValueError: If map data is invalid
638
+
639
+ :usage:
640
+ # 示例用法
641
+ renderer = MapRender('map1.json')
642
+ renderer.render('output.png')
643
+ """
644
+ self._load_map()
645
+
646
+ # Get map dimensions
647
+ width, height = self._get_map_dimensions()
648
+ img_width = width * self._cell_size
649
+ img_height = height * self._cell_size
650
+
651
+ print(f"[DEBUG] Creating image {img_width}x{img_height}") # 调试输出:查看图像尺寸
652
+
653
+ # Create image with alpha channel for transparency support
654
+ img = Image.new('RGBA', (img_width, img_height), color=(255, 255, 255, 255))
655
+ draw = ImageDraw.Draw(img)
656
+
657
+ # Draw terrain
658
+ self._draw_terrain(draw, img_width, img_height)
659
+
660
+ # Draw grid if requested (returns overlay image)
661
+ if show_grid:
662
+ grid_overlay = self._draw_grid(draw, img_width, img_height)
663
+ img = Image.alpha_composite(img, grid_overlay)
664
+ draw = ImageDraw.Draw(img)
665
+
666
+ # Draw objects
667
+ self._draw_objects(draw)
668
+
669
+ # Save image
670
+ if output_path is None:
671
+ map_dir = os.path.dirname(self._map_path)
672
+ output_path = os.path.join(map_dir, 'map_render.png')
673
+
674
+ # Convert to RGB if saving as JPEG, keep RGBA for PNG
675
+ if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'):
676
+ img = img.convert('RGB')
677
+
678
+ img.save(output_path)
679
+ print(f"[DEBUG] Map rendered to {output_path}") # 调试输出:查看输出路径
680
+
681
+ return output_path
682
+
683
+
684
+ def main():
685
+ """Main function for command line usage."""
686
+ import sys
687
+
688
+ if len(sys.argv) < 2:
689
+ print("Usage: python map_render.py <map1.json> [output.png]")
690
+ sys.exit(1)
691
+
692
+ map_path = sys.argv[1]
693
+ output_path = sys.argv[2] if len(sys.argv) > 2 else None
694
+
695
+ try:
696
+ renderer = MapRender(map_path)
697
+ result_path = renderer.render(output_path)
698
+ print(f"Map rendered successfully to {result_path}")
699
+ except Exception as e:
700
+ print(f"Error rendering map: {e}")
701
+ sys.exit(1)
702
+
703
+
704
+ if __name__ == "__main__":
705
+ main()