This commit is contained in:
whiteline 2024-02-26 19:25:32 +01:00
parent 252ae8d03c
commit d5d80345fc
4 changed files with 629 additions and 2 deletions

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/bin/zsh
xrdb -merge ~/.Xresources.green #xrdb -merge ~/.Xresources.green
urxvt -fn xft:inconsolata-26:antialias=true:hinting=true -letsp 1 -b 26 & urxvt -fn xft:inconsolata-26:antialias=true:hinting=true -letsp 1 -b 26 &
xrdb -merge ~/.Xresources #xrdb -merge ~/.Xresources

324
fov.py Normal file
View File

@ -0,0 +1,324 @@
"""
Author: Aaron MacDonald
Date: June 14, 2007
Description: An implementation of the precise permissive field
of view algorithm for use in tile-based games.
Based on the algorithm presented at
http://roguebasin.roguelikedevelopment.org/
index.php?title=
Precise_Permissive_Field_of_View.
You are free to use or modify this code as long as this notice is
included.
This code is released without warranty.
"""
import copy
def fieldOfView(startX, startY, mapWidth, mapHeight, radius, \
funcVisitTile, funcTileBlocked):
"""
Determines which coordinates on a 2D grid are visible from a
particular coordinate.
startX, startY: The (x, y) coordinate on the grid that
is the centre of view.
mapWidth, mapHeight: The maximum extents of the grid. The
minimum extents are assumed to be both
zero.
radius: How far the field of view may extend
in either direction along the x and y
axis.
funcVisitTile: User function that takes two integers
representing an (x, y) coordinate. Is
used to "visit" visible coordinates.
funcTileBlocked: User function that takes two integers
representing an (x, y) coordinate.
Returns True if the coordinate blocks
sight to coordinates "behind" it.
"""
visited = set() # Keep track of what tiles have been visited so
# that no tile will be visited twice.
# Will always see the centre.
funcVisitTile(startX, startY)
visited.add((startX, startY))
# Ge the dimensions of the actual field of view, making
# sure not to go off the map or beyond the radius.
if startX < radius:
minExtentX = startX
else:
minExtentX = radius
if mapWidth - startX - 1 < radius:
maxExtentX = mapWidth - startX - 1
else:
maxExtentX = radius
if startY < radius:
minExtentY = startY
else:
minExtentY = radius
if mapHeight - startY - 1 < radius:
maxExtentY = mapHeight - startY - 1
else:
maxExtentY = radius
# Northeast quadrant
__checkQuadrant(visited, startX, startY, 1, 1, \
maxExtentX, maxExtentY, \
funcVisitTile, funcTileBlocked)
# Southeast quadrant
__checkQuadrant(visited, startX, startY, 1, -1, \
maxExtentX, minExtentY, \
funcVisitTile, funcTileBlocked)
# Southwest quadrant
__checkQuadrant(visited, startX, startY, -1, -1, \
minExtentX, minExtentY, \
funcVisitTile, funcTileBlocked)
# Northwest quadrant
__checkQuadrant(visited, startX, startY, -1, 1, \
minExtentX, maxExtentY, \
funcVisitTile, funcTileBlocked)
#-------------------------------------------------------------
class __Line(object):
def __init__(self, xi, yi, xf, yf):
self.xi = xi
self.yi = yi
self.xf = xf
self.yf = yf
dx = property(fget = lambda self: self.xf - self.xi)
dy = property(fget = lambda self: self.yf - self.yi)
def pBelow(self, x, y):
return self.relativeSlope(x, y) > 0
def pBelowOrCollinear(self, x, y):
return self.relativeSlope(x, y) >= 0
def pAbove(self, x, y):
return self.relativeSlope(x, y) < 0
def pAboveOrCollinear(self, x, y):
return self.relativeSlope(x, y) <= 0
def pCollinear(self, x, y):
return self.relativeSlope(x, y) == 0
def lineCollinear(self, line):
return self.pCollinear(line.xi, line.yi) \
and self.pCollinear(line.xf, line.yf)
def relativeSlope(self, x, y):
return (self.dy * (self.xf - x)) \
- (self.dx * (self.yf - y))
class __ViewBump:
def __init__(self, x, y, parent):
self.x = x
self.y = y
self.parent = parent
class __View:
def __init__(self, shallowLine, steepLine):
self.shallowLine = shallowLine
self.steepLine = steepLine
self.shallowBump = None
self.steepBump = None
def __checkQuadrant(visited, startX, startY, dx, dy, \
extentX, extentY, funcVisitTile, funcTileBlocked):
activeViews = []
shallowLine = __Line(0, 1, extentX, 0)
steepLine = __Line(1, 0, 0, extentY)
activeViews.append( __View(shallowLine, steepLine) )
viewIndex = 0
# Visit the tiles diagonally and going outwards
#
# .
# .
# . .
# 9 .
# 5 8 .
# 2 4 7
# @ 1 3 6 . . .
maxI = extentX + extentY
i = 1
while i != maxI + 1 and len(activeViews) > 0:
if 0 > i - extentX:
startJ = 0
else:
startJ = i - extentX
if i < extentY:
maxJ = i
else:
maxJ = extentY
j = startJ
while j != maxJ + 1 and viewIndex < len(activeViews):
x = i - j
y = j
__visitCoord(visited, startX, startY, x, y, dx, dy, \
viewIndex, activeViews, \
funcVisitTile, funcTileBlocked)
j += 1
i += 1
def __visitCoord(visited, startX, startY, x, y, dx, dy, viewIndex, \
activeViews, funcVisitTile, funcTileBlocked):
# The top left and bottom right corners of the current coordinate.
topLeft = (x, y + 1)
bottomRight = (x + 1, y)
while viewIndex < len(activeViews) \
and activeViews[viewIndex].steepLine.pBelowOrCollinear( \
bottomRight[0], bottomRight[1]):
# The current coordinate is above the current view and is
# ignored. The steeper fields may need it though.
viewIndex += 1
if viewIndex == len(activeViews) \
or activeViews[viewIndex].shallowLine.pAboveOrCollinear( \
topLeft[0], topLeft[1]):
# Either the current coordinate is above all of the fields
# or it is below all of the fields.
return
# It is now known that the current coordinate is between the steep
# and shallow lines of the current view.
isBlocked = False
# The real quadrant coordinates
realX = x * dx
realY = y * dy
if (startX + realX, startY + realY) not in visited:
visited.add((startX + realX, startY + realY))
funcVisitTile(startX + realX, startY + realY)
"""else:
# Debugging
print (startX + realX, startY + realY)"""
isBlocked = funcTileBlocked(startX + realX, startY + realY)
if not isBlocked:
# The current coordinate does not block sight and therefore
# has no effect on the view.
return
if activeViews[viewIndex].shallowLine.pAbove( \
bottomRight[0], bottomRight[1]) \
and activeViews[viewIndex].steepLine.pBelow( \
topLeft[0], topLeft[1]):
# The current coordinate is intersected by both lines in the
# current view. The view is completely blocked.
del activeViews[viewIndex]
elif activeViews[viewIndex].shallowLine.pAbove( \
bottomRight[0], bottomRight[1]):
# The current coordinate is intersected by the shallow line of
# the current view. The shallow line needs to be raised.
__addShallowBump(topLeft[0], topLeft[1], \
activeViews, viewIndex)
__checkView(activeViews, viewIndex)
elif activeViews[viewIndex].steepLine.pBelow( \
topLeft[0], topLeft[1]):
# The current coordinate is intersected by the steep line of
# the current view. The steep line needs to be lowered.
__addSteepBump(bottomRight[0], bottomRight[1], activeViews, \
viewIndex)
__checkView(activeViews, viewIndex)
else:
# The current coordinate is completely between the two lines
# of the current view. Split the current view into two views
# above and below the current coordinate.
shallowViewIndex = viewIndex
viewIndex += 1
steepViewIndex = viewIndex
activeViews.insert(shallowViewIndex, \
copy.deepcopy(activeViews[shallowViewIndex]))
__addSteepBump(bottomRight[0], bottomRight[1], \
activeViews, shallowViewIndex)
if not __checkView(activeViews, shallowViewIndex):
viewIndex -= 1
steepViewIndex -= 1
__addShallowBump(topLeft[0], topLeft[1], activeViews, \
steepViewIndex)
__checkView(activeViews, steepViewIndex)
def __addShallowBump(x, y, activeViews, viewIndex):
activeViews[viewIndex].shallowLine.xf = x
activeViews[viewIndex].shallowLine.yf = y
activeViews[viewIndex].shallowBump = __ViewBump(x, y, \
activeViews[viewIndex].shallowBump)
curBump = activeViews[viewIndex].steepBump
while curBump is not None:
if activeViews[viewIndex].shallowLine.pAbove( \
curBump.x, curBump.y):
activeViews[viewIndex].shallowLine.xi = curBump.x
activeViews[viewIndex].shallowLine.yi = curBump.y
curBump = curBump.parent
def __addSteepBump(x, y, activeViews, viewIndex):
activeViews[viewIndex].steepLine.xf = x
activeViews[viewIndex].steepLine.yf = y
activeViews[viewIndex].steepBump = __ViewBump(x, y, \
activeViews[viewIndex].steepBump)
curBump = activeViews[viewIndex].shallowBump
while curBump is not None:
if activeViews[viewIndex].steepLine.pBelow( \
curBump.x, curBump.y):
activeViews[viewIndex].steepLine.xi = curBump.x
activeViews[viewIndex].steepLine.yi = curBump.y
curBump = curBump.parent
def __checkView(activeViews, viewIndex):
"""
Removes the view in activeViews at index viewIndex if
- The two lines are coolinear
- The lines pass through either extremity
"""
shallowLine = activeViews[viewIndex].shallowLine
steepLine = activeViews[viewIndex].steepLine
if shallowLine.lineCollinear(steepLine) \
and ( shallowLine.pCollinear(0, 1) \
or shallowLine.pCollinear(1, 0) ):
del activeViews[viewIndex]
return False
else:
return True

