Replace valley obstacles with lush green forest groves and pine tree graphics

This commit is contained in:
Isaac Johnson 2026-09-05 19:50:49 -05:00
parent d12b5615e5
commit 85ee13a513
5 changed files with 92 additions and 52 deletions

View File

@ -104,25 +104,23 @@ class GameEngine:
curr_x += dx curr_x += dx
curr_y += dy curr_y += dy
# 2. Procedural Valleys / chasms (5-7 winding gorges) # 2. Procedural Dense Forests / Woodlands (6-8 organic forest groves)
for _ in range(random.randint(5, 7)): for _ in range(random.randint(6, 8)):
curr_x = random.randint(min_x + 5, max_x - 5) center_x = random.randint(min_x + 6, max_x - 6)
curr_y = random.randint(min_y + 5, max_y - 5) center_y = random.randint(min_y + 6, max_y - 6)
length = random.randint(16, 28) grove_cells = [(center_x, center_y)]
angle_dx = random.choice([-1, 1]) target_trees = random.randint(22, 36)
angle_dy = random.choice([-1, 1])
for _ in range(length): for _ in range(target_trees):
if (curr_x, curr_y) not in obstacles: if not grove_cells:
add_obstacle(curr_x, curr_y, "valley") break
if random.random() < 0.35: cx, cy = random.choice(grove_cells)
curr_x += random.choice([-1, 0, 1]) if (cx, cy) not in obstacles:
else: add_obstacle(cx, cy, "forest")
curr_x += angle_dx for nx, ny in [(cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)]:
if random.random() < 0.35: if min_x <= nx <= max_x and min_y <= ny <= max_y and (nx, ny) not in obstacles:
curr_y += random.choice([-1, 0, 1]) if random.random() < 0.65:
else: grove_cells.append((nx, ny))
curr_y += angle_dy
# Enforce max allowed obstacles (cap strictly below 50%) # Enforce max allowed obstacles (cap strictly below 50%)
if len(obstacles) > max_allowed_obstacles: if len(obstacles) > max_allowed_obstacles:

View File

@ -104,6 +104,7 @@ class AvailableMovesResponse(BaseModel):
class ObstacleType(str, Enum): class ObstacleType(str, Enum):
MOUNTAIN = "mountain" MOUNTAIN = "mountain"
FOREST = "forest"
VALLEY = "valley" VALLEY = "valley"

View File

@ -243,7 +243,7 @@ def test_map_obstacles_mountains_valleys_and_connectivity():
obstacle_types = {obs["type"] for obs in obstacles} obstacle_types = {obs["type"] for obs in obstacles}
assert "mountain" in obstacle_types 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} obstacle_coords = {(obs["x"], obs["y"]) for obs in obstacles}

View File

