A procedurally-generated maze game featuring dynamic difficulty adjustment, terrain mechanics, and smooth physics-based movement.
This game implements an ML based difficulty system inspired by Left 4 Dead's dynamic difficulty adjustment. The game analyzes your performance in real-time and adapts the challenge to keep you in an optimal "flow state" - not too easy, not too hard.
- Procedural Generation: Every maze is unique, generated using algorithms
- Adaptive Difficulty: The AI monitors your performance and adjusts complexity
- Terrain Physics: Navigate mud (slow), ice (slippery), and standard paths
- Weighted Pathfinding: The game calculates optimal solutions considering terrain costs
-
Real-time Performance Analysis
- Tracks path efficiency (precision)
- Monitors completion speed
- Calculates composite performance scores
-
Smooth Difficulty Scaling
- Grid size: 10×10 to 45×45
- Maze complexity (braiding factor)
- Terrain density adjustment
- Exponential moving average for smooth transitions
-
Persistent Progress
- Saves difficulty state between sessions
- Tracks historical performance
- JSON-based save system
-
Recursive Backtracker Algorithm
- Generates perfect mazes with guaranteed solutions
- Longest possible solution paths
- High complexity and challenge
-
Braiding System
- Adds loops and multiple paths
- Reduces dead ends
- Adjustable based on difficulty
-
Terrain Generation
- Mud (Brown): 5× movement cost, 70% speed penalty
- Ice (Light Blue): Auto-sliding mechanic
- Paths (White): Standard movement
-
Smooth Physics
- Sub-pixel interpolated movement
- Terrain-specific behaviors
- Visual feedback for all actions
-
Ice Sliding
- Continuous movement until hitting non-ice
- Strategic challenge element
- Stops at walls
-
Cost Tracking
- Real-time cost accumulation
- Comparison to optimal solution
- Performance metrics calculation
-
Minimap (for large mazes)
- Real-time position tracking
- Goal location indicator
- Toggleable display
-
Difficulty Bar
- Visual difficulty indicator
- Color gradient (green → red)
- Smooth animations
-
Comprehensive HUD
- Level counter
- Grid size
- Current cost vs optimal
- Time elapsed
- Control reminders
- Python 3.7 or higher
- pip package manager
# Option 1: If using git
git clone https://github.com/infinitecoder1729/dynamic-maze-challenge
cd dynamic-maze-challenge
# Option 2: If downloaded as ZIP
unzip dynamic-maze-challenge
cd dynamic-maze-challengepip install pygame>=2.0.0 numpy>=1.19.0python adaptive_maze_game.pyIf the game window opens, you're ready to play!
- Start Simple: The game begins at 30% difficulty
- Learn Mechanics: First few levels introduce terrain gradually
- Feel the Adaptation: Complete 3-5 levels to see difficulty adjust
- Master the Challenge: Game adapts to your skill ceiling
| Key | Action |
|---|---|
| ↑ ↓ ← → | Move player |
| M | Toggle terrain mode (mud/ice on/off) |
| P | Pause/Resume game |
| N | Toggle minimap display |
| ESC | Quit game |
Navigate from the yellow starting position to the green goal as efficiently as possible.
Your performance is evaluated on two dimensions:
-
Precision (60% weight)
- Measures path efficiency
- Formula:
optimal_cost / actual_cost - Perfect score = 1.0 (took optimal path)
-
Speed (40% weight)
- Measures completion time
- Formula:
expected_time / actual_time - Perfect score = 1.0 (completed in expected time)
| Performance Score | Difficulty Change | Description |
|---|---|---|
| 0.8+ | +0.08 | Excelling - Increase challenge |
| 0.6-0.8 | +0.04 | Good - Slight increase |
| 0.5-0.6 | 0.00 | Balanced - Maintain |
| 0.3-0.5 | -0.05 | Below average - Ease challenge |
| < 0.3 | -0.10 | Struggling - Reduce difficulty |
- Movement Cost: 5× standard
- Visual Speed: 30% of normal
- Strategy: Avoid when possible, use for shortcuts only
- Movement Cost: 1× (same as path)
- Special Mechanic: Auto-sliding
- Behavior: Cannot stop until reaching non-ice tile
- Strategy: Plan ahead, use walls to stop momentum
- Movement Cost: 1× (baseline)
- Speed: Normal
- Strategy: Preferred route when available
-
Plan Your Route
- Survey the maze before moving
- Identify mud patches to avoid
- Use ice strategically for fast traversal
-
Balance Speed and Precision
- Rushing increases cost but saves time
- Taking optimal path improves precision
- Find your personal balance point
-
Understand the AI Director
- Consistency is key - avoid extreme variance
- The director adapts over 5-level windows
- Intentionally playing poorly won't help long-term
-
Use Minimap on Large Mazes
- Press N to toggle
- Helps maintain orientation
- Shows goal location
┌─────────────────────────────────────────────────────────┐
│ GAME LOOP │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Input │→ │ Update │→ │ Render │ │
│ │ Handler │ │ Physics │ │ System │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────┐
│ ML │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Analyze Performance → Adjust Difficulty │ │
│ │ (Precision + Speed) → (Size + Complexity) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────┐
│ PROCEDURAL GENERATION │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Maze Gen │→ │ Braiding │→ │ Terrain │ │
│ │ (DFS) │ │ (Loops) │ │ (Mud/Ice) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
Purpose: Adaptive difficulty management
Key Methods:
analyze_and_adapt(metrics): Updates difficulty based on performanceget_level_parameters(): Returns concrete generation parameterssave_progress()/load_progress(): Persistence layer
Algorithm:
performance_score = (precision × 0.6) + (speed × 0.4)
if performance_score > threshold:
difficulty += delta
difficulty_smooth = (difficulty × 0.7) + (target_difficulty × 0.3)Purpose: Procedural maze generation
Algorithm: Recursive Backtracker (DFS)
1. Start with grid of walls
2. Pick random starting cell, mark as path
3. Push to stack
4. While stack not empty:
- Look at cell on top of stack
- If has unvisited neighbors:
- Choose random neighbor
- Carve path between them
- Push neighbor to stack
- Else:
- Pop from stack (backtrack)
5. Apply braiding (add loops)
6. Apply terrain (mud/ice)
Complexity: O(rows × cols)
Purpose: Calculate optimal path cost
Algorithm: Dijkstra's shortest path
1. Initialize priority queue with start position
2. While queue not empty:
- Pop cell with lowest cost
- If reached goal, return cost
- For each neighbor:
- Calculate new_cost = current_cost + terrain_cost
- If better than known cost:
- Update cost table
- Push to priority queue
Complexity: O((rows × cols) × log(rows × cols))
Purpose: Physics-based player controller
Key Features:
- Smooth interpolation between grid positions
- Terrain-specific speed modifiers
- Ice sliding state machine
- Cost accumulation tracking
Purpose: Optimized rendering with caching
Optimization: Pre-renders static maze to surface, only redraws dynamic elements (player, HUD)
Performance Gain: ~50-70% FPS improvement
Edit these constants in adaptive_maze_game.py:
# In FluidDirector.__init__:
self.current_difficulty = 0.3 # Starting difficulty (0.0-1.0)
# 0.3 = Easy start
# 0.5 = Medium start
# 0.7 = Hard start
# Performance thresholds (in analyze_and_adapt):
if performance_score > 0.8: delta = 0.08 # Increase sensitivity
elif performance_score > 0.6: delta = 0.04
# Adjust these values to change adaptation speed# Physics costs
COST_PATH = 1
COST_MUD = 5 # Change to 3 for easier mud, 7 for harder
COST_ICE = 1 # Keep at 1 (ice is meant to be fast)
# In Player.update():
if current_tile == 2: # Mud
speed *= 0.3 # Change to 0.5 for less penalty, 0.2 for more# Window configuration
WINDOW_WIDTH = 1000 # Default 1000
WINDOW_HEIGHT = 800 # Default 800
FPS = 60 # Target frame rate
# Colors (RGB tuples)
COLOR_PLAYER = (255, 255, 0) # Yellow - change to your preference
COLOR_GOAL = (0, 255, 0) # Green
COLOR_MUD = (101, 67, 33) # Brown
COLOR_ICE = (135, 206, 235) # Light Blue# In FluidDirector.get_level_parameters():
size_val = 10 + (diff * 35) # Min 10, Max 45
# Change formula for different ranges:
# size_val = 15 + (diff * 25) # Min 15, Max 40# === CONFIGURATION ===
# Constants, colors, enums
# === AI DIRECTOR ===
class FluidDirector
# === PROCEDURAL GENERATION ===
class AdvancedMazeGen
# === OPTIMAL PATH SOLVER ===
class WeightedSolver
# === PLAYER CONTROLLER ===
class Player
# === RENDERING SYSTEM ===
class GameRenderer
+ Helper functions
# === MAIN GAME LOOP ===
def main()# 1. Add color constant
COLOR_LAVA = (255, 69, 0) # Orange-red
# 2. Add cost constant
COST_LAVA = 10 # Very expensive
# 3. Modify AdvancedMazeGen._apply_terrain()
lava_mask = mask & (random_values >= density) & (random_values < density * 1.2)
self.grid[lava_mask] = 4 # Lava tile type
# 4. Update rendering in GameRenderer.render_maze()
elif val == 4:
pygame.draw.rect(self.maze_surface, COLOR_LAVA, rect)
# 5. Update WeightedSolver.get_optimal_cost()
if tile_type == 4: move_cost = COST_LAVA
# 6. Update Player movement (optional special behavior)
if current_tile == 4: # Lava
speed *= 0.1 # Very slow
# Could add damage over time, etc.# Test with different starting difficulties
python adaptive_maze_game.py # Default (0.3)
# Modify in code temporarily:
director = FluidDirector(starting_difficulty=0.7) # Hard start# Add at top of file:
import cProfile
import pstats
# Replace main() call:
if __name__ == "__main__":
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(20) # Top 20 functionsWhy This Algorithm?
- Generates "perfect" mazes (one solution path between any two points)
- Creates long, winding corridors (high challenge)
- Efficient: O(n) time complexity
- Simple to implement
Visual Walkthrough:
Step 1: All walls Step 2: Start point Step 3: Carve paths
███████████ ███████████ ███████████
███████████ █ ███████ █ █ ███
███████████ → ███████████ → █████ ███
███████████ ███████████ █████ ███
███████████ ███████████ █████ ██
Step 4: Backtrack Step 5: Complete
███████████ ███████████
█ █ ███ █ █ █ █
█ █ ███ → █ █ █ █
█ █ ███ █ █ █ █
█████ █ █ █████ █ █
Purpose: Reduce difficulty by adding alternative paths
Method:
- Scan grid for dead-end walls
- Check if removal connects two paths
- Remove with probability =
braid_chance
Effect:
braid_chance = 0.0: Pure maze (hardest)braid_chance = 0.6: Multiple paths (easier)
Why Not A?*
- We need exact cost to all reachable cells
- Terrain weights are not uniform (can't use heuristic effectively)
- Dijkstra guarantees optimal solution
Pseudocode:
function dijkstra(grid, start, goal):
costs = {start: 0}
pq = [(0, start)]
while pq not empty:
current_cost, current_pos = pq.pop_min()
if current_pos == goal:
return current_cost
for neighbor in get_neighbors(current_pos):
new_cost = current_cost + terrain_cost(neighbor)
if new_cost < costs[neighbor]:
costs[neighbor] = new_cost
pq.push((new_cost, neighbor))
return INFINITY # No path
Contributions welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch
git checkout -b feature/amazing-feature
- Commit your changes
git commit -m "Add amazing feature" - Push to branch
git push origin feature/amazing-feature
- Open a Pull Request
- Follow PEP 8 guidelines
- Add docstrings to all functions/classes
- Comment complex algorithms
- Keep functions under 50 lines when possible
Made with ❤️ by @infinitecoder1729
Note on AI Assistance > This project utilized Large Language Models (LLMs) to refine code documentation, standardize variable naming conventions, and assist with code formatting and beautification.