diff --git a/bomberman/__init__.py b/bomberman/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bomberman/entity.py b/bomberman/entity.py new file mode 100644 index 00000000..4c16ff86 --- /dev/null +++ b/bomberman/entity.py @@ -0,0 +1,300 @@ +from math import copysign + +############ +# Entities # +############ + +class Entity(object): + """Abstract entity class""" + pass + +##################### +# Positional entity # +##################### + +class PositionalEntity(Entity): + """Entity with a position""" + + # PARAM x [int]: x coordinate in the grid + # PARAM y [int]: y coordinate in the grid + def __init__(self, x, y): + """Class constructor""" + self.x = x + self.y = y + + ################### + # Private methods # + ################### + + def __eq__(self, other): + if(not other): return False + return (self.x, self.y) == (other.x, other.y) + + def __ne__(self, other): + return not(self == other) + +################## +# Movable entity # +################## + +def __sign__(x): + if x == 0.0: + return 0 + return int(copysign(1, x)) + +class MovableEntity(PositionalEntity): + """Positional entity that can move""" + + # PARAM x [int]: x coordinate in the grid + # PARAM y [int]: y coordinate in the grid + def __init__(self, x, y): + """Class constructor""" + super().__init__(x, y) + self.dx = 0 + self.dy = 0 + + # Sets the desired direction of the entity + # PARAM dx [int]: delta x + # PARAM dy [int]: delta y + # The passed values are clamped in [-1,1] + def move(self, dx, dy): + """Move entity""" + # Make sure dx is in [-1,1] + self.dx = __sign__(dx) + # Make sure dy is in [-1,1] + self.dy = __sign__(dy) + + # Returns the next position of this entity as a tuple (x,y) + def nextpos(self): + """Returns the next position of this entity""" + return (self.x + self.dx, self.y + self.dy) + + ################### + # Private methods # + ################### + + def __eq__(self, other): + if(not other): return False + return (super().__eq__(other) and + (self.dx, self.dy) == (other.dx, other.dy)) + + def __ne__(self, other): + return not(self == other) + +################ +# Timed entity # +################ + +class TimedEntity(Entity): + """Entity with a time limit""" + + def __init__(self, timer): + """Class constructor""" + self.timer = timer + + def tick(self): + """Performs a clock tick""" + self.timer = self.timer - 1 + + def expired(self): + return self.timer < 0 + + ################### + # Private methods # + ################### + + def __eq__(self, other): + if(not other): return False + return self.timer == other.timer + + def __ne__(self, other): + return not(self == other) + +############# +# AI entity # +############# + +class AIEntity(Entity): + """Entity with an AI""" + + # PARAM name [string]: A unique name for this entity + # PARAM rep [character]: A single character used to draw the entity + def __init__(self, name, avatar): + self.name = name + self.avatar = avatar[0] + + def do(self, wrld): + """Pick an action for the entity given the world state""" + pass + + ################### + # Private methods # + ################### + + def __eq__(self, other): + if(not other): return False + return self.name == other.name + + def __ne__(self, other): + return not(self == other) + +################ +# Owned entity # +################ + +class OwnedEntity(Entity): + """Entity with an owner""" + + def __init__(self, owner): + self.owner = owner + + ################### + # Private methods # + ################### + + def __eq__(self, other): + if(not other): return False + return self.owner == other.owner + + def __ne__(self, other): + return not(self == other) + +############### +# Bomb entity # +############### + +class BombEntity(PositionalEntity, TimedEntity, OwnedEntity): + """Bomb entity""" + + def __init__(self, x, y, timer, character): + PositionalEntity.__init__(self, x, y) + TimedEntity.__init__(self, timer) + OwnedEntity.__init__(self, character) + + ################### + # Private methods # + ################### + + def __eq__(self, other): + if(not other): return False + return (super(PositionalEntity, self).__eq__(other) and + super(TimedEntity, self).__eq__(other) and + super(OwnedEntity, self).__eq__(other)) + + def __ne__(self, other): + return not(self == other) + +#################### +# Explosion entity # +#################### + +class ExplosionEntity(PositionalEntity, TimedEntity, OwnedEntity): + """Explosion entity""" + + def __init__(self, x, y, timer, character): + PositionalEntity.__init__(self, x, y) + TimedEntity.__init__(self, timer) + OwnedEntity.__init__(self, character) + + ################### + # Private methods # + ################### + + def __eq__(self, other): + if(not other): return False + return (super(PositionalEntity, self).__eq__(other) and + super(TimedEntity, self).__eq__(other) and + super(OwnedEntity, self).__eq__(other)) + + def __ne__(self, other): + return not(self == other) + +################## +# Monster entity # +################## + +class MonsterEntity(AIEntity, MovableEntity): + """Monster Entity""" + + def __init__(self, name, avatar, x, y): + AIEntity.__init__(self, name, avatar) + MovableEntity.__init__(self, x, y) + + ################### + # Private methods # + ################### + + @classmethod + def from_monster(cls, monster): + """Clone this monster""" + new = MonsterEntity(monster.name, monster.avatar, monster.x, monster.y) + new.dx = monster.dx + new.dy = monster.dy + return new + + ################### + # Private methods # + ################### + + def __hash__(self): + return hash((self.name, self.x, self.y)) + + def __eq__(self, other): + if(not other): return False + return (super(MovableEntity, self).__eq__(other) and + super(AIEntity, self).__eq__(other)) + + def __ne__(self, other): + return not(self == other) + +#################### +# Character entity # +#################### + +class CharacterEntity(AIEntity, MovableEntity): + """Basic definitions for a custom-made character""" + + def __init__(self, name, avatar, x, y): + AIEntity.__init__(self, name, avatar) + MovableEntity.__init__(self, x, y) + # Whether this character wants to place a bomb + self.maybe_place_bomb = False + # Debugging elements + self.tiles = {} + + def place_bomb(self): + """Attempts to place a bomb""" + self.maybe_place_bomb = True + + def set_cell_color(self, x, y, color): + """Sets the cell color at (x,y)""" + self.tiles[(x,y)] = color + + def done(self, wrld): + pass + + ################### + # Private methods # + ################### + + @classmethod + def from_character(cls, character): + """Clone this character""" + new = CharacterEntity(character.name, character.avatar, character.x, character.y) + new.dx = character.dx + new.dy = character.dy + new.maybe_place_bomb = character.maybe_place_bomb + return new + + def __hash__(self): + return hash((self.name, self.x, self.y)) + + def __eq__(self, other): + if(not other): return False + return (self.maybe_place_bomb == other.maybe_place_bomb and + super(MovableEntity, self).__eq__(other) and + super(AIEntity, self).__eq__(other)) + + def __ne__(self, other): + return not(self == other) + diff --git a/bomberman/events.py b/bomberman/events.py new file mode 100644 index 00000000..9eea8663 --- /dev/null +++ b/bomberman/events.py @@ -0,0 +1,27 @@ +class Event: + + BOMB_HIT_WALL = 0 + BOMB_HIT_MONSTER = 1 + BOMB_HIT_CHARACTER = 2 + CHARACTER_KILLED_BY_MONSTER = 3 + CHARACTER_FOUND_EXIT = 4 + + def __init__(self, tpe, character, other=None): + self.tpe = tpe + self.character = character + self.other = other + + def __str__(self): + if self.tpe == self.BOMB_HIT_WALL: + return self.character.name + "'s bomb hit a wall" + if self.tpe == self.BOMB_HIT_MONSTER: + return self.character.name + "'s bomb hit a monster" + if self.tpe == self.BOMB_HIT_CHARACTER: + if self.character != self.other: + return self.character.name + "'s bomb hit " + self.other.name + else: + return self.character.name + " killed itself" + if self.tpe == self.CHARACTER_KILLED_BY_MONSTER: + return self.character.name + " was killed by " + self.other.name + if self.tpe == self.CHARACTER_FOUND_EXIT: + return self.character.name + " found the exit" diff --git a/bomberman/game.py b/bomberman/game.py new file mode 100644 index 00000000..9370d941 --- /dev/null +++ b/bomberman/game.py @@ -0,0 +1,147 @@ +from real_world import RealWorld +from events import Event +import colorama +import pygame +import math + +class Game: + """Game class""" + + def __init__(self, width, height, max_time, bomb_time, expl_duration, expl_range, sprite_dir="../../bomberman/sprites/"): + self.world = RealWorld.from_params(width, height, max_time, bomb_time, expl_duration, expl_range) + self.sprite_dir = sprite_dir + self.load_gui(width, height) + + @classmethod + def fromfile(cls, fname, sprite_dir="../../bomberman/sprites/"): + with open(fname, 'r') as fd: + # First lines are parameters + max_time = int(fd.readline().split()[1]) + bomb_time = int(fd.readline().split()[1]) + expl_duration = int(fd.readline().split()[1]) + expl_range = int(fd.readline().split()[1]) + # Next line is top border, use it for width + width = len(fd.readline()) - 3 + # Count the rows + startpos = fd.tell() + height = 0 + row = fd.readline() + while row and row[0] == '|': + height = height + 1 + if len(row) != width + 3: + raise RuntimeError("Row", height, "is not", width, "characters long") + row = fd.readline() + # Create empty world + gm = cls(width, height, max_time, bomb_time, expl_duration, expl_range, sprite_dir) + # Now parse the data in the world + fd.seek(startpos) + for y in range(0, height): + ln = fd.readline() + for x in range(0, width): + if ln[x+1] == 'E': + if not gm.world.exitcell: + gm.world.add_exit(x,y) + else: + raise RuntimeError("There can be only one exit cell, first one found at", x, y) + elif ln[x+1] == 'W': + gm.world.add_wall(x,y) + # All done + return gm + + def load_gui(self, board_width, board_height): + pygame.init() + self.height = 24 * board_height + self.width = 24 * board_width + self.screen = pygame.display.set_mode((self.width, self.height)) + self.block_height = int(math.floor(self.height / board_height)) + self.block_width = int(math.floor(self.width / board_width)) + rect = (self.block_height, self.block_width) + self.wall_sprite = pygame.image.load(self.sprite_dir + "wall.png") + self.wall_sprite = pygame.transform.scale(self.wall_sprite, rect) + self.bomberman_sprite = pygame.image.load(self.sprite_dir + "bomberman.png") + self.bomberman_sprite = pygame.transform.scale(self.bomberman_sprite, rect) + self.monster_sprite = pygame.image.load(self.sprite_dir + "monster.png") + self.monster_sprite = pygame.transform.scale(self.monster_sprite, rect) + self.portal_sprite = pygame.image.load(self.sprite_dir + "portal.png") + self.portal_sprite = pygame.transform.scale(self.portal_sprite, rect) + self.bomb_sprite = pygame.image.load(self.sprite_dir + "bomb.png") + self.bomb_sprite = pygame.transform.scale(self.bomb_sprite, rect) + self.explosion_sprite = pygame.image.load(self.sprite_dir + "explosion.png") + self.explosion_sprite = pygame.transform.scale(self.explosion_sprite, rect) + + def display_gui(self): + for x in range(self.world.width()): + for y in range(self.world.height()): + top = self.block_height * y + left = self.block_width * x + pygame.draw.rect(self.screen, (65, 132, 15), [left, top, self.block_width, self.block_height]) + rect = (left, top, self.block_width, self.block_height) + if self.world.wall_at(x, y): # Walls + self.screen.blit(self.wall_sprite, rect) + if self.world.explosion_at(x, y): # Explosion + self.screen.blit(self.explosion_sprite, rect) + if self.world.characters_at(x, y): # Player + self.screen.blit(self.bomberman_sprite, rect) + if self.world.monsters_at(x, y): # Monster + self.screen.blit(self.monster_sprite, rect) + if self.world.exit_at(x, y): # Portal + self.screen.blit(self.portal_sprite, rect) + if self.world.bomb_at(x, y): # Bomb + self.screen.blit(self.bomb_sprite, rect) + pygame.display.flip() + + def go(self, wait=0): + """ Main game loop. """ + + if wait is 0: + def step(): + pygame.event.clear() + input("Press Enter to continue or CTRL-C to stop...") + else: + def step(): + pygame.time.wait(abs(wait)) + + colorama.init(autoreset=True) + self.display_gui() + self.draw() + step() + while not self.done(): + (self.world, self.events) = self.world.next() + self.display_gui() + self.draw() + step() + self.world.next_decisions() + colorama.deinit() + + ################### + # Private methods # + ################### + + def draw(self): + self.world.printit() + + def done(self): + # User Exit + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return True + # Time's up + if self.world.time <= 0: + return True + # No more characters left + if not self.world.characters: + return True + # Last man standing + if not self.world.exitcell: + count = 0 + for k,clist in self.world.characters.items(): + count = count + len(clist) + if count == 0: + return True + return False + + def add_monster(self, m): + self.world.add_monster(m) + + def add_character(self, c): + self.world.add_character(c) diff --git a/bomberman/monsters/__init__.py b/bomberman/monsters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bomberman/monsters/selfpreserving_monster.py b/bomberman/monsters/selfpreserving_monster.py new file mode 100644 index 00000000..84798369 --- /dev/null +++ b/bomberman/monsters/selfpreserving_monster.py @@ -0,0 +1,75 @@ +# import sys +# sys.path.insert(0, '..') +from entity import MonsterEntity +import random + +class SelfPreservingMonster(MonsterEntity): + """A random monster that walks away from explosions""" + + def __init__(self, name, avatar, x, y, rnge): + super().__init__(name, avatar, x, y) + self.rnge = rnge + + def look_for_character(self, wrld): + for dx in range(-self.rnge, self.rnge+1): + # Avoid out-of-bounds access + if ((self.x + dx >= 0) and (self.x + dx < wrld.width())): + for dy in range(-self.rnge, self.rnge+1): + # Avoid out-of-bounds access + if ((self.y + dy >= 0) and (self.y + dy < wrld.height())): + # Is a character at this position? + if (wrld.characters_at(self.x + dx, self.y + dy)): + return (True, dx, dy) + # Nothing found + return (False, 0, 0) + + def must_change_direction(self, wrld): + # Get next desired position + (nx, ny) = self.nextpos() + # If next pos is out of bounds, must change direction + if ((nx < 0) or (nx >= wrld.width()) or + (ny < 0) or (ny >= wrld.height())): + return True + # If these cells are an explosion, a wall, or a monster, go away + return (wrld.explosion_at(nx, ny) or + wrld.wall_at(nx, ny) or + wrld.monsters_at(nx, ny) or + wrld.exit_at(nx, ny)) + + def look_for_empty_cell(self, wrld): + # List of empty cells + cells = [] + # Go through neighboring cells + for dx in [-1, 0, 1]: + # Avoid out-of-bounds access + if ((self.x + dx >= 0) and (self.x + dx < wrld.width())): + for dy in [-1, 0, 1]: + # Avoid out-of-bounds access + if ((self.y + dy >= 0) and (self.y + dy < wrld.height())): + # Is this cell safe? + if(wrld.exit_at(self.x + dx, self.y + dy) or + wrld.empty_at(self.x + dx, self.y + dy)): + # Yes + cells.append((dx, dy)) + # All done + return cells + + def do(self, wrld): + """Pick an action for the monster""" + # If a character is in the neighborhood, go to it + (found, dx, dy) = self.look_for_character(wrld) + if found and not self.must_change_direction(wrld): + self.move(dx, dy) + return + # If I'm idle or must change direction, change direction + if ((self.dx == 0 and self.dy == 0) or + self.must_change_direction(wrld)): + # Get list of safe moves + safe = self.look_for_empty_cell(wrld) + if not safe: + # Accept death + self.move(0,0) + else: + # Pick a move at random + (dx, dy) = random.choice(safe) + self.move(dx, dy) diff --git a/bomberman/monsters/stupid_monster.py b/bomberman/monsters/stupid_monster.py new file mode 100644 index 00000000..a9239475 --- /dev/null +++ b/bomberman/monsters/stupid_monster.py @@ -0,0 +1,31 @@ +# import sys +# sys.path.insert(0, '..') +from entity import MonsterEntity +import random + +class StupidMonster(MonsterEntity): + """A pretty stupid monster""" + + def look_for_empty_cell(self, wrld): + # List of empty cells + cells = [] + # Go through neighboring cells + for dx in [-1, 0, 1]: + # Avoid out-of-bounds access + if ((self.x + dx >= 0) and (self.x + dx < wrld.width())): + for dy in [-1, 0, 1]: + # Avoid out-of-bounds access + if ((self.y + dy >= 0) and (self.y + dy < wrld.height())): + # Is this cell walkable? + if not wrld.wall_at(self.x + dx, self.y + dy): + cells.append((dx, dy)) + # All done + return cells + + def do(self, wrld): + """Pick an action for the monster""" + # Get list of safe moves + safe = self.look_for_empty_cell(wrld) + # Pick a move at random + (dx, dy) = random.choice(safe) + self.move(dx, dy) diff --git a/bomberman/real_world.py b/bomberman/real_world.py new file mode 100644 index 00000000..0e64b482 --- /dev/null +++ b/bomberman/real_world.py @@ -0,0 +1,58 @@ +from world import World +from sensed_world import SensedWorld +from events import Event + +class RealWorld(World): + """The real world state""" + + def add_exit(self, x, y): + """Adds an exit cell at (x,y)""" + self.exitcell = (x,y) + + def add_wall(self, x, y): + """Adds a wall cell at (x,y)""" + self.grid[x][y] = True + + def add_monster(self, m): + """Adds the given monster to the world""" + self.monsters[self.index(m.x,m.y)] = [m] + + def add_character(self, c): + """Adds the given character to the world""" + self.characters[self.index(c.x,c.y)] = [c] + self.scores[c.name] = -self.time + + ################### + # Private methods # + ################### + + def next(self): + """Returns a new world state, along with the events that occurred""" + self.time = self.time - 1 + self.update_explosions() + self.events = self.update_bombs() + self.update_monsters() + self.update_characters() + self.update_scores() + self.manage_events() + return (self, self.events) + + def next_decisions(self): + self.aientity_do(self.monsters) + self.aientity_do(self.characters) + + def aientity_do(self, entities): + """Call AI to get actions for next step""" + for i, elist in entities.items(): + for e in elist: + # Call AI + e.do(SensedWorld.from_world(self)) + + def manage_events(self): + for e in self.events: + if e.tpe == Event.BOMB_HIT_CHARACTER: + e.other.done(SensedWorld.from_world(self)) + elif e.tpe == Event.CHARACTER_KILLED_BY_MONSTER: + self.remove_character(e.character) + e.character.done(SensedWorld.from_world(self)) + elif e.tpe == Event.CHARACTER_FOUND_EXIT: + e.character.done(SensedWorld.from_world(self)) + diff --git a/bomberman/sensed_world.py b/bomberman/sensed_world.py new file mode 100644 index 00000000..2d77ce7a --- /dev/null +++ b/bomberman/sensed_world.py @@ -0,0 +1,104 @@ +from entity import * +from events import * +from world import World + +class SensedWorld(World): + """The world state as seen by a monster or a robot""" + + @classmethod + def from_world(cls, wrld): + """Create a new world state from an existing state""" + new = cls() + new.bomb_time = wrld.bomb_time + new.expl_duration = wrld.expl_duration + new.expl_range = wrld.expl_range + new.exitcell = wrld.exitcell + new.time = wrld.time + # Copy grid + new.grid = [[wrld.wall_at(x,y) for y in range(wrld.height())] for x in range(wrld.width())] + # Copy monsters + mmapping = {} + for k, omonsters in wrld.monsters.items(): + # Make a new list of monsters at k + nmonsters = [] + # Create a new generic monster for each monster + # This way, every monster instance can be manipulated individually + for m in omonsters: + nm = MonsterEntity.from_monster(m) + nmonsters.append(nm) + mmapping[m] = nm + # Set list of monsters at k + new.monsters[k] = nmonsters + # Copy characters, scores, and build a mapping between old and new + cmapping = {} + for k, ocharacters in wrld.characters.items(): + # Make a new list of characters at k + ncharacters = [] + # Create a new generic character for each character + # This way, every character instance can be manipulated individually + # Plus, you can't peek into other characters' variables + for oc in ocharacters: + # Add to new list of characters + nc = CharacterEntity.from_character(oc) + ncharacters.append(nc) + # Add to mapping + cmapping[oc] = nc + new.characters[k] = ncharacters + # Copy bombs + for k, ob in wrld.bombs.items(): + c = cmapping.get(ob.owner, ob.owner) + new.bombs[k] = BombEntity(ob.x, ob.y, ob.timer, c) + # Copy explosions + for k, oe in wrld.explosions.items(): + c = cmapping.get(oe.owner) + if c: + new.explosions[k] = ExplosionEntity(oe.x, oe.y, oe.timer, c) + # Copy events + for e in wrld.events: + # Create a new event + # Tricky: if the character related to the event has died, duplicate the original character + newev = Event(e.tpe, cmapping.get(e.character, CharacterEntity.from_character(e.character))) + # Manage other attribute + if e.tpe == Event.BOMB_HIT_MONSTER: + newev.other = MonsterEntity.from_monster(e.other) + elif e.tpe == Event.BOMB_HIT_CHARACTER: + newev.other = CharacterEntity.from_character(e.other) + elif e.tpe == Event.CHARACTER_KILLED_BY_MONSTER: + newev.other = mmapping.get(e.other, MonsterEntity.from_monster(e.other)) + new.events.append(newev) + # Copy scores + for name,score in wrld.scores.items(): + new.scores[name] = score + return new + + def me(self, character): + for k,clist in self.characters.items(): + for c in clist: + if c.name == character.name: + return c + + def next(self): + """Returns a new world state, along with the events that occurred""" + new = SensedWorld.from_world(self) + new.time = new.time - 1 + new.update_explosions() + new.events = new.update_bombs() + new.update_monsters() + new.update_characters() + new.update_scores() + new.manage_events() + return (new, new.events) + + ################### + # Private methods # + ################### + + def aientity_do(self, entities): + """Call AI to get actions for next step""" + for i, elist in entities.items(): + for e in elist: + # Call AI + e.do(None) + + def manage_events(self): + for e in self.events: + if e.tpe == Event.CHARACTER_KILLED_BY_MONSTER: + self.remove_character(e.character) diff --git a/bomberman/sprites/bomb.png b/bomberman/sprites/bomb.png new file mode 100644 index 00000000..2e178d51 Binary files /dev/null and b/bomberman/sprites/bomb.png differ diff --git a/bomberman/sprites/bomberman.png b/bomberman/sprites/bomberman.png new file mode 100644 index 00000000..0367bbca Binary files /dev/null and b/bomberman/sprites/bomberman.png differ diff --git a/bomberman/sprites/explosion.png b/bomberman/sprites/explosion.png new file mode 100644 index 00000000..2773a44a Binary files /dev/null and b/bomberman/sprites/explosion.png differ diff --git a/bomberman/sprites/monster.png b/bomberman/sprites/monster.png new file mode 100644 index 00000000..9db5012a Binary files /dev/null and b/bomberman/sprites/monster.png differ diff --git a/bomberman/sprites/portal.png b/bomberman/sprites/portal.png new file mode 100644 index 00000000..c4ef4ae2 Binary files /dev/null and b/bomberman/sprites/portal.png differ diff --git a/bomberman/sprites/wall.png b/bomberman/sprites/wall.png new file mode 100644 index 00000000..250e398a Binary files /dev/null and b/bomberman/sprites/wall.png differ diff --git a/bomberman/world.py b/bomberman/world.py new file mode 100644 index 00000000..9a4458d2 --- /dev/null +++ b/bomberman/world.py @@ -0,0 +1,391 @@ +from entity import * +from events import Event +import sys +from colorama import Fore, Back, Style + +class World: + + def __init__(self): + """Class constructor""" + # Time for bomb to explode + self.bomb_time = -1 + # Explosion duration + self.expl_duration = -1 + # Explosion range + self.expl_range = -1 + # A pointer to the exit cell, if present + self.exitcell = None + # Time left + self.time = -1 + # Grid of cell types + self.grid = None + # List of dynamic elements + self.bombs = {} + self.explosions = {} + self.monsters = {} + self.characters = {} + # Scores + self.scores = {} + # Events + self.events = [] + + @classmethod + def from_params(cls, width, height, max_time, bomb_time, expl_duration, expl_range): + """Create a new empty world state""" + new = cls() + new.bomb_time = bomb_time + new.expl_duration = expl_duration + new.expl_range = expl_range + new.time = max_time + new.grid = [[False for y in range(height)] for x in range(width)] + return new + + def width(self): + """Returns the world width""" + return len(self.grid) + + def height(self): + """Returns the world height""" + return len(self.grid[0]) + + def empty_at(self, x, y): + """Returns True if there is nothing at (x,y)""" + return not (self.exit_at(x,y) or + self.wall_at(x,y) or + self.bomb_at(x,y) or + self.explosion_at(x,y) or + self.monsters_at(x,y) or + self.characters_at(x,y)) + + def exit_at(self, x, y): + """Returns True if there is an exit at (x,y)""" + return self.exitcell == (x,y) + + def wall_at(self, x, y): + """Returns True if there is a wall at (x,y)""" + return self.grid[x][y] + + def bomb_at(self, x, y): + """Returns the bomb at (x,y) or None""" + return self.bombs.get(self.index(x,y)) + + def explosion_at(self, x, y): + """Returns the explosion at (x,y) or None""" + return self.explosions.get(self.index(x,y)) + + def monsters_at(self, x, y): + """Returns the monsters at (x,y) or None""" + return self.monsters.get(self.index(x,y)) + + def characters_at(self, x, y): + """Returns the characters at (x,y) or None""" + return self.characters.get(self.index(x,y)) + + def next(self): + """Returns a new world state, along with the events that occurred""" + raise NotImplementedError("Method not implemented") + + def printit(self): + """Prints the current state of the world""" + border = "+" + "-" * self.width() + "+\n" + print("\nTIME LEFT: ", self.time) + sys.stdout.write(border) + for y in range(self.height()): + sys.stdout.write("|") + for x in range(self.width()): + if self.characters_at(x,y): + for c in self.characters_at(x,y): + sys.stdout.write(Back.GREEN + c.avatar) + elif self.monsters_at(x,y): + for m in self.monsters_at(x,y): + sys.stdout.write(Back.BLUE + m.avatar) + elif self.exit_at(x,y): + sys.stdout.write(Back.YELLOW + "#") + elif self.bomb_at(x,y): + sys.stdout.write(Back.MAGENTA + "@") + elif self.explosion_at(x,y): + sys.stdout.write(Fore.RED + "*") + elif self.wall_at(x,y): + sys.stdout.write(Back.WHITE + " ") + else: + tile = False + for k,clist in self.characters.items(): + for c in clist: + if c.tiles.get((x,y)): + sys.stdout.write(c.tiles[(x,y)] + ".") + tile = True + break + if not tile: + sys.stdout.write(" ") + sys.stdout.write(Style.RESET_ALL) + sys.stdout.write("|\n") + sys.stdout.write(border) + sys.stdout.flush() + print("SCORES") + for c,s in self.scores.items(): + print(c,s) + print("EVENTS") + for e in self.events: + print(e) + + ################### + # Private methods # + ################### + + def index(self, x, y): + """Returns an index used in internal dictionaries""" + return x + y * self.width() + + def add_explosion(self, x, y, bomb): + """Adds an explosion to the world state""" + self.explosions[self.index(x,y)] = ExplosionEntity(x, y, self.expl_duration, bomb.owner) + + def add_bomb(self, x, y, character): + """Adds a bomb to the world state""" + self.bombs[self.index(x,y)] = BombEntity(x, y, self.bomb_time, character) + + def remove_character(self, character): + # Remove character if it exists + i = self.index(character.x, character.y) + if (i in self.characters) and (character in self.characters[i]): + self.characters[i].remove(character) + + def check_blast(self, bomb, x, y): + # Check if a wall has been hit + if self.wall_at(x, y): + return [Event(Event.BOMB_HIT_WALL, bomb.owner)] + # Check monsters and characters + ev = [] + # Check if a monster has been hit + mlist = self.monsters_at(x,y) + if mlist: + for m in mlist: + ev.append(Event(Event.BOMB_HIT_MONSTER, bomb.owner, m)) + self.monsters[self.index(x,y)].remove(m) + # Check if a character has been hit + clist = self.characters_at(x,y) + if clist: + for c in clist: + ev.append(Event(Event.BOMB_HIT_CHARACTER, bomb.owner, c)) + self.remove_character(c) + # Return collected events + return ev + + def add_blast_dxdy(self, bomb, dx, dy): + # Current position + xx = bomb.x + dx + yy = bomb.y + dy + # Range + rnge = 0 + while ((rnge < self.expl_range) and + (xx >= 0) and (xx < self.width()) and + (yy >= 0) and (yy < self.height())): + # Cannot destroy exit or another bomb + if (self.exitcell == (xx,yy)) or (self.bomb_at(xx, yy)): + return [] + # Place explosion + self.add_explosion(xx, yy, bomb) + # Check what has been killed, stop if so + ev = self.check_blast(bomb, xx, yy) + if ev: + return ev + # Next cell + xx = xx + dx + yy = yy + dy + rnge = rnge + 1 + # No events happened + return [] + + def add_blast(self, bomb): + """Add blast, return hit events""" + # Add explosion at current position + self.add_explosion(bomb.x, bomb.y, bomb) + # Check what has been killed, stop if so + ev = self.check_blast(bomb, bomb.x, bomb.y) + if ev: + return ev + # Add explosions within range + ev = self.add_blast_dxdy(bomb, 1, 0) + ev = ev + self.add_blast_dxdy(bomb,-1, 0) + ev = ev + self.add_blast_dxdy(bomb, 0, 1) + ev = ev + self.add_blast_dxdy(bomb, 0,-1) + return ev + + def update_movable_entity(self, e): + """Moves a movable entity in the world, return True if actually moved""" + # Get the desired next position of the entity + (nx, ny) = e.nextpos() + # Make sure the position is within the bounds + nx = max(0, min(self.width() - 1, nx)) + ny = max(0, min(self.height() - 1, ny)) + # Make sure we are actually moving + if(((nx != e.x) or (ny != e.y)) and (not self.wall_at(nx, ny))): + # Save new entity position + e.x = nx + e.y = ny + return True + return False + + def update_monster_move(self, monster, update_dict): + # Save old index + oi = self.index(monster.x, monster.y) + # Try to move + if self.update_movable_entity(monster): + ev = [] + # Check for collision with explosion + expl = self.explosion_at(monster.x, monster.y) + if expl: + ev.append(Event(Event.BOMB_HIT_MONSTER, expl.owner, monster)) + if update_dict: + # Remove monster + self.monsters[oi].remove(monster) + return ev + # Otherwise, the monster can walk safely + if update_dict: + # Remove monster from previous position + self.monsters[oi].remove(monster) + # Put monster in new position + ni = self.index(monster.x, monster.y) + np = self.monsters.get(ni, []) + np.append(monster) + self.monsters[ni] = np + # Check for collisions with characters + characters = self.characters_at(monster.x, monster.y) + if characters: + for c in characters: + ev.append(Event(Event.CHARACTER_KILLED_BY_MONSTER, c, monster)) + return ev + return [] + + def update_character_move(self, character, update_dict): + # Save old index + oi = self.index(character.x, character.y) + # Try to move + if self.update_movable_entity(character): + ev = [] + # Check for collision with explosion + expl = self.explosion_at(character.x, character.y) + if expl: + ev.append(Event(Event.BOMB_HIT_CHARACTER, expl.owner, character)) + if update_dict: + # Remove character + self.characters[oi].remove(character) + return ev + # Otherwise, the character can walk + if update_dict: + # Remove character from previous position + self.characters[oi].remove(character) + # Put character in new position + ni = self.index(character.x, character.y) + np = self.characters.get(ni, []) + np.append(character) + self.characters[ni] = np + # Check for collision with monster + monsters = self.monsters_at(character.x, character.y) + if monsters: + return [Event(Event.CHARACTER_KILLED_BY_MONSTER, + character, monsters[0])] + # Check for exit cell + if self.exitcell == (character.x, character.y): + return [Event(Event.CHARACTER_FOUND_EXIT, character)] + return [] + + def update_explosions(self): + """Updates explosions""" + todelete = [] + for i,e in self.explosions.items(): + e.tick() + if e.expired(): + todelete.append(i) + self.grid[e.x][e.y] = False + for i in todelete: + del self.explosions[i] + + def update_bombs(self): + """Updates explosions""" + todelete = [] + ev = [] + for i,b in self.bombs.items(): + b.tick() + if b.expired(): + todelete.append(i) + ev = ev + self.add_blast(b) + for i in todelete: + del self.bombs[i] + return ev + + def update_monsters(self): + """Update monster state""" + # Event list + ev = [] + # Update all the monsters + nmonsters = {} + for i, mlist in self.monsters.items(): + for m in mlist: + # Update position and check for events + ev2 = self.update_monster_move(m, False) + ev = ev + ev2 + # Monster gets inserted in next step's list unless hit + if not (ev2 and ev2[0].tpe == Event.BOMB_HIT_MONSTER): + # Update new index + ni = self.index(m.x, m.y) + np = nmonsters.get(ni, []) + np.append(m) + nmonsters[ni] = np + # Save new index + self.monsters = nmonsters + # Return events + return ev + + def update_characters(self): + """Update character state""" + # Event list + ev = [] + # Update all the characters + ncharacters = {} + for i, clist in self.characters.items(): + for c in clist: + # Attempt to place bomb + if c.maybe_place_bomb: + c.maybe_place_bomb = False + can_bomb = True + # Make sure this character has not already placed another bomb + for k,b in self.bombs.items(): + if b.owner == c: + can_bomb = False + break + if can_bomb: + self.add_bomb(c.x, c.y, c) + # Update position and check for events + ev2 = self.update_character_move(c, False) + ev = ev + ev2 + # Character gets inserted in next step's list unless hit, + # escaped, or killed + if not (ev2 and ev2[0].tpe in [Event.BOMB_HIT_CHARACTER, Event.CHARACTER_FOUND_EXIT, Event.CHARACTER_KILLED_BY_MONSTER]): + # Update new index + ni = self.index(c.x, c.y) + np = ncharacters.get(ni, []) + np.append(c) + ncharacters[ni] = np + # Save new index + self.characters = ncharacters + # Return events + return ev + + def update_scores(self): + """Updates scores and manages events""" + for e in self.events: + if e.tpe == Event.BOMB_HIT_WALL: + self.scores[e.character.name] = self.scores[e.character.name] + 10 + elif e.tpe == Event.BOMB_HIT_MONSTER: + self.scores[e.character.name] = self.scores[e.character.name] + 50 + elif e.tpe == Event.BOMB_HIT_CHARACTER: + if e.character != e.other: + self.scores[e.character.name] = self.scores[e.character.name] + 100 + elif e.tpe == Event.CHARACTER_KILLED_BY_MONSTER: + self.remove_character(e.character) + elif e.tpe == Event.CHARACTER_FOUND_EXIT: + self.scores[e.character.name] = self.scores[e.character.name] + 2 * self.time + for k,clist in self.characters.items(): + for c in clist: + self.scores[c.name] = self.scores[c.name] + 1