From 7d47319445ebb0a94a27d8fcf4e74817064b60d2 Mon Sep 17 00:00:00 2001 From: DUBSON0 Date: Sun, 9 Feb 2025 12:54:06 -0500 Subject: [PATCH] Bomberman file changed to bomberman --- bomberman/__init__.py | 0 bomberman/entity.py | 300 ++++++++++++++ bomberman/events.py | 27 ++ bomberman/game.py | 147 +++++++ bomberman/monsters/__init__.py | 0 bomberman/monsters/selfpreserving_monster.py | 75 ++++ bomberman/monsters/stupid_monster.py | 31 ++ bomberman/real_world.py | 58 +++ bomberman/sensed_world.py | 104 +++++ bomberman/sprites/bomb.png | Bin 0 -> 534 bytes bomberman/sprites/bomberman.png | Bin 0 -> 515 bytes bomberman/sprites/explosion.png | Bin 0 -> 7110 bytes bomberman/sprites/monster.png | Bin 0 -> 675 bytes bomberman/sprites/portal.png | Bin 0 -> 839 bytes bomberman/sprites/wall.png | Bin 0 -> 371 bytes bomberman/world.py | 391 +++++++++++++++++++ 16 files changed, 1133 insertions(+) create mode 100644 bomberman/__init__.py create mode 100644 bomberman/entity.py create mode 100644 bomberman/events.py create mode 100644 bomberman/game.py create mode 100644 bomberman/monsters/__init__.py create mode 100644 bomberman/monsters/selfpreserving_monster.py create mode 100644 bomberman/monsters/stupid_monster.py create mode 100644 bomberman/real_world.py create mode 100644 bomberman/sensed_world.py create mode 100644 bomberman/sprites/bomb.png create mode 100644 bomberman/sprites/bomberman.png create mode 100644 bomberman/sprites/explosion.png create mode 100644 bomberman/sprites/monster.png create mode 100644 bomberman/sprites/portal.png create mode 100644 bomberman/sprites/wall.png create mode 100644 bomberman/world.py 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 0000000000000000000000000000000000000000..2e178d51082cab23a8fb22d701dcf3534adacd9b GIT binary patch literal 534 zcmV+x0_pvUP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00DkUL_t(|+U=ad62c%1L=D6L z|8l%|a2y>=TSzv%y-UfPT}Wx`gEQcJ7k27d{FLQuQx+Vdr{ z5i!oR2KpEG3;~Q#&jjq^@9lH5Spm5Vg8MhFRgCZ{Xzvj|X{PD*HDSl4xOOiJZdtTj zad^|`)K^XNXfHc?i26vs?;w#eo84gSoVc(F= zp_-RHR8qfXBf$KsuIj>&?`eH^BN5@i2v>2TMFUiZXc-)+ z3enOGA_&pa4I~26(hryzqR9-J45Y~po&ciBUmyTx$-jPD0n$oZd?L`AAD$qD5JKpn Yo_%LvYl3vOT~Adw=xSIk#OQVtM5} z75h`_TQwK9Eiq#YUgFCs>CwGO$oGeZ^lJViJH>jYd_1#WTSlGJ^;TCz^)9EAZE2k@ z>wXF(9+@WOFTa3+6=Lh9+nfvY`@3{_?LM&7xrtM(weF2@7%qq z>gnMbha9*?7JPX%QAQ{>{N+;TtgG_D4M`DpV$rf<(bCTXl7zSIV1LRC^a0RVNehuq z_od$~&3!UCC)UsYbe zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{02?kzL_t(|+P!;etYuks-nZ7; zd!KXe^j^K1ySnKKgMseGK$r}eCImS~PU1Kbj+`ixe}n`l5mAUi#+EH`LNF1E9NCVH zBH{c?q?kxnV#wq)Az+!o3^qdxZ(_7y7!)Q_TFpd$3FMgtEyK`f$1FS>eley zIcJUEw}wq0fAGa1BA6Kf0A{eb#CNzHfDQly(0hLRADy}D)vp~7|Mw^Wre-t3X`G>!Oum9Euv*pOw{g?vq!}qOT<-Nt>wjceeFSya@1Vb|{XTTK5 z>3ZS$h>ruX9R3V|P)(;l)-RWTE64aY036#EDMojo^|bWT~KELNK^5E8-Ll|le3r&!YrfE91cre{}so1Mtwh-wx{+t5Ezr zfFHinr+h59TYpyS!34lG17?S{^qqeBs_-m$!v}s7MC69waUiORsD1E5A8~`_Zn!5g zgP3nEa@my!5&=XYBE0-vzp_K(uYl;`;eCGlSa4?myy_Meod8s9nr;?1M@v4u0M^!4 zeDi<)cXS8)R}*Qugn;(-|Kd0AJ$z3h0{aHl?Ju24*g<* z93ytmpKmXG?W@(vH~mDKJ@AG~Mw<#y1K>9Rya&LS09<*N+v*THN&fa7n@7366e~H9 zt3V_mSTTSEkgrQjQbCPwzC_n2`0*IkWP-I&M9j|p<7dA0=r6`A=b4)s>T}P>TLk#TJ%&fXY3I1x68h?=t33J%PQG zr@+o(d~6Hs9Eb>-lAV0nt1K-Ss85}RYevx6A@1&gV1UN8u-ACdN{K^awwy4(bP*gp zyQ;E!Vou;h6I={_SkY1z?6u|Opo&NC4lC71#orS3vl)gZ^p4Z?gWV- zwo6R!xDysT^j9uHm;11+AOg%BzyiP!cP?S?iAPaQXYf^xFdBhGkW(T*9%J)G_ro`hm$B1o*Zf_T< z>i}k0${=D83&0GLL5Hd!Ujxp8gNLgs00zy8(y~QrTZ+p$y6sDdJKNCt9xS%kKyS$} zTm&Zpu>&N4e1J4#un$8L9JEHn;vAE6#Ns5!2s0Cy1*ir1s*>>rH)rnPb8q=^?0oGj zz#(+Obxegvx8DO658sU7stW8JBm}s+L6}bACsX*@2BfM%z5@FSIm++ah%xavvcfKth1UgmkbEUCv>tgP9iev6AdCd}t7!d<7N*!5 zbRU|{O}OBTg$;1t!;eN_=i$fG;_@CoM4$@nxU~rZdFdmY#_-nj?yR0PySh}=WtaBt`h(!@d2=J3Bq^{w|Gq~{tQrDnR zm5=uzUo;yJ_R;R_AT5{015$+M48Xu4K$;0G_kgBB-j%Qda=xIB1i%GQC|<_S4M~*& zSm6*vpg}2?Gnj=X1oq>x_{mKC=BD_KEvh%SsG4mcG!0k;Kk_gCtseFQR|z`+zf+w! zEyN5MkW+*t4|X0b0$}v2D7sb&PXy$vlJG`TgkvWFB52ovy@S*vxVjkxXRvc{ zp%`h!Vo8KtRgrH-ASU?n6r zcA&lm*XJ<1_XjaKdk$eVM!UaKpS zg_=PZ2Qagu#8@&m;D*fIA|C7^FPGrZKxUgIl==>7z6YcT6G1q3yr9jfL4E8Prf1Kh zKiEgVSRl0>%ramAdj|rQa1e%wAf-fRM#PNK<}s>g8>q%(h!2R%CEERcv^(2$tpdb* z4i&L5a!v(Wd;qaPeFg`CtTpbSRSiEHLsNp8BDG6siZB2vXRt_7LN0vxvq`pVLSOA(! z*nrl#!OTokk7_cddNM^Agf~^hzC*jei#R`63xE9eKMqILh)7(46O{0XU0Yl^I#{IQ5eIFu!yO-S#DD?21cD zfSYVWLKwoRgAVsF(-MZG5zGu}HbY!2O88kJVHpuQwh$L6Or{hj6ZlYpn2}?Te%T@} z7i3V>cimT#EdYm{)MAg^bx2)H(ia8?D}|Z?5a$P1D~cdyG_x5@73>`P`5ZE;3(rpp zEG2AM1eP;$*TZ7M_>Q}A>Lm{VAf&D>nVzI1wNV2O72J3XiwRxc$N1E#qHw+1u454d z5jgLmIU}}9^vf2h?*Ijb@fg*33_s+Gl7Aw)g9AVfliTh@+}OhS^jRD{{S+KIp8!y9 zmxzl6*n85P*`;8rNULpU^>n;>Dhx`t^+Y+G2?HC6+J6caQ>)F)5l z#0y@CYGVWY=P$rwkJK&-(uf1sify%^==GQ${__h4`dg;~c0*m8x(=EXLYN%%$&_PMWjLESrfC$cwr#sqr z(7u;`zVDow7WXH!l$n^VFhZu(b(HEFc{GBAK;pdl(TJ+?q_90xg_3Ong634Rvp7snpTX?RS@^0#Y}z*X9y4IQYehDsF~$dS&u#uI?Z{H3`=4m%jzuWU^!Q!+7NLgRb`qpL5;Fzan8eq!XYY*OFTLU z9q>E0Epp!@E|vxP>IP=H(ClUe4i$3WL#pBde5eV!E*7utnH38Ua0{YA0km$s^2*ZA zuOXZUMH&D{Kn8OQ&8&T!h-jp+tTAmvbq_?5M3~J20ksm>#N1^UW^)rc7c06!0jsKy2f|~6Td%^P{GWq)fJCk~ zRhZUFSi$!VNHQc(CU(v<9bI8hB%ezAzS!Y90gptyb*jZkfDqMsiWAWtLxVJs!e7v(ApDLu`C1L z7sXud^Au(94T6Ix14x<@xpc7K6H5mcfvQ^HEmZYs(|kZ83Su)D6VQTq36h2+b(V6n zoV}?|huLc+aJrV5_(cHx=&+LWG6H9<913FD$B{ z5>GT?=S{qKt1Nwey}g2>!Wv}_Rc19@s1PO-jJCE)-2jw2V94x+hL^e@>n6wzLajZ4 zu7}ARTj4!`WM(Y^&#=g2PM7=JxgKBOP@N>^iFVz#S?)Mgf$I^GbH?JCgc%?uc9EEw zM51`-#Jfo;`4n*14P#dEI>ejC1ZIY>8-NJaWQx(oCc=19s4NjQg|%Ck`h$iTz83#+ zvw_OZ2Q7o{=#rUTHsE4T=@AFh3u%A*TW;gTl88@hYV{Yx17u(VJ1^`# zySfQsGCl3;`d$DvRByoO>nBOX1abm&rNKfXsK#Sdvl)2s8YYHnDR)V@G&|N1NLK~? z4%4KY4;;<6^La{r|0FYCY!CK(XQ_MKa++rs9wX=3Qh(f^yzO|HZgdJ-jMyJEZ=2o@2)oM-=KxAiJ`s*3=esj{guR97ZHH1N3U0W2~kH-c}P(q&=>o52Q3yh06I%@GIhpl5gQXBRwY zn2CWs<&|P>!bh*n?kN7!I!0!NfnT$4WpWS(P*KBPa07$U9F<75tst9OvPJk)_NbLQdI5#G7 zm58fXRpXH!z!3FC=aN<9d5PPrAQ-eBIDQtEo7cDcu8VA6?5Pa>I_jaQg`#F+aAo=)^!N`JUj zZY+{gPDxYpn$iYfcLDfDE&PM)Z7@~`!vn)|5T{yz3Doc9M8&db5KpQufBYxuFw z0bo9UM?ClXw*(@h<=!6rs3Bih4uH$zB%5Z-8p=Em36m-Lx}N&cXv2k2k%$Z~^_|u| zo?FYWtgniklBF22+})-5rDtgW>2J&K!(YYn(LY)3LlFRSymfoJ{f$5Bh?$5PzOKpC zm_&?A-5w-s&M7g>OBju(EMg?$#JRxEdFJQPoZqGJstQfX`sEVs?k<*lyS%u335)Yj zAuaaGT;cF&?5?-|WbXI&#E&QD>!wVCeK2M*5U~iu#Dtw=A_`Puod#co}SHi>n0^f#ujrUDd@m%yQq8gism^ z%)H|HH!MjFaAT^aMRn8$*#8GbXmNY=ZHs=npxAcEeFw`5OOZ;u?ugYBNsYO@943kkC#%LKi_As?l?vlSI;jPNm=MwT@c6H+wg=e@>!Bqj| zeA!1J0+GAMAdbDyni8{fnp1Mscw$UqYDzrZ`1&0^Gh2;pisoaa*tRe=__`j77`p0i zt3N+SO0cW3zb>oI6yzKvR2ZK+1wWa<|Ap7#_E)?T)p&e$T<_}!Ire6%+#ekL=P(-I z!_M93Tv*NMZ4iJ^_IFoqPpi>aj{PvW)YA3wWhzWmxV@!9Bb=FLW`TI`Rkhpw_LKiZj4Yv3TOfr#^=EOTfEfl5QR zU83DT!2Hrh>^}Y|E`R0|W!-Ps4=}*^iO+me$ER=mDfa&3i)Su=){n+dRI}O4kH*L9 zjg31))0`oq8ioL(iioP*M;H56QrEHN%q(u*lvzz?iuk5B3|pQHC9FF_t7?)4!DB@1 zJM{Af+Pz&Yw=Y}!%y~k-x*h8t{^+k&U^Q;kPEOJ;gHtn1OB%ycArR#-5<((&zzyqoaAbhq-w0cbTQ7iPR?Cb z2lgR^jSULZsWJ<(b0i@UR~5Ms;6hk){-OU)W+pktY|Kg`Mnp_&Jbf;e0ch;4?ByU5 zQBNXEH{Jj@qtnfq+u?>&7pwUEy8xD!<2|;Fr>$Q$y6_?GZ@awMKPB~;sj4~d?^4=2 zPJT9%YBJ;CJh{4B?-tT9oE$1aVi7SjRx?HH4MaM$$%#d5AXD^x_5>r4f+dRc`@U{2 zx$*ddo6H^u^Im=O%;o$4`rrJNYqRQSFYV(;PHzZ+cK~=kfC&-pl4&L)=1>cTig`3* zSC3dmjev-RMOhEikBtY4@xvT3P zS5;3`vyDf>WcskHo3HFX@~2l`{8xVBtL)r8^s!ez54A84RiRvM#4xI}_o6;`mz{dx zWpF-#y(qGl2@POiCSTVaMq_eAheuYetpWROSfkaF3k+taIcGB~CA0{Wh!C^TDjdEa ze8DWK*%g@0S!55u(*XVyz$fZgzV5#Oz~6hzo8T(p8;+9@5!|t}2&d2Cg>U_5I962N`!=RW-afM0#+bDz)jdp~^FO89vAP(yv!eR##+f2ZZxQ#GEf_k)PJEGo_C zNGU>`M?D^wY42b;yH@aWsjfcR%rXe8B+4x8%Cg`9Xt!Eit`tb5AkqQW=@u21uV0rx zR3X%O?K|INd(T{?@v$v1GgP%~UYKFIw~KCnA0&d&na!)4VTX_24GhrCvZ>~k;AWP} z0umDl9xygF27$;7#(ZlMtQcwe&Q#`oa*GO})w;Yp>a^DD-v4X*#FxIvr(W#naO|w=F14vEUTGnGP4e*y%~DIdIDtQ^IBA@*7m$2N~ zhNM>j1*l|t2_cB{jd?!;;G<~;GjC=r;*9GW zEW)=^Hk&F^-&^Xs32}mLop(-k-&TANmMVjQG^gzoYEfi^Hpa z{#VoXzdiDYe<0hR{=~Wt2MhdP{L7eMdgg8Y{@xwN{2RybzPIvqz3J;_ z9NTuZxU%iQOw4S)uC>}6`)WLvTY*0_vu-{|>N@nxMGxS77>ynVaMyPs{QAJ5>%mPkOMm!*cRS>$ z0KN!zm;Kh6Upe`n_dUiUA3REi>#iQa7-s3yAACPv^K-wje#q(|VRzjP6hO670!zSR zXNObQT>(&8x479D7Jy0j^dsM}et|Fi`Ue5xg#C+S0DAaXEONv{?|!?!=|dk`F~FnW z#Y`W1_uHNIi(AmmM2jBBP9ewjF wxb(HJ-VobI+;0p3dlxR?pe zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Ik1L_t(|+U=WLZp0uAMX{Ct zcBkzMI%-$Y?zCM2{TeBgA^3qIJdD(@sEqH)wLLHZF~-nh0D$O&VvNuvu37-3g0BK% z4!~M?Z4f2^YKyK3qZoix{O`wG_WA4APg4kL09@geU}|uIXhEm|aKx7i*MQJK5CNcs zTZ`7hkbuC^#B)ANQkPf`$W7Q%xYP|CC*a89sTep;$U5L`f*yoR+>mnuTIXM{ZV&@V zz5SlW=VHvI0`~OP_KMXg&S3qLSaBBXYg1vfu>Q;E_vy30+k24^>$w3S?z;lcWPMHS zIIDQB2AsL_b20~CulWkY8-!oIxc6a=h8(E+W{pfEP8NDNenSY zz>&pU#DcBF?Jb?MH}$+0q^;$fqJ-5D&T>Gi4$RaIQd;hsm;WBH?TJWshwkm?YNu%0 z;NI~MCqzoO$6LWDjY;&|;K;Rr6Z>`~Q?f}~J1s1BtBwUfc}Qx8GXj+L#jC-QQb7_c za%?igIMalZe;C09T_@yboHs=>7O@mtU>TC-T!(5)W0@7+h_75r)V=laJo)Z87002ov JPDHLkV1iHJByIoz literal 0 HcmV?d00001 diff --git a/bomberman/sprites/portal.png b/bomberman/sprites/portal.png new file mode 100644 index 0000000000000000000000000000000000000000..c4ef4ae245223791073076cdda88b1e64c233d10 GIT binary patch literal 839 zcmV-N1GxN&P)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00OT`L_t(|+U;9Qj>0eur3Q&h za110QPQfK+$8p$kiMa$LA?6qyf?13@t4N&^C(ll3;*>0_Ht~D*bMtfexLp=gwBvo( zOWr?iuH6)Nb*~~o3OE-b>+Qb(`Tl~*TTKc4d{{z#e0=`0{pOAgHUcCAa-dPb8bWfB zv3iBaL%=yOp>N%@@wdnOt`CvG$zk}NIs(SPInXh{v4-~RvlGb&sw9Yj4*}PZg^Zz* zgouC`a1MYr7oh#65CIdQ0WZk~(!7z2gzj;>M71&m9~(GVEx6mJfB$Eki4Z%a2jB3&2oc$zau!Si2M*wnwVKE;dHXrZ1 zo?7y@CMu}q?2llhmi3Qdp%(Qov7Ht(Z*KzL1SC!*EKh=h1hl>wQZ0vD{2kI9CP1Nx zAmjkiVpJeDpElQywAyK9GDIkQRyRds_LFua!K(Mi&&X+EZi^v@S+pFsK#Fu%o5qa9 zKD2x`=)+1mI;>gy;?rTIgBqhidzedHXr!LI*A)7pn<5m_hczk09OT26bUJ)KEQ>}0 zH@@n0SSy86azO4WA(y0}dxEuGlS9Vy*&6!AQ>wP6j0WvR$@O;MuebYN909{9(1New zs;mR=yAJ$BfbV7Nm!*>l7?Xf%-^_J+ga9oGDS4q-!9TT$X?><>&pI^iG$Zz?6vrBL!gjkiEBiObAE1aYF-J0b5UwyNotBh zd1gt5g1e`0KzJjcI8f1TPZ!6Kid%1QJMtY?;Bg5|{@PODH~->Ym(bNrp%I~v^E-Vv zeY9chRJx?3^mc94`(+Vpx4tah{)p9~_2vFpshKlXn>Mj;VE1{wakr%4rr`OKKr=}K zzqZAA87J^>rtTf2Z^( QCxYDL>FVdQ&MBb@0C$Ri-v9sr literal 0 HcmV?d00001 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