From d5d80345fc3ce7c7c3ac58253b8d62caf8f29162 Mon Sep 17 00:00:00 2001 From: whiteline Date: Mon, 26 Feb 2024 19:25:32 +0100 Subject: [PATCH] rouge --- coding-terminal | 4 +- fov.py | 324 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ grid.py | 60 +++++++++++ rougelike.py | 243 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 fov.py create mode 100644 grid.py create mode 100755 rougelike.py diff --git a/coding-terminal b/coding-terminal index 91592bf..97c2f1b 100755 --- a/coding-terminal +++ b/coding-terminal @@ -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 diff --git a/fov.py b/fov.py new file mode 100644 index 0000000..6efd5f3 --- /dev/null +++ b/fov.py @@ -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 + diff --git a/grid.py b/grid.py new file mode 100644 index 0000000..5ff92c3 --- /dev/null +++ b/grid.py @@ -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 diff --git a/rougelike.py b/rougelike.py new file mode 100755 index 0000000..5305a9b --- /dev/null +++ b/rougelike.py @@ -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()