yeely1310

Untitled

Apr 7th, 2026
544
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.01 KB | None | 0 0
  1. That sounds like the classic **Raycasting** engine, the same technique used in the original *Wolfenstein 3D*. Those Javascript snippets you saw likely cast "rays" from the player's eye across a 2D grid, calculating the distance to the nearest wall and drawing a vertical line that grows shorter as the distance increases. It tricks the brain into seeing 3D depth using only text characters.
  2.  
  3. To bring that elegance to Ubuntu, we need to control the terminal buffer directly to redraw the frame without the lag of standard output. Python's `curses` library is perfect for this, as it handles terminal input (like arrow keys) and screen rendering natively.
  4.  
  5. Here is a complete, self-contained script that recreates that specific vibe. I've tuned the wall rendering to use characters that vary based on depth, giving you that "dots and slashes" perspective shift you remember.
  6.  
  7. ### The Code (maze_3d.py)
  8.  
  9. Save this file as `maze_3d.py` and run it with `python3 maze_3d.py`.
  10.  
  11. ```python
  12. import curses
  13. import math
  14. import time
  15. import sys
  16.  
  17. # Configuration
  18. MAP_SIZE = 30
  19. SCREEN_WIDTH = 80
  20. SCREEN_HEIGHT = 40
  21. FOV = math.pi / 3
  22. SPEED = 0.05
  23. ROTATION_SPEED = 0.03
  24.  
  25. # 2D Map: 1 = Wall, 0 = Empty
  26. # A simple square maze
  27. MAZE = [
  28.    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  29.    [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
  30.    [1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1],
  31.    [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  32.    [1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1],
  33.    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  34.    [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1],
  35.    [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],
  36.    [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],
  37.    [1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],
  38.    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1],
  39.    [1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1],
  40.    [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  41.    [1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1],
  42.    [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  43.    [1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  44.    [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  45.    [1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  46.    [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1],
  47.    [1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  48.    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  49.    [1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1],
  50.    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  51.    [1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  52.    [1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1],
  53.    [1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1],
  54.    [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1],
  55.    [1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1],
  56.    [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  57.    [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1],
  58.    [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
  59.    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
  60. ]
  61.  
  62. # Wall texture characters based on face type
  63. WALL_CHARS = ['#', '|', '/', '\\', '.', 'o', '=', ' ']
  64.  
  65. class Raycaster:
  66.     def __init__(self, stdscr, maze):
  67.         self.screen = stdscr
  68.         self.maze = maze
  69.         self.x = 5
  70.         self.y = 5
  71.         self.angle = 0
  72.         self.draw()
  73.  
  74.     def move(self, direction):
  75.         # Basic collision detection
  76.         dx = math.cos(self.angle) * SPEED
  77.         dy = math.sin(self.angle) * SPEED
  78.         next_x = self.x + dx * direction
  79.         next_y = self.y + dy * direction
  80.  
  81.         # Check grid bounds and walls
  82.         mx = int(next_x)
  83.         my = int(next_y)
  84.  
  85.         if 0 < mx < MAP_SIZE - 1 and 0 < my < MAP_SIZE - 1:
  86.             if self.maze[my][mx] == 0:
  87.                 self.x, self.y = next_x, next_y
  88.  
  89.     def rotate(self, delta):
  90.         self.angle += delta
  91.  
  92.     def cast_rays(self):
  93.         # For each column in the terminal
  94.         for col in range(SCREEN_WIDTH):
  95.             # Calculate ray angle for this column
  96.             # FOV is pi/3, so we spread from -FOV/2 to +FOV/2
  97.             ray_angle = (self.angle + (col / SCREEN_WIDTH - 0.5) * FOV)
  98.            
  99.             # Normalize angle to 0-2pi
  100.             ray_angle %= 2 * math.pi
  101.            
  102.             # Step ray forward until it hits a wall
  103.             distance = 0.0
  104.             hit = False
  105.            
  106.             # Using step size based on distance for performance
  107.             # Small steps are more accurate, large are faster.
  108.             # 0.05 is a good balance for text rendering.
  109.             step = 0.05
  110.            
  111.             while not hit and distance < 50:
  112.                 distance += step
  113.                 # Check the grid index
  114.                 check_x = int(self.x + math.cos(ray_angle) * distance)
  115.                 check_y = int(self.y + math.sin(ray_angle) * distance)
  116.                
  117.                 if 0 <= check_y < MAP_SIZE and 0 <= check_x < MAP_SIZE:
  118.                     if self.maze[check_y][check_x] == 1:
  119.                         hit = True
  120.                         break
  121.            
  122.             if hit:
  123.                 # Correct for fisheye effect
  124.                 # Distance needs to be divided by the cosine of the difference
  125.                 # between the ray angle and the player's viewing angle
  126.                 corrected_dist = distance * math.cos(ray_angle - self.angle)
  127.                
  128.                 # Calculate wall height on screen
  129.                 # Closer walls = taller lines
  130.                 # Avoid division by zero
  131.                 wall_height = min(SCREEN_HEIGHT, int((SCREEN_HEIGHT * MAP_SIZE) / corrected_dist))
  132.                
  133.                 # Calculate top position
  134.                 top = int((SCREEN_HEIGHT - wall_height) / 2)
  135.                
  136.                 # Pick character based on distance and angle
  137.                 # Creates the "dots and slashes" texture variation
  138.                 char_idx = int(distance) % len(WALL_CHARS)
  139.                 char = WALL_CHARS[char_idx]
  140.                
  141.                 # Draw the vertical line for this column
  142.                 for row in range(top, top + wall_height):
  143.                     self.screen.addch(row, col, char)
  144.  
  145.     def draw(self):
  146.         # Clear screen
  147.         self.screen.clear()
  148.        
  149.         # Render the rays
  150.         self.cast_rays()
  151.        
  152.         # Draw player arrow
  153.         # Just a static indicator at the bottom center roughly
  154.         # self.screen.addch(SCREEN_HEIGHT - 1, SCREEN_WIDTH // 2, '>')
  155.  
  156.         self.screen.refresh()
  157.         time.sleep(0.02)
  158.  
  159. def main(stdscr):
  160.     # Setup curses
  161.     curses.curs_set(0) # Hide cursor
  162.     stdscr.nodelay(True) # Non-blocking input
  163.     stdscr.clear()
  164.    
  165.     # Center the screen logic
  166.     y, x = stdscr.getmaxyx()
  167.    
  168.     # Initialize raycaster
  169.     rc = Raycaster(stdscr, MAZE)
  170.    
  171.     running = True
  172.     while running:
  173.         # Check input
  174.         key = stdscr.getch()
  175.         if key == curses.KEY_UP:
  176.             rc.move(1)
  177.         elif key == curses.KEY_DOWN:
  178.             rc.move(-1)
  179.         elif key == curses.KEY_LEFT:
  180.             rc.rotate(-ROTATION_SPEED)
  181.         elif key == curses.KEY_RIGHT:
  182.             rc.rotate(ROTATION_SPEED)
  183.         elif key == ord('q'):
  184.             running = False
  185.         elif key == ord('p'):
  186.             rc.x = 5
  187.             rc.y = 5
  188.             rc.angle = 0
  189.        
  190.         # Update display
  191.         rc.draw()
  192.  
  193. if __name__ == "__main__":
  194.     curses.wrapper(main)
  195. ```
  196.  
  197. ### The Math Behind the Illusion
  198.  
  199. You mentioned you got into coding because of the elegance of this logic. The "magic" here is purely trigonometry and geometry. Here is the breakdown of the calculation used in `cast_rays()`:
  200.  
  201. 1.  **The Field of View (FOV):**
  202.     We want the player to see a cone. We define this angle mathematically. If our FOV is \( \frac{\pi}{3} \) (60 degrees), we need to divide that cone into \( SCREEN\_WIDTH \) slices. For each column \( col \), we calculate the specific angle:
  203.     \[
  204.     \theta_{ray} = \theta_{player} + \left(\frac{col}{SCREEN\_WIDTH} - 0.5\right) \times FOV
  205.     \]
  206.     This ensures the rays spread out correctly from the center of the player's vision.
  207.  
  208. 2.  **Fisheye Correction:**
  209.    When casting rays, a ray shooting straight ahead at a wall is shorter than a ray shooting at a 45-degree angle to a wall that is geometrically "parallel" to the center. To make the walls look flat and not like a wide-angle lens, we must correct the distance. We divide the raw hit distance by the cosine of the angle difference:
  210.    \[
  211.    D_{corrected} = \frac{D_{raw}}{\cos(\theta_{ray} - \theta_{player})}
  212.    \]
  213.  
  214. 3.  **Wall Height Projection:**
  215.    This is a simple similar triangle problem. We know the map unit size and the screen size. As the distance \( D_{corrected} \) gets smaller, the wall height \( H \) gets larger.
  216.    \[
  217.    H = \frac{C}{D_{corrected}}
  218.    \]
  219.    Where \( C \) is a constant scaling factor to map units to pixels.
  220.  
  221. 4.  **Texture Generation:**
  222.    By using the modulo operator on the distance `int(distance) % len(WALL_CHARS)`, we cycle through different ASCII characters (`#`, `|`, `/`, `\`) as the ray gets closer or further away. This creates the illusion of different lighting or wall textures without needing an actual 2D texture map.
  223.  
  224. ### How to Run on Ubuntu
  225.  
  226. 1.  Open your terminal.
  227. 2.  Install Python if not already installed (usually `sudo apt install python3`).
  228. 3.  Copy the code above into a file named `maze_3d.py`.
  229. 4.  Run it:
  230.    ```bash
  231.    python3 maze_3d.py
  232.    ```
  233. 5.  **Controls:**
  234.    *   **Up/Down Arrow**: Move forward/backward.
  235.    *   **Left/Right Arrow**: Turn.
  236.    *   **P**: Reset position.
  237.    *   **Q**: Quit.
  238.  
  239. This should bring back that spark. It's raw, efficient, and relies on math rather than heavy graphics libraries. Enjoy the trip back to the terminal days!
Advertisement
Add Comment
Please, Sign In to add comment