@ -223,50 +223,91 @@ export const BoardCanvas: React.FC<BoardCanvasProps> = ({
ctx.fillRect(x0, y0, cellSize, cellSize); ctx.fillRect(x0, y0, cellSize, cellSize);
} }
} else { } else {
// Valley / Chasm trench // Forest / Woodland Grove (Green & Trees)
ctx.fillStyle = '#060913'; ctx.fillStyle = '#022c22';
ctx.fillRect(x0, y0, cellSize, cellSize); ctx.fillRect(x0, y0, cellSize, cellSize);
ctx.strokeStyle = '#312e81'; ctx.strokeStyle = '#064e3b';
ctx.lineWidth = 0.5; ctx.lineWidth = 0.5;
ctx.strokeRect(x0, y0, cellSize, cellSize); ctx.strokeRect(x0, y0, cellSize, cellSize);
if (cellSize >= 6) { if (cellSize >= 6) {
// Canyon cliffs // Sturdy timber tree trunk
ctx.fillStyle = '#1e1b4b'; 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.beginPath();
ctx.moveTo(x0, y0); ctx.moveTo(cx, y0 + cellSize * 0.44);
ctx.lineTo(x0 + cellSize * 0.3, y0); ctx.lineTo(cx - cellSize * 0.38, y0 + cellSize * 0.74);
ctx.lineTo(x0 + cellSize * 0.45, y0 + cellSize); ctx.lineTo(cx + cellSize * 0.38, y0 + cellSize * 0.74);
ctx.lineTo(x0, y0 + cellSize);
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
// Tier 2 (Middle canopy - rich emerald)
ctx.fillStyle = '#059669';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x0 + cellSize, y0); ctx.moveTo(cx, y0 + cellSize * 0.26);
ctx.lineTo(x0 + cellSize * 0.7, y0); ctx.lineTo(cx - cellSize * 0.30, y0 + cellSize * 0.52);
ctx.lineTo(x0 + cellSize * 0.55, y0 + cellSize); ctx.lineTo(cx + cellSize * 0.30, y0 + cellSize * 0.52);
ctx.lineTo(x0 + cellSize, y0 + cellSize);
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
// Deep chasm rift fissure // Tier 1 (Top canopy - vibrant fresh pine green)
ctx.strokeStyle = '#4338ca'; ctx.fillStyle = '#10b981';
ctx.lineWidth = Math.max(0.8, cellSize * 0.08);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(cx - cellSize * 0.05, y0); ctx.moveTo(cx, y0 + cellSize * 0.12);
ctx.lineTo(cx + cellSize * 0.05, cy); ctx.lineTo(cx - cellSize * 0.20, y0 + cellSize * 0.34);
ctx.lineTo(cx - cellSize * 0.05, y0 + cellSize); 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(); ctx.stroke();
// Glowing crevasse depth highlight // If zoomed in large, render two flanking mini trees to form a lush dense grove
ctx.strokeStyle = '#818cf8'; if (cellSize >= 18) {
ctx.lineWidth = Math.max(0.5, cellSize * 0.03); // Left mini tree
ctx.beginPath(); const lcx = cx - cellSize * 0.26;
ctx.moveTo(cx, y0 + cellSize * 0.2); const ltrunkW = Math.max(1, cellSize * 0.08);
ctx.lineTo(cx, y0 + cellSize * 0.8); ctx.fillStyle = '#78350f';
ctx.stroke(); 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 { } else {
ctx.fillStyle = '#1e1b4b'; ctx.fillStyle = '#059669';
ctx.fillRect(x0, y0, cellSize, cellSize); ctx.fillRect(x0, y0, cellSize, cellSize);
} }
} }
@ -575,8 +616,8 @@ export const BoardCanvas: React.FC<BoardCanvasProps> = ({
{hoveredObstacle && ( {hoveredObstacle && (
<> <>
<span className="text-slate-500">|</span> <span className="text-slate-500">|</span>
<span className={hoveredObstacle.type === 'mountain' ? 'text-slate-300 font-bold' : 'text-indigo-400 font-bold'}> <span className={hoveredObstacle.type === 'mountain' ? 'text-slate-300 font-bold' : 'text-emerald-400 font-bold'}>
{hoveredObstacle.type === 'mountain' ? '▲ Mountain (Impassable)' : '▼ Valley (Impassable)'} {hoveredObstacle.type === 'mountain' ? '▲ Mountain (Impassable)' : '🌲 Forest (Impassable)'}
</span> </span>
</> </>
)} )}
@ -590,9 +631,9 @@ export const BoardCanvas: React.FC<BoardCanvasProps> = ({
<span className="inline-block w-2.5 h-2.5 rounded-sm bg-slate-600 border border-slate-400" /> <span className="inline-block w-2.5 h-2.5 rounded-sm bg-slate-600 border border-slate-400" />
Mountain Mountain
</span> </span>
<span className="flex items-center gap-1 text-indigo-300"> <span className="flex items-center gap-1 text-emerald-400">
<span className="inline-block w-2.5 h-2.5 rounded-sm bg-indigo-950 border border-indigo-500" /> <span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-700 border border-emerald-500" />
Valley Forest
</span> </span>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ export interface GridConfig {
export interface Obstacle { export interface Obstacle {
x: number; x: number;
y: number; y: number;
type: 'mountain' | 'valley'; type: 'mountain' | 'forest' | 'valley';
} }
export interface Player { export interface Player {