diff --git a/backend/app/game.py b/backend/app/game.py index 2f8e9ae..cb8b63a 100644 --- a/backend/app/game.py +++ b/backend/app/game.py @@ -104,25 +104,23 @@ class GameEngine: curr_x += dx curr_y += dy - # 2. Procedural Valleys / chasms (5-7 winding gorges) - for _ in range(random.randint(5, 7)): - curr_x = random.randint(min_x + 5, max_x - 5) - curr_y = random.randint(min_y + 5, max_y - 5) - length = random.randint(16, 28) - angle_dx = random.choice([-1, 1]) - angle_dy = random.choice([-1, 1]) + # 2. Procedural Dense Forests / Woodlands (6-8 organic forest groves) + for _ in range(random.randint(6, 8)): + center_x = random.randint(min_x + 6, max_x - 6) + center_y = random.randint(min_y + 6, max_y - 6) + grove_cells = [(center_x, center_y)] + target_trees = random.randint(22, 36) - for _ in range(length): - if (curr_x, curr_y) not in obstacles: - add_obstacle(curr_x, curr_y, "valley") - if random.random() < 0.35: - curr_x += random.choice([-1, 0, 1]) - else: - curr_x += angle_dx - if random.random() < 0.35: - curr_y += random.choice([-1, 0, 1]) - else: - curr_y += angle_dy + for _ in range(target_trees): + if not grove_cells: + break + cx, cy = random.choice(grove_cells) + if (cx, cy) not in obstacles: + add_obstacle(cx, cy, "forest") + for nx, ny in [(cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)]: + if min_x <= nx <= max_x and min_y <= ny <= max_y and (nx, ny) not in obstacles: + if random.random() < 0.65: + grove_cells.append((nx, ny)) # Enforce max allowed obstacles (cap strictly below 50%) if len(obstacles) > max_allowed_obstacles: diff --git a/backend/app/models.py b/backend/app/models.py index aed2fef..347ab4d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -104,6 +104,7 @@ class AvailableMovesResponse(BaseModel): class ObstacleType(str, Enum): MOUNTAIN = "mountain" + FOREST = "forest" VALLEY = "valley" diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 783a0ae..0e5393d 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -243,7 +243,7 @@ def test_map_obstacles_mountains_valleys_and_connectivity(): obstacle_types = {obs["type"] for obs in obstacles} assert "mountain" in obstacle_types - assert "valley" in obstacle_types + assert "forest" in obstacle_types obstacle_coords = {(obs["x"], obs["y"]) for obs in obstacles} diff --git a/frontend/src/components/BoardCanvas.tsx b/frontend/src/components/BoardCanvas.tsx index 12ab3e1..6a0dcca 100644 --- a/frontend/src/components/BoardCanvas.tsx +++ b/frontend/src/components/BoardCanvas.tsx @@ -223,50 +223,91 @@ export const BoardCanvas: React.FC = ({ ctx.fillRect(x0, y0, cellSize, cellSize); } } else { - // Valley / Chasm trench - ctx.fillStyle = '#060913'; + // Forest / Woodland Grove (Green & Trees) + ctx.fillStyle = '#022c22'; ctx.fillRect(x0, y0, cellSize, cellSize); - ctx.strokeStyle = '#312e81'; + ctx.strokeStyle = '#064e3b'; ctx.lineWidth = 0.5; ctx.strokeRect(x0, y0, cellSize, cellSize); if (cellSize >= 6) { - // Canyon cliffs - ctx.fillStyle = '#1e1b4b'; + // Sturdy timber tree trunk + const trunkWidth = Math.max(1.5, cellSize * 0.14); + const trunkHeight = cellSize * 0.22; + ctx.fillStyle = '#78350f'; + ctx.fillRect(cx - trunkWidth / 2, y0 + cellSize * 0.70, trunkWidth, trunkHeight); + + // Tier 3 (Bottom evergreen canopy - deep pine green) + ctx.fillStyle = '#065f46'; ctx.beginPath(); - ctx.moveTo(x0, y0); - ctx.lineTo(x0 + cellSize * 0.3, y0); - ctx.lineTo(x0 + cellSize * 0.45, y0 + cellSize); - ctx.lineTo(x0, y0 + cellSize); + ctx.moveTo(cx, y0 + cellSize * 0.44); + ctx.lineTo(cx - cellSize * 0.38, y0 + cellSize * 0.74); + ctx.lineTo(cx + cellSize * 0.38, y0 + cellSize * 0.74); ctx.closePath(); ctx.fill(); + // Tier 2 (Middle canopy - rich emerald) + ctx.fillStyle = '#059669'; ctx.beginPath(); - ctx.moveTo(x0 + cellSize, y0); - ctx.lineTo(x0 + cellSize * 0.7, y0); - ctx.lineTo(x0 + cellSize * 0.55, y0 + cellSize); - ctx.lineTo(x0 + cellSize, y0 + cellSize); + ctx.moveTo(cx, y0 + cellSize * 0.26); + ctx.lineTo(cx - cellSize * 0.30, y0 + cellSize * 0.52); + ctx.lineTo(cx + cellSize * 0.30, y0 + cellSize * 0.52); ctx.closePath(); ctx.fill(); - // Deep chasm rift fissure - ctx.strokeStyle = '#4338ca'; - ctx.lineWidth = Math.max(0.8, cellSize * 0.08); + // Tier 1 (Top canopy - vibrant fresh pine green) + ctx.fillStyle = '#10b981'; ctx.beginPath(); - ctx.moveTo(cx - cellSize * 0.05, y0); - ctx.lineTo(cx + cellSize * 0.05, cy); - ctx.lineTo(cx - cellSize * 0.05, y0 + cellSize); + ctx.moveTo(cx, y0 + cellSize * 0.12); + ctx.lineTo(cx - cellSize * 0.20, y0 + cellSize * 0.34); + ctx.lineTo(cx + cellSize * 0.20, y0 + cellSize * 0.34); + ctx.closePath(); + ctx.fill(); + + // Sunlight foliage highlight along left edges + ctx.strokeStyle = '#34d399'; + ctx.lineWidth = Math.max(0.6, cellSize * 0.04); + ctx.beginPath(); + ctx.moveTo(cx, y0 + cellSize * 0.12); + ctx.lineTo(cx - cellSize * 0.20, y0 + cellSize * 0.34); + ctx.moveTo(cx - cellSize * 0.08, y0 + cellSize * 0.34); + ctx.lineTo(cx - cellSize * 0.30, y0 + cellSize * 0.52); + ctx.moveTo(cx - cellSize * 0.12, y0 + cellSize * 0.52); + ctx.lineTo(cx - cellSize * 0.38, y0 + cellSize * 0.74); ctx.stroke(); - // Glowing crevasse depth highlight - ctx.strokeStyle = '#818cf8'; - ctx.lineWidth = Math.max(0.5, cellSize * 0.03); - ctx.beginPath(); - ctx.moveTo(cx, y0 + cellSize * 0.2); - ctx.lineTo(cx, y0 + cellSize * 0.8); - ctx.stroke(); + // If zoomed in large, render two flanking mini trees to form a lush dense grove + if (cellSize >= 18) { + // Left mini tree + const lcx = cx - cellSize * 0.26; + const ltrunkW = Math.max(1, cellSize * 0.08); + ctx.fillStyle = '#78350f'; + ctx.fillRect(lcx - ltrunkW / 2, y0 + cellSize * 0.76, ltrunkW, cellSize * 0.14); + + ctx.fillStyle = '#047857'; + ctx.beginPath(); + ctx.moveTo(lcx, y0 + cellSize * 0.48); + ctx.lineTo(lcx - cellSize * 0.16, y0 + cellSize * 0.78); + ctx.lineTo(lcx + cellSize * 0.16, y0 + cellSize * 0.78); + ctx.closePath(); + ctx.fill(); + + // Right mini tree + const rcx = cx + cellSize * 0.26; + const rtrunkW = Math.max(1, cellSize * 0.08); + ctx.fillStyle = '#78350f'; + ctx.fillRect(rcx - rtrunkW / 2, y0 + cellSize * 0.76, rtrunkW, cellSize * 0.14); + + ctx.fillStyle = '#059669'; + ctx.beginPath(); + ctx.moveTo(rcx, y0 + cellSize * 0.44); + ctx.lineTo(rcx - cellSize * 0.16, y0 + cellSize * 0.78); + ctx.lineTo(rcx + cellSize * 0.16, y0 + cellSize * 0.78); + ctx.closePath(); + ctx.fill(); + } } else { - ctx.fillStyle = '#1e1b4b'; + ctx.fillStyle = '#059669'; ctx.fillRect(x0, y0, cellSize, cellSize); } } @@ -575,8 +616,8 @@ export const BoardCanvas: React.FC = ({ {hoveredObstacle && ( <> | - - {hoveredObstacle.type === 'mountain' ? '▲ Mountain (Impassable)' : '▼ Valley (Impassable)'} + + {hoveredObstacle.type === 'mountain' ? '▲ Mountain (Impassable)' : '🌲 Forest (Impassable)'} )} @@ -590,9 +631,9 @@ export const BoardCanvas: React.FC = ({ Mountain - - - Valley + + + Forest diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a2c0d79..d999d61 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -10,7 +10,7 @@ export interface GridConfig { export interface Obstacle { x: number; y: number; - type: 'mountain' | 'valley'; + type: 'mountain' | 'forest' | 'valley'; } export interface Player {