Zolfenstein
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from zolf.game import Game | |
from zolf.game import Map | |
from zolf.game import Vector | |
from zolf.graphics import OptimizedApp as App | |
mapstring = ''' | |
XXXXXXX | |
X XX | |
X X | |
X X | |
X X | |
XX X | |
XXX XXX | |
XXX XXX | |
XX XXX | |
XX XXXX | |
XXXXXXX | |
''' | |
def main(): | |
game = Game((800, 600), Map.loads(mapstring), Vector(2.5, 2.1), 0.1) | |
App(game).run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from zolf import main | |
if __name__ == '__main__': | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
from zolf.vector import Vector | |
from zolf.vector import segment_inters | |
class Map: | |
sides = { | |
('x', False): 'L', | |
('x', True): 'R', | |
('y', False): 'U', | |
('y', True): 'D' | |
} | |
def __init__(self, walls): | |
self.walls = walls | |
@classmethod | |
def loads(cls, mapstring, cwall='X'): | |
return cls({ | |
(x, y): c == cwall | |
for y, line in enumerate(mapstring.strip('\n').splitlines()) | |
for x, c in enumerate(line) | |
}) | |
def map_inters(self, origin, vector): | |
vorient = Vector(vector.x < 0, vector.y < 0) | |
for k, position, side in segment_inters(origin, vector): | |
orient = getattr(vorient, side) | |
if orient: # If seen from the right/bottom, get the left/up block | |
setattr(position, side, getattr(position, side) - 1) | |
if side == 'x': | |
offset = position.y - math.floor(position.y) | |
else: | |
offset = position.x - math.floor(position.x) | |
case = (math.floor(position.x), math.floor(position.y)) | |
if case not in self.walls: | |
break | |
yield (k, case, self.sides[side, orient], offset) | |
def wall_inter(self, origin, vector): | |
for k, case, side, offset in self.map_inters(origin, vector): | |
if self.walls[case]: | |
return (k, case, side, offset) | |
class Player: | |
def __init__(self, map, position, angle=0, speed=0.1): | |
self.map = map | |
self.position = position | |
self.dangle = angle | |
self.speed = speed | |
@property | |
def angle(self): | |
return math.radians(self.dangle) | |
@property | |
def sight(self): | |
return Vector(math.cos(self.angle), -math.sin(self.angle)) | |
@property | |
def screen(self): | |
"direction vector of screen" | |
return Vector(-self.sight.y, self.sight.x) | |
def rotate(self, degrees): | |
self.dangle += degrees | |
def walk(self, direction=1): | |
self.position += self.sight * direction * self.speed | |
def wall_inter(self, dx): # -1. <= dx < 1. | |
"Get wall size for all columns of the screen" | |
vector = Vector(self.sight.x + dx * self.screen.x, self.sight.y + dx * self.screen.y) | |
inter = self.map.wall_inter(self.position, vector) | |
if inter is None: | |
return (0, None, None, 0) | |
k, case, side, offset = inter | |
h = 1 / k if k else 1. | |
return (h, case, side, offset) | |
class Game: | |
def __init__(self, screen_size, map, player_position, player_speed=0.1): | |
self.width, self.height = screen_size | |
self.map = map | |
self.player = Player(map, player_position, speed=player_speed) | |
def get_wall(self, x): | |
dx = 2 * x / self.width - 1 | |
return self.player.wall_inter(dx) | |
def get_walls(self): | |
for x in range(self.width): | |
ret = self.get_wall(x) | |
yield (x,) + ret | |
def get_height(self, h): | |
return math.floor(min(h, 1.) * self.height) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
import pyglet | |
from zolf.optimize import WallsOptimizer | |
from zolf.optimize import best_indices | |
COLORS = { | |
'G': (128,128,128,255), # ground | |
'S': (100,100,255,255) # sky | |
} | |
TEXTURE_OFFSETS = { | |
'L': 0, # left | |
'R': 0.25, # right | |
'U': 0.5, # up | |
'D': 0.75, # down | |
} | |
def load_texture(filename): | |
texture = pyglet.image.load(filename).get_texture() | |
# Avoid aliasing | |
pyglet.gl.glTexParameteri( | |
pyglet.gl.GL_TEXTURE_2D, | |
pyglet.gl.GL_TEXTURE_MAG_FILTER, | |
pyglet.gl.GL_NEAREST, | |
) | |
return texture | |
class Canvas: | |
def __init__(self, game): | |
self.game = game | |
self.width, self.height = game.width, game.height | |
self.walls = [] | |
self.batch = pyglet.graphics.Batch() | |
self._init_batch() | |
def _init_batch(self): | |
texture = load_texture('texture.png') | |
bg_group = pyglet.graphics.OrderedGroup(0) | |
fg_group = pyglet.graphics.TextureGroup( | |
texture, | |
pyglet.graphics.OrderedGroup(1), | |
) | |
for x in range(1, self.width+1): | |
# Ground batch | |
self.batch.add(2, | |
pyglet.gl.GL_LINES, | |
bg_group, | |
('v2i', (x, 0, x, self.height//2)), | |
('c4B', COLORS['G']*2), | |
) | |
# Sky batch | |
self.batch.add(2, | |
pyglet.gl.GL_LINES, | |
bg_group, | |
('v2i', (x, self.height//2, x, self.height)), | |
('c4B', COLORS['S']*2), | |
) | |
# Walls batches | |
self.walls.append( | |
self.batch.add(2, | |
pyglet.gl.GL_LINES, | |
fg_group, | |
('v2i', (x, 0, x, 0)), | |
('t2f', (0,0,0,1)), | |
) | |
) | |
def draw(self): | |
self.batch.draw() | |
def redraw_wall(self, x, height, side, offset): | |
wall = self.walls[x] | |
x += 1 # opengl starts X at 1 | |
# Setup wall position (height) | |
line_height = self.game.get_height(height) | |
y = (self.height - line_height) // 2 | |
wall.vertices = (x, y, x, y + line_height) | |
# Setup wall texture | |
x_offset = TEXTURE_OFFSETS[side] + offset / len(TEXTURE_OFFSETS) | |
y_offset = 0 if height <= 1 else (height-1) / (2*height) | |
wall.tex_coords = (x_offset, y_offset, x_offset, 1-y_offset) | |
class Window: | |
def __init__(self, canvas): | |
self.canvas = canvas | |
self.window = pyglet.window.Window(width=canvas.width, height=canvas.height) | |
self.window.set_exclusive_mouse(True) | |
self.keys = pyglet.window.key.KeyStateHandler() | |
self.window.push_handlers(self.keys) | |
self.window.event(self.on_draw) | |
self.window.event(self.on_expose) | |
def on_draw(self): | |
self.canvas.draw() | |
def on_expose(self): | |
pass | |
class App: | |
def __init__(self, game): | |
self.game = game | |
self.canvas = Canvas(game) | |
self.window = Window(self.canvas) | |
pyglet.clock.schedule_interval(self.update, 0.01) | |
def update(self, dt): | |
updated = False | |
if self.window.keys[pyglet.window.key.LEFT]: | |
self.game.player.rotate(3) | |
updated = True | |
elif self.window.keys[pyglet.window.key.RIGHT]: | |
self.game.player.rotate(-3) | |
updated = True | |
if self.window.keys[pyglet.window.key.UP]: | |
self.game.player.walk() | |
updated = True | |
elif self.window.keys[pyglet.window.key.DOWN]: | |
self.game.player.walk(-1) | |
updated = True | |
if updated: | |
self.redraw_walls() | |
def redraw_walls(self): | |
for x, height, _, side, offset in self.game.get_walls(): | |
self.canvas.redraw_wall(x, height, side, offset) | |
def run(self): | |
self.redraw_walls() | |
pyglet.app.run() | |
class OptimizedApp(App): | |
def __init__(self, game): | |
super().__init__(game) | |
self.indices = best_indices(game.width) | |
def redraw_walls(self): | |
walls = WallsOptimizer(self.indices) | |
for x in walls.indices: | |
height, case, side, offset = self.game.get_wall(x) | |
if case is None: | |
continue | |
walls.add_wall((case, side), x, height, offset) | |
for (_, side), x, height, offset in walls.walls: | |
self.canvas.redraw_wall(x, height, side, offset) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from collections import namedtuple | |
_Wall = namedtuple('_Wall', ('x', 'height', 'offset')) | |
class WallsOptimizer: | |
# Optimize to avoid calculating multiple times for a same wall block | |
def __init__(self, indices): | |
self._indices = list(indices) | |
self.cache = {} | |
@property | |
def indices(self): | |
while self._indices: | |
yield self._indices.pop() | |
def add_wall(self, key, x, height, offset): | |
wall = _Wall(x, height, offset) | |
if key in self.cache: | |
left, right = self.cache[key] | |
if wall.x < left.x: | |
self.cache[key] = (wall, right) | |
for i in range(wall.x + 1, left.x): | |
self._indices.remove(i) | |
else: # x > right.x | |
self.cache[key] = (left, wall) | |
for i in range(right.x + 1, wall.x): | |
self._indices.remove(i) | |
else: | |
self.cache[key] = (wall, wall) | |
@property | |
def walls(self): | |
for key, (left, right) in self.cache.items(): | |
height_slope = offset_slope = 0 | |
amplitude = right.x - left.x | |
if amplitude: | |
height_slope = (right.height - left.height) / amplitude | |
offset_slope = (right.offset - left.offset) / amplitude | |
for x in range(left.x, right.x + 1): | |
height = left.height + (x - left.x) * height_slope | |
offset = left.offset + (x - left.x) * offset_slope | |
yield key, x, height, offset | |
def best_indices(width): | |
def _get_indices(indices): | |
if len(indices) > 2: | |
pivot = len(indices) // 2 | |
yield indices[pivot] | |
yield from _get_indices(indices[:pivot]) | |
yield from _get_indices(indices[pivot+1:]) | |
else: | |
yield from indices | |
indices = range(width) | |
return [indices[0], *_get_indices(indices[1:-1]), indices[-1]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
import typing | |
from dataclasses import dataclass | |
@dataclass | |
class Vector: | |
x: float | |
y: float | |
def __mul__(self, k: float) -> 'Vector': | |
return type(self)(self.x * k, self.y * k) | |
def __rmul__(self, k: float) -> 'Vector': | |
return self * k | |
def __add__(self, v: 'Vector') -> 'Vector': | |
return type(self)(self.x + v.x, self.y + v.y) | |
def segment_coord_inters(origin: float, dist: float): # may be infinite | |
""" | |
Intersections between a 1D segment and integers | |
yields (k, pos) couples such that origin + k * dist == pos, where pos is an integer | |
ordered by k | |
""" | |
if dist: | |
step = abs(1 / dist) | |
if dist > 0: | |
direction = 1 | |
position = math.ceil(origin) | |
else: | |
direction = -1 | |
position = math.floor(origin) | |
k = (position - origin) / dist | |
while True: | |
yield (k, position) | |
k += step | |
position += direction | |
def segment_inters(origin: Vector, vector: Vector): # infinite | |
""" | |
Intersections between a 2D segment and integer cases | |
yields (k, pos, dim) tuples such that origin + k * vector == pos, where the dim value of pos is an integer | |
ordered by k | |
""" | |
xgen = segment_coord_inters(origin.x, vector.x) | |
ygen = segment_coord_inters(origin.y, vector.y) | |
x = y = None | |
while True: | |
if x is None: | |
kx, x = next(xgen, (float('inf'), 0)) | |
if y is None: | |
ky, y = next(ygen, (float('inf'), 0)) | |
if kx < ky: | |
yield (kx, Vector(x, origin.y + kx * vector.y), 'x') | |
x = None | |
else: | |
yield (ky, Vector(origin.x + ky * vector.x, y), 'y') | |
y = None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment