Browse Source

rouge

master
whiteline 2 months ago
parent
commit
d5d80345fc
4 changed files with 629 additions and 2 deletions
  1. +2
    -2
      coding-terminal
  2. +324
    -0
      fov.py
  3. +60
    -0
      grid.py
  4. +243
    -0
      rougelike.py

+ 2
- 2
coding-terminal View File

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

+ 324
- 0
fov.py 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
- 0
grid.py 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
- 0
rougelike.py 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()

Loading…
Cancel
Save