60
grid.py Normal file
View File

@ -0,0 +1,60 @@
class Grid():
def __init__(self, x, y, initial):
self.x = x
self.y = y
self.initial = initial
self.data = [initial] * x * y
def get(self, x, y):
return self.data[self.x * y + x]
def set(self, x, y, v):
self.data[self.x * y + x] = v
def in_bounds(self, x, y):
return x < self.x and x >= 0 and y < self.y and y >= 0
def __getitem__(self, coord):
(x, y) = coord
return self.get(x, y)
def __setitem__(self, coord, value):
(x, y) = coord
self.set(x, y, value)
def neighbors(self, x, y):
r = []
for direction in [(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)]:
xc, yc = direction
tx, ty = x + xc, y + yc
if self.in_bounds(tx, ty):
r.append((tx, ty))
return r
def bresenhams(x1, y1, x2, y2):
r = []
dx = abs(x2 - x1)
dy = abs(y2 - y1)
x, y = x1, y1
sx = -1 if x1 > x2 else 1
sy = -1 if y1 > y2 else 1
if dx > dy:
err = dx / 2.0
while x != x2:
r.append((x, y))
err -= dy
if err < 0:
y += sy
err += dx
x += sx
else:
err = dy / 2.0
while y != y2:
r.append((x, y))
err -= dx
if err < 0:
x += sx
err += dy
y += sy
r.append((x, y))
return r

