Skip to content

Instantly share code, notes, and snippets.

@entwanne

entwanne/__init__.py Secret

Last active Apr 5, 2020
Embed
What would you like to do?
Zolfenstein
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()
from zolf import main
if __name__ == '__main__':
main()
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)
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)
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]]
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
You can’t perform that action at this time.