Created
September 17, 2017 07:24
-
-
Save carlfindahl/7c3e34d453169c06096def63b64de568 to your computer and use it in GitHub Desktop.
DailyProgrammer Weekly #25 Challenge - Escape The Trolls. A solution using Python 3 and Curses
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
# TROLLBYRINTH - FOR /R/DAILYPROGRAMMER WEEKLY #25 - BY CARL F. (2016) | |
import curses | |
import random | |
from collections import deque | |
from functools import partial | |
LABYRINTH = [\ | |
"#########################################################################", | |
"# # # # # # #", | |
"# # ######### # ##### ######### ##### ##### ##### # #", | |
"# # # # # # # # # #", | |
"######### # ######### ######### ##### # # # ######### #", | |
"# # # # # # # # # # #", | |
"# # ############# # # ######### ##### # ######### # #", | |
"# # # # # # # # # #", | |
"# ############# ##### ##### # ##### ######### # ##### #", | |
"# # # # # # # # # #", | |
"# ##### ##### # ##### # ######### # # # #############", | |
"# # # # # # # # # # # #", | |
"############# # # # ######### # ##### # ##### ##### #", | |
"# # # # # # # # # #", | |
"# ##### # ######### ##### # ##### ##### ############# #", | |
"# # # # # # # # # #", | |
"# # ######### # ##### ######### # # ############# # #", | |
"# # # # # # # # # # #", | |
"# ######### # # # ##### ######### ######### # #########", | |
"# # # # # # # # # #", | |
"# # ##### ##### ##### ######### ##### # ######### # #", | |
"# # # # # # #", | |
"# X #####################################################################"] | |
class Vector2D(object): | |
"""Defines a location vector (x, y) that can be added /\ | |
multiplied etc by another Vector2D""" | |
def __init__(self, x, y): | |
self.x = x | |
self.y = y | |
@classmethod | |
def RightVector(self): | |
return Vector2D(1, 0) | |
@classmethod | |
def UpVector(self): | |
return Vector2D(0, -1) | |
@classmethod | |
def LeftVector(self): | |
return Vector2D(-1, 0) | |
@classmethod | |
def DownVector(self): | |
return Vector2D(0, 1) | |
def __str__(self): | |
return "X = {0.x} Y = {0.y}".format(self) | |
# COMPARISONS | |
def __eq__(self, other): | |
return self.x == other.x and self.y == other.y | |
def __ne__(self, other): | |
return self.x != other.x or self.y != other.y | |
def __lt__(self, other): | |
return self.x + self.y < other.x + other.y | |
def __gt__(self, other): | |
return self.x + self.y > other.x + other.y | |
def __le__(self, other): | |
return self.x + self.y <= other.x + other.y | |
def __ge__(self, other): | |
return self.x + self.y >= other.x + other.y | |
# MATH | |
def __radd__(self, other): | |
return self.__add__(other) | |
def __rsub__(self, other): | |
return self.__sub__(other) | |
def __rmul__(self, other): | |
return self.__mul__(other) | |
def __rtruediv__(self, other): | |
return self.__div__(other) | |
def __add__(self, other): | |
return Vector2D(self.x + other.x, self.y + other.y) | |
def __sub__(self, other): | |
return Vector2D(self.x - other.x, self.y - other.y) | |
def __mul__(self, other): | |
try: | |
if other[1]: | |
return Vector2D(self.x * other.x, self.y * other.y) | |
except: | |
return Vector2D(self.x * other, self.y * other) | |
def __truediv__(self, other): | |
try: | |
if other[1]: | |
return Vector2D(self.x / other.x, self.y / other.y) | |
except: | |
return Vector2D(self.x / other, self.y / other) | |
def __iadd__(self, other): | |
return self.__add__(other) | |
def __isub__(self, other): | |
return self.__sub__(other) | |
def __imul__(self, other): | |
return self.__mul__(other) | |
def __itruediv__(self, other): | |
return self.__truediv__(other) | |
class Queue(object): | |
def __init__(self): | |
self.elements = deque() | |
def IsEmptyQueue(self): | |
return len(self.elements) == 0 | |
def Put(self, elem): | |
self.elements.append(elem) | |
def Get(self): | |
return self.elements.popleft() | |
class Event(object): | |
def __init__(self, key, func): | |
self.key = key | |
self.func = func | |
def __call__(self): | |
self.func() | |
class Creature(object): | |
def __init__(self, world, location, graphic, color, canPushBlocks=True): | |
self.graphic = graphic | |
self.color = color | |
self.world = world | |
self.location = location | |
self.canPushBlocks = canPushBlocks | |
def Die(self): | |
del self | |
def Move(self, direction): | |
ApproachedTile = self.world.GetTile(self.location + direction) | |
if ApproachedTile and ApproachedTile.passable: | |
self.location += direction | |
elif ApproachedTile and ApproachedTile.pushable: | |
if ApproachedTile.Push(direction): | |
self.location += direction | |
class Player(Creature): | |
def __init__(self, world, location): | |
super().__init__(world, location, "@", 1, True) | |
self.BindPlayerInput() | |
def Move(self, direction): | |
super().Move(direction) | |
if self.world.GetCreature(self.location): | |
self.Die() | |
self.world.application.Running = False | |
self.world.application.bDidWin = False | |
elif self.world.GetTile(self.location).isExit: | |
self.world.application.Running = False | |
self.world.application.bDidWin = True | |
def BindPlayerInput(self): | |
Bind = self.world.application.BindEvent | |
Bind(ord('w'), partial(self.Move, Vector2D(0, -1))) | |
Bind(ord('a'), partial(self.Move, Vector2D(-1, 0))) | |
Bind(ord('s'), partial(self.Move, Vector2D(0, 1))) | |
Bind(ord('d'), partial(self.Move, Vector2D(1, 0))) | |
Bind(curses.KEY_UP, partial(self.Move, Vector2D(0, -1))) | |
Bind(curses.KEY_LEFT, partial(self.Move, Vector2D(-1, 0))) | |
Bind(curses.KEY_DOWN, partial(self.Move, Vector2D(0, 1))) | |
Bind(curses.KEY_RIGHT, partial(self.Move, Vector2D(1, 0))) | |
def Update(self): | |
pass | |
class Troll(Creature): | |
def __init__(self, world, location): | |
super().__init__(world, location, "T", 3, True) | |
def Die(self): | |
self.world.trolls.remove(self) | |
super().Die() | |
def PathWithBreadthSearch(self, targetTile): | |
pathingQueue = Queue() | |
pathingQueue.Put(self.world.GetTile(self.location)) | |
cameFrom = {} | |
cameFrom[self.world.GetTile(self.location)] = None | |
while not pathingQueue.IsEmptyQueue(): | |
currentTile = pathingQueue.Get() | |
if currentTile == targetTile: | |
break | |
for tile in currentTile.adjacentTiles: | |
if tile and tile.passable and tile not in cameFrom: | |
pathingQueue.Put(tile) | |
cameFrom[tile] = currentTile | |
return cameFrom | |
def EvaluatePath(self, pathDict, targetTile): | |
try: | |
cameFrom = pathDict[targetTile] | |
while True: | |
if pathDict[cameFrom] and not\ | |
pathDict[cameFrom].location == self.location: | |
cameFrom = pathDict[cameFrom] | |
else: | |
break | |
return cameFrom | |
except: | |
return self.world.GetTile(self.location) | |
def Update(self): | |
paths = self.PathWithBreadthSearch(self.world.GetPlayerLocation()) | |
target = self.EvaluatePath(paths, self.world.GetPlayerLocation()) | |
if not target.IsOccupied(): | |
self.location = target.location | |
class World(object): | |
def __init__(self, application, mapToInitialize): | |
self.tiles = [] | |
self.spawnPoints = [] | |
self.application = application | |
self.InitMap(mapToInitialize) | |
self.window = curses.newwin(len(mapToInitialize),\ | |
len(mapToInitialize[0])) | |
self.window.keypad(True) | |
self.player = None | |
self.trolls = [] | |
self.SpawnCreature(True) | |
self.SpawnCreature() | |
self.SpawnCreature() | |
self.SpawnCreature() | |
self.SpawnCreature() | |
self.SpawnCreature() | |
self.SpawnCreature() | |
self.SpawnCreature() | |
def InitMap(self, mapToInitialize): | |
for y, row in enumerate(mapToInitialize): | |
for x, character in enumerate(row): | |
if character == "#": | |
self.tiles.append(Tile(self, Vector2D(x, y),\ | |
curses.ACS_CKBOARD, 0,\ | |
curses.A_NORMAL, False, True)) | |
elif character == " ": | |
self.spawnPoints.append(Vector2D(x, y)) | |
self.tiles.append(Tile(self, Vector2D(x, y),\ | |
ord(" "), 0, curses.A_NORMAL,\ | |
True, False)) | |
elif character == "X": | |
self.tiles.append(Tile(self, Vector2D(x, y),\ | |
ord('X'), 2, curses.A_BOLD,\ | |
True, False, True)) | |
self.FixIfIsEdgeTile(self.tiles[-1], mapToInitialize) | |
for tile in self.tiles: | |
tile.adjacentTiles = tile.GetAdjacent() | |
def FixIfIsEdgeTile(self, tile, mapToInitialize): | |
if tile.location.x == 0 or\ | |
tile.location.x == len(mapToInitialize) - 1 or\ | |
tile.location.y == 0 or\ | |
tile.location.y == len(mapToInitialize[0]) - 1: | |
tile.pushable = False | |
def GetTile(self, atLocation): | |
for tile in self.tiles: | |
if tile.location == atLocation: | |
return tile | |
return None | |
def GetPlayerLocation(self): | |
return self.GetTile(self.player.location) | |
def GetCreature(self, location): | |
for troll in self.trolls: | |
if troll.location == location: | |
return troll | |
return None | |
def SpawnCreature(self, spawnPlayer=False): | |
spawnLocation = random.choice(self.spawnPoints) | |
if spawnPlayer: | |
self.player = Player(self, spawnLocation) | |
else: | |
self.trolls.append(Troll(self, spawnLocation)) | |
def Update(self): | |
for troll in self.trolls: | |
troll.Update() | |
def Draw(self): | |
self.window.erase() | |
self.DrawMap() | |
self.DrawCreatures() | |
self.window.refresh() | |
def DrawMap(self): | |
for tile in self.tiles: | |
try: | |
self.window.addch(tile.location.y, tile.location.x,\ | |
tile.graphic, curses.color_pair(tile.color)\ | |
+ tile.attr) | |
except: | |
pass | |
def DrawCreatures(self): | |
self.window.addch(self.player.location.y, self.player.location.x,\ | |
self.player.graphic,\ | |
curses.color_pair(self.player.color)) | |
for troll in self.trolls: | |
self.window.addch(troll.location.y, troll.location.x,\ | |
troll.graphic, curses.color_pair(troll.color)) | |
class Tile(object): | |
def __init__(self, world, location, graphic,\ | |
color, attr, passable, pushable, isExit=False): | |
self.world = world | |
self.location = location | |
self.graphic = graphic | |
self.color = color | |
self.attr = attr | |
self.passable = passable | |
self.pushable = pushable | |
self.isExit = isExit | |
self.adjacentTiles = [] | |
def Push(self, direction): | |
approachedTile = self.world.GetTile(self.location + direction) | |
if approachedTile and approachedTile.passable: | |
approachedTile.location = self.location | |
self.location += direction | |
self.UpdateAdjacentAfterPush(approachedTile) | |
if self.world.GetCreature(self.location): | |
self.world.GetCreature(self.location).Die() | |
return True | |
return False | |
def IsOccupied(self): | |
if self.world.player.location == self.location: | |
return False | |
for troll in self.world.trolls: | |
if troll.location == self.location: | |
return True | |
return False | |
def UpdateAdjacentAfterPush(self, approachedTile): | |
self.adjacentTiles = self.GetAdjacent() | |
approachedTile.adjacentTiles = approachedTile.GetAdjacent() | |
for tile in approachedTile.adjacentTiles: | |
tile.adjacentTiles = tile.GetAdjacent() | |
for tile in self.adjacentTiles: | |
tile.adjacentTiles = tile.GetAdjacent() | |
def GetAdjacent(self): | |
adjacent = [] | |
GetTile = self.world.GetTile | |
adjacent.append(GetTile(self.location + Vector2D(0, -1))) | |
adjacent.append(GetTile(self.location + Vector2D(1, 0))) | |
adjacent.append(GetTile(self.location + Vector2D(-1, 0))) | |
adjacent.append(GetTile(self.location + Vector2D(0, 1))) | |
return adjacent | |
class Trollbyrinth(object): | |
def __init__(self, stdscr): | |
self.Screen = stdscr | |
self.Running = True | |
curses.curs_set(0) | |
curses.raw() | |
# SET UP COLOR PAIRS | |
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) | |
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) | |
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) | |
curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_BLACK) | |
curses.init_pair(5, curses.COLOR_BLUE, curses.COLOR_BLACK) | |
curses.init_pair(6, curses.COLOR_MAGENTA, curses.COLOR_BLACK) | |
curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_GREEN) | |
curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_YELLOW) | |
curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED) | |
curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_CYAN) | |
curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_BLUE) | |
curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_MAGENTA) | |
self.Loading() | |
# EVENTS | |
self.events = [] | |
self.BindEvent(ord('q'), self.Quit) | |
# GAME OBJECTS | |
self.world = World(self, LABYRINTH) | |
# GAME DATA | |
self.Turn = 1 | |
self.bDidWin = False | |
# MAIN LOOP | |
self.MainLoop() | |
def Loading(self): | |
self.Screen.addstr("LOADING & INITIALIZING TILES...", | |
curses.color_pair(2)) | |
self.Screen.refresh() | |
def GameOver(self): | |
endGameWindow = curses.newwin(curses.LINES, curses.COLS, 0, 0) | |
if self.bDidWin: | |
endGameWindow.addstr(curses.LINES // 2, 0, "{0:^{1}}".format\ | |
("YOU WON! CONGRATULATIONS!", curses.COLS),\ | |
curses.color_pair(1) + curses.A_BOLD) | |
endGameWindow.addstr(curses.LINES // 2 + 1, 0, "{0:^{1}}".format\ | |
("SCORE: {0}".format(self.GetScore()), curses.COLS),\ | |
curses.color_pair(2) + curses.A_BOLD) | |
else: | |
endGameWindow.addstr(curses.LINES // 2, 0, "{0:^{1}}".format\ | |
("GAME OVER! THE TROLLS GOT THE BETTER OF YOU!",\ | |
curses.COLS), curses.color_pair(3) + curses.A_BOLD) | |
endGameWindow.getch() | |
def GetScore(self): | |
return (200 - self.Turn) + (7 - len(self.world.trolls)) * 5 | |
def Quit(self): | |
self.Running = False | |
def BindEvent(self, key, func): | |
self.events.append(Event(key, func)) | |
def HandleInput(self): | |
KeyboardEvent = self.world.window.getch() | |
for event in self.events: | |
if event.key == KeyboardEvent: | |
event() | |
def Update(self): | |
self.Screen.addstr(24, 0, "TURN: ", curses.A_BOLD) | |
self.Screen.addstr(str(self.Turn)) | |
self.Screen.addstr(24, 12, "TROLLS ALIVE: ", curses.A_BOLD) | |
self.Screen.addstr(str(len(self.world.trolls))) | |
self.Screen.addstr(25, 0, "SCORE: ", curses.A_BOLD) | |
self.Screen.addstr(str(self.GetScore())) | |
self.Screen.refresh() | |
self.Turn += 1 | |
def MainLoop(self): | |
self.world.Draw() | |
self.Update() | |
while self.Running: | |
self.HandleInput() | |
self.world.Update() | |
self.world.Draw() | |
self.Update() | |
self.GameOver() | |
if __name__ == "__main__": | |
curses.wrapper(Trollbyrinth) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment