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_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:

View File

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

View File

@ -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}

View File

@ -223,50 +223,91 @@ export const BoardCanvas: React.FC<BoardCanvasProps> = ({
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<BoardCanvasProps> = ({
{hoveredObstacle && (
<>
<span className="text-slate-500">|</span>
<span className={hoveredObstacle.type === 'mountain' ? 'text-slate-300 font-bold' : 'text-indigo-400 font-bold'}>
{hoveredObstacle.type === 'mountain' ? '▲ Mountain (Impassable)' : '▼ Valley (Impassable)'}
<span className={hoveredObstacle.type === 'mountain' ? 'text-slate-300 font-bold' : 'text-emerald-400 font-bold'}>
{hoveredObstacle.type === 'mountain' ? '▲ Mountain (Impassable)' : '🌲 Forest (Impassable)'}
</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" />
Mountain
</span>
<span className="flex items-center gap-1 text-indigo-300">
<span className="inline-block w-2.5 h-2.5 rounded-sm bg-indigo-950 border border-indigo-500" />
Valley
<span className="flex items-center gap-1 text-emerald-400">
<span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-700 border border-emerald-500" />
Forest
</span>
</div>
</div>

View File

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