243
rougelike.py Executable file
View File

@ -0,0 +1,243 @@
#!/usr/bin/python3
import collections as cl
import networkx as nx
import curses as c
import random
from grid import Grid, bresenhams
from fov import fieldOfView
class Tile():
def __init__(self, glyph, name, description, solid=False, transparent=True, color=c.COLOR_WHITE):
self.appearance = glyph
self.name = name
self.description = description
self.solid = solid
self.transparent = transparent
self.color = color
def draw(self):
return (self.glyph, self.color)
def describe(self):
return self.description
class World(Grid):
def __init__(self, window, width, height, initial_tile):
super().__init__(width, height, initial_tile)
self.run = True
self.window = window
self.width = width
self.height = height
self.bodies = []
self.behavior = []
class Spirit():
def __init__(self, name):
self.name = name
self.body = None
self.world = None
def behave(self):
pass
def incarnate(self, body):
if not self.body is None:
self.body.behavior = None
if not body.behavior is None:
body.behavior.body = None
self.body = body
body.behavior = self
def message(self, msg):
pass
class Player(Spirit):
def __init__(self, name):
super().__init__(name)
self.msgbuffer = cl.deque([], 200)
self.messaged = False
def message(self, msg):
if len(self.msgbuffer) > 0 and msg == self.msgbuffer[0][1]:
self.msgbuffer.appendleft((self.msgbuffer.pop()[0] + 1, msg))
else:
self.msgbuffer.appendleft((1, msg))
self.messaged = True
def behave(self):
# calculate visibility
visibility = Grid(self.world.width, self.world.height, False)
fieldOfView(self.body.x, self.body.y, self.world.width, self.world.height, 100, lambda x, y: visibility.set(x, y, True), lambda x, y: self.world.get(x, y).solid)
self.world.window.erase()
# draw interface
for x in range(0, self.world.width):
for y in range(0, self.world.height):
if visibility.get(x, y):
self.world.window.addch(y, x, self.world.get(x, y).appearance)
for body in self.world.bodies:
if visibility.get(body.x, body.y):
body.draw()
if not len(self.msgbuffer) == 0 and self.messaged:
if self.msgbuffer[0][0] == 1:
self.world.window.addstr(self.world.height + 1, 0, self.msgbuffer[0][1])
else:
self.world.window.addstr(self.world.height + 1, 0, self.msgbuffer[0][1] + " (x" + str(self.msgbuffer[0][0]) + ")")
self.messaged = False
self.world.window.refresh()
# this is the one gameplay input point.
key = self.world.window.getkey()
match key:
case 'w':
if not self.body.move((0, -1)):
self.message("You cannot go there.")
case 's':
if not self.body.move((0, +1)):
self.message("You cannot go there.")
case 'a':
if not self.body.move((-1, 0)):
self.message("You cannot go there.")
case 'd':
if not self.body.move((+1, 0)):
self.message("You cannot go there.")
case 'c':
coords = self.world.neighbors(self.body.x, self.body.y)
for coord in coords:
x, y = coord
t = self.world.get(x, y)
if t.name == "open door":
self.body.close_door(x, y)
break
else:
self.message("You see nothing here to close.")
case 'q':
self.world.run = False
# case _: self.world.message(key)
class Drunkard(Spirit):
def behave(self):
while True:
dx, dy = random.randint(-1,1), random.randint(-1,1)
tx, ty = dx + self.body.x, dy + self.body.y
if self.world.in_bounds(tx, ty) and not self.world.get(tx, ty).solid:
break
bodies = [body for body in self.world.bodies if body.x == tx and body.y == ty]
if bodies:
for tb in bodies:
tb.behavior.message("A " + self.body.name + " bumps into you.")
else:
self.body.move((dx, dy))
class Body():
def __init__(self, name, appearence, description, color):
self.name = name
self.appearence = appearence
self.description = description
self.color = color
self.behavior = None
self.world = None
self.x = None
self.y = None
def move(self, direction):
x, y = direction
tx, ty = self.x + x, self.y + y
if not self.world.in_bounds(tx, ty):
return False
elif self.world.get(tx, ty).name == "closed door":
self.open_door(tx, ty)
return True
elif self.world.get(tx, ty).solid:
return False
else:
self.x = tx
self.y = ty
return True
def open_door(self, x, y):
if not self.world.in_bounds(x, y):
self.behavior.message("You cannot open anything there.")
return False
elif self.world.get(x, y).name == "closed door":
self.world.set(x, y, door_open)
self.behavior.message("You open the door.")
return True
else:
self.behavior.message("You cannot open anything there.")
return False
def close_door(self, x, y):
if not self.world.in_bounds(x, y):
self.behavior.message("You cannot close anything there.")
return False
elif self.world.get(x, y).name == "open door":
self.world.set(x, y, door_closed)
self.behavior.message("You close the door.")
return True
else:
self.behavior.message("You cannot close anything there.")
return False
def describe(self):
return self.description
def draw(self):
self.world.window.addch(self.y, self.x, self.appearence)
p = Player("you")
pb = Body("yourself", '@', "an unremarkable person", c.COLOR_WHITE)
p.incarnate(pb)
floor = Tile('.', "floor", "an unremarkable piece of floor")
wall = Tile('#', "wall", "an unremarkable wall", solid=True, transparent=False)
door_closed = Tile('+', "closed door", "an unremarkable door", solid=True, transparent=False)
door_open = Tile(',', "open door", "an unremarkable door", solid=False, transparent=True)
w = c.initscr()
c.curs_set(0)
c.raw()
wld = World(w, 80, 23, floor)
drunk = Drunkard("drunkard")
db = Body("drunkard", 'H', "A smelly drunkard, stumbling about.", c.COLOR_WHITE)
drunk.incarnate(db)
db.world = wld
drunk.world = wld
db.x = 20
db.y = 16
wld.behaviors = [p, drunk]
wld.bodies = [pb, db]
pb.world = wld
p.world = wld
pb.x = 3
pb.y = 6
wld.set(12, 12, wall)
wld.set(13, 12, wall)
wld.set(14, 12, wall)
wld.set(15, 12, wall)
wld.set(16, 12, wall)
wld.set(16, 13, wall)
wld.set(16, 14, wall)
wld.set(18, 12, wall)
wld.set(18, 13, wall)
wld.set(18, 14, wall)
wld.set(12, 13, door_closed)
while wld.run:
for behavior in wld.behaviors:
behavior.behave()
c.endwin()