diff --git a/livrables/1/README.txt b/livrables/1/README.txt new file mode 100644 index 0000000..e69de29 diff --git a/livrables/1/graphic.py b/livrables/1/graphic.py new file mode 100755 index 0000000..02d5521 --- /dev/null +++ b/livrables/1/graphic.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +import pygame as pg +import pacman_sprite +import pacman as m_pacman # m_ for module to avoid conflicts +from physic_engine import PhysicEngine +import pacmap as m_pacmap +import sys + +pg.init() + +class Screen: + def __init__(self, size, pacmap: m_pacmap.Map, physic_engine: PhysicEngine, pacman: m_pacman.Pacman): + self.screen = pg.display.set_mode(size) + # self.screen.set_caption("Pacman") + self.physic_engine = physic_engine + self.pacman = pacman + self.pacman_sprite = pacman_sprite.PacmanSprite(size[0]/28) + self.clock = pg.time.Clock() + self.max_fps = 40 + self.entity_group = pg.sprite.Group(self.pacman_sprite) + self.loop() + + + def user_events(self): + key = pg.key.get_pressed() + if key[pg.K_UP]: + pacman.set_next_dir(m_pacman.direction.up) + if key[pg.K_DOWN]: + pacman.set_next_dir(m_pacman.direction.down) + if key[pg.K_LEFT]: + pacman.set_next_dir(m_pacman.direction.left) + if key[pg.K_RIGHT]: + pacman.set_next_dir(m_pacman.direction.right) + + def refresh(self): + """refresh/redraw all""" + pac_x, pac_y = self.pacman.position + pac_res = self.pacman.resolution + pacmap.draw(self.screen) + self.pacman_sprite.rect.x = int(pac_x / 28 / pac_res * self.screen.get_width()) - 10 + self.pacman_sprite.rect.y = int(pac_y / 31 / pac_res * self.screen.get_height()) - 10 + self.entity_group.draw(self.screen) + + + def create_maze_surface(self): + pass + + def loop(self): + while 1: + self.screen.fill((0, 0, 0)) + for event in pg.event.get(): + if event.type == pg.QUIT: + sys.exit() + + self.user_events() + self.physic_engine.move_all() + self.refresh() + + self.clock.tick(self.max_fps) + pg.display.flip() + +if __name__ == '__main__': + pacman = m_pacman.Pacman((1,1)) + pacmap = m_pacmap.Map(maze_img_file="pacmap_maze1.png") + phys_engine = PhysicEngine(pacmap, pacman) + screen = Screen((560, 620), pacmap, phys_engine, pacman) diff --git a/livrables/1/pacman.py b/livrables/1/pacman.py new file mode 100755 index 0000000..867a261 --- /dev/null +++ b/livrables/1/pacman.py @@ -0,0 +1,135 @@ +import pacmap +import os + +from enum import IntEnum +from collections import namedtuple + +_tempdir = namedtuple("Direction", ["up", "down", "left", "right", "none"]) +direction = _tempdir( + up= (0, -1), + down=(0, 1), + left=(-1, 0), + right=(1, 0), + none=(0, 0) +) +pacdot_counter = 88 +score = 0 +lives = 3 + +class FruitType(IntEnum): + A = 0 + +class Fruit: + def __init__(self, fruit_type, score, position = (0, 0)): + self.fruit_type = fruit_type + self.score = score + self.position = position + + +class Pacman: + def __init__(self, position=[0, 0], map_size=(28, 31), resolution=10): + self.position = [position[0]*resolution+int(resolution/2), position[1]*resolution+int(resolution/2)] + self.direction = direction.right + self.next_direction = direction.none + self.super_power = 0 # Counter of super pacdots in effect (> 0 means super power is active) + self.ghost_combo = 0 + self.size = (1.8, 1.8) # size related to tile size + self.speed = 0.1 + self.resolution = resolution # when pacman in 0:10 he's in the 1st cell if resolution=10 !!! must be even number + self.map_size = map_size + + def matrix_position(self): + return int(self.position[0] / self.resolution), int(self.position[1] / self.resolution) + + def next_matrix_position(self): + next_x = int(self.position[0] / self.resolution + self.direction[0]) % self.map_size[0] + next_y = int(self.position[1] / self.resolution + self.direction[1]) % self.map_size[1] + return next_x, next_y + + def has_super_power(self): + return self.super_power > 0 + + def eat_pacdot(self, dot_map): + global score + global pacdot_counter + dot_map.dots_map[self.matrix_position()[0]][self.matrix_position()[1]] = pacmap.DotTile.NDT + pacdot_counter -= 1 + score += 10 + if pacdot_counter == 0: + game_over("win") + + def eat_super_pacdot(self, dot_map): + global score + dot_map.dots_map[self.matrix_position()[0]][self.matrix_position()[1]] = pacmap.DotTile.NDT + score += 50 + self.super_power += 1 + + # TODO + # Requires UNIX - or use of async + # pid = os.fork() + #if pid == 0: + # return + + #os.sleep(10) + self.super_power -= 1 + self.ghost_combo = 0 + + def eat_fruit(self, fruit, dot_map): + global score + dot_map.dots_map[self.matrix_position()[0]][self.matrix_position()[1]] = pacmap.DotTile.NDT + score += fruit.score + + def eat_ghost(self, ghost): + global score + ghost.despawn_and_respawn() + self.ghost_combo += 1 + score += (2 ** self.ghost_combo) * 100 + + def get_eaten(self, dot_map): + global lives + #TODO score loss ? + self.position = dot_map.spawn_point #TODO + lives -= 1 + if lives < 0: # à vérifier + game_over() + + def is_at_center_tile(self): + """return True if pacman is at the center of a tile else False""" + clause_1 = not self.position[0] % (self.resolution/2) and self.position[0] % self.resolution + clause_2 = not self.position[1] % (self.resolution/2) and self.position[1] % self.resolution + return clause_1 and clause_2 + + def change_dir(self, new_dir): + self.direction = new_dir + + def set_next_dir(self, next_dir): + if next_dir[0] and self.direction[0]: + self.change_dir(next_dir) + elif next_dir[1] and self.direction[1]: + self.change_dir(next_dir) + else: + self.next_direction = next_dir + + def change_to_next_dir(self): + self.direction = self.next_direction + self.next_direction = direction.none + + def get_next_dir_tile(self): + """return x, y corresponding to the tile if we move with next_direction""" + next_x = int(self.position[0] / self.resolution + self.next_direction[0]) % self.map_size[0] + next_y = int(self.position[1] / self.resolution + self.next_direction[1]) % self.map_size[1] + return next_x, next_y + + def move(self): + self.position[0] = (self.position[0] + self.direction[0]) % (self.resolution * self.map_size[0]) + self.position[1] = (self.position[1] + self.direction[1]) % (self.resolution * self.map_size[1]) + +def game_over(status = "lose"): + #TODO + pass + + + + + + diff --git a/livrables/1/pacman_sprite.py b/livrables/1/pacman_sprite.py new file mode 100755 index 0000000..4ecfe76 --- /dev/null +++ b/livrables/1/pacman_sprite.py @@ -0,0 +1,13 @@ +"""Pacman Graphic Object""" +import pygame as pg + +class PacmanSprite(pg.sprite.Sprite): + def __init__(self, size): + super().__init__() + self.image = pg.Surface([size, size]) + half_size = int(size / 2) + pg.draw.circle(self.image, (255, 255, 0), (half_size, half_size), half_size) + + # Fetch the rectangle object that has the dimensions of the image + # Update the position of this object by setting the values of rect.x and rect.y + self.rect = self.image.get_rect() \ No newline at end of file diff --git a/livrables/1/pacmap.py b/livrables/1/pacmap.py new file mode 100755 index 0000000..7cb35d1 --- /dev/null +++ b/livrables/1/pacmap.py @@ -0,0 +1,242 @@ +from enum import IntEnum +from copy import deepcopy +from PIL import Image +import pygame +import os + +class DotTile(IntEnum): + NDT = 0 # no dot + SPD = 1 # small pac-dot + BPD = 2 # big pac-dot + FRT = 3 # fruit + +class PhysTile(IntEnum): + GRD = 0 # ground + WAL = 1 # wall + GSD = 2 # ghost door + GWL = 3 # ghost cell wall + GCF = 4 # ghost cell floor + TPT = 5 # teleporter tile + FIT = 6 # fully inaccessible tile + # ghost-cell ground as FIT ? verify no pac-dot here + +class CrossTile(IntEnum): + # code where we can find another GRD/TPT Tile from a given Tile + # with binary masks + UP = 1 # 0001 + DOWN = 2 # 0010 + LEFT = 4 # 0100 + RIGHT = 8 # 1000 + # direction changement if != 3 or != 12 + +class Map: + """ + Pacman maps size is 28×31 + """ + + width = 28 + height = 31 + # tile_size = 16 # tile subdivision for dynamic movement + + def __init__(self, phys_map = [], dots_map = [], maze_img_file=""): + """ + physic_map is the array containing elements: + 0: ground tile + 1: wall + 2: ghost door + 3: teleporter (no need to precise which to go because we assume it will + be at the opposite map tile) + 4: fully inaccessible tile (basically tiles that represent the "in-wall" + space) + + dots_map is a layer on top of the physic_map of the same dimension which contains + 0: no dot + 1: small pac-dot + 2: big pac-dot + """ + self._surf = pygame.Surface((Map.width * 20, Map.height * 20)) + if maze_img_file and os.path.isfile(maze_img_file): + try: + self.phys_map = self.decode_map(maze_img_file) + except Exception as e: + raise e + else: + self.phys_map = phys_map # in the first part we assume phys_map is correct and no need to verify + self.dots_map = dots_map + self.intersect_map = [] # TODO - the layer which contains intersections pre-calculated + + + def verify(self, phys_map, dots_map) -> bool: + """ + This method will verify if a given map is valid or not + Return True if correct else False + + we will assume + there are only: + - 1 ghost door + - 4 big pac-dots + - 240 pac-dots + + Each ground tile must have at least 2 ground tile neighboors because + there is no dead-end tile in pac-man + Each dot must be on a ground tile (not even a teleporter one) + """ + + if not (len(phys_map) == len(dots_map) == 31): + return False + + for i in range(len(phys_map)): + if not (len(phys_map[i]) == len(dots_map[i]) == 28): + return False + + # 1 ghost door verification + if sum(sub_arr.count(PhysTile.GSD) for sub_arr in phys_map) != 1: + return False + + # # 4 big pac dots + if sum(sub_arr.count(DotTile.BPD) for sub_arr in dots_map) != 4: + return False + + # # 240 small pac-dots + if sum(sub_arr.count(DotTile.SPD) for sub_arr in dots_map) != 240: + return False + + # dots are only in ground tile + for row in range(len(phys_map)): + for col in range(len(phys_map[0])): + if dots_map[row][col] and phys_map[row][col]: # no dot = 0; ground = 0 + return False + + # odd number of teleporter tiles + teleporter_count = sum(sub_arr.count(PhysTile.TPT) for sub_arr in phys_map) + if teleporter_count % 2: + return False + + edges = phys_map[0][1:] + phys_map[-1][1:] + [sub[0] for sub in phys_map] \ + + [sub[-1] for sub in phys_map][1:-1] + + # no ground tile on edges + if PhysTile.GRD in edges: + return False + + # teleporters are in front of the other + for col in range(1, len(phys_map[0])-1): + if phys_map[0][col] == PhysTile.TPT or phys_map[-1][col] == PhysTile.TPT: + if phys_map[0][col] == phys_map[-1][col]: + teleporter_count -= 2 + else: + return False + for row in range(1, len(phys_map)-1): + if phys_map[row][0] == PhysTile.TPT or phys_map[row][-1] == PhysTile.TPT: + if phys_map[row][0] == phys_map[row][-1]: + teleporter_count -= 2 + else: + return False + + if teleporter_count: # not all teleporters are on edges + return False + + # no teleporter on corners + if any(PhysTile.TPT in (phys_map[0][0], phys_map[0][-1], + phys_map[self.height-1][0], phys_map[-1][-1])): + return False + + # now we have to verify there is no dead-end ground tile + for col in range(1, self.width-1): + for row in range(1, self.height-1): + cpt = 0 + for x, y in ((0, 1), (1, 0), (-1, 0), (0, -1)): + if phys_map[row+y][col+x] in (PhysTile.GRD, PhysTile.TPT): + cpt += 1 + if cpt == 2: + break + if cpt < 2: + return False + # we have to verify if there is only 1 connexity component of (PhysTile.GRD, PhysTile.TPT, PhysTile.BPD, PhysTile.SPD) + if not connex(phys_map): + return False + + return True + + def get_tile(self, x, y): + return self.phys_map[y][x] + + def create_cross_layer(self): + dictionnary = { + (0, 1):CrossTile.DOWN, + (0, -1): CrossTile.UP, + (1, 0): CrossTile.RIGHT, + (-1, 0): CrossTile.LEFT + } + for row in range(self.height): + self.intersect_map.append([]) + for col in range(self.width): + if not self.get_tile(col, row) in (PhysTile.GRD, PhysTile.TPT): + self.intersect_map[-1].append(0) + if self.get_tile(col, row) in (PhysTile.GRD, PhysTile.TPT): + cpt = 0 + for dir in dictionnary: + test_col = (col + dir[0]) % self.width + test_row = (row + dir[1]) % self.height + if self.get_tile(test_col, test_row) in (PhysTile.GRD, PhysTile.TPT): + cpt |= dictionnary[dir] + self.intersect_map[-1].append(cpt) + + def draw(self, surf): + surf.blit(self._surf, (0, 0)) + + def decode_map(self, img_file): + img = Image.open(img_file) + dictionnary = { + (0 , 0, 0): PhysTile.GRD, + (255, 0, 255): PhysTile.GCF, + (0 , 0, 255): PhysTile.WAL, + (0 , 255, 0): PhysTile.GSD, + (0 , 255, 255): PhysTile.GWL, + (255, 0, 0): PhysTile.TPT, + (255, 255, 0): PhysTile.FIT, + } + data = list(img.getdata()) + matrix = [] + for row in range(img.height): + matrix.append([]) + for col in range(img.width): + try: + color = data[col + row*img.width][:3] + tile = dictionnary[color] # avoid alpha component + self._surf.fill(color, pygame.Rect(col*20, row*20, 20, 20)) + except: + raise ValueError("Pixel " + str(col) + "," + str(row) + " is invalid") + matrix[-1].append(tile) + return matrix + + + +def explore(matrix, x=-1, y=-1): + """explore the given matrix and change it (GRD and TPT become FIT)""" + if x < 0 and y < 0: + # searching the beginning + for row in range(len(matrix)): + for col in range(len(matrix[0])): + if matrix[row][col]: + explore(matrix, col, row) + elif matrix[y][x] in (PhysTile.TPT, PhysTile.GRD): + matrix[y][x] = PhysTile.FIT + for col, row in ((0, 1), (1, 0), (0, -1), (-1, 0)): + explore(matrix, (x+col)%len(matrix[0]), (y+row)%len(matrix)) + + +def connex(matrix, x=-1, y=-1): + """return True if the matrix is connex - has only one connexity part""" + temp = deepcopy(matrix) + explore(temp) + if any(tile in (PhysTile.GRD, PhysTile.TPT) for row in temp for tile in row): + return False + return True + +def get_one_in_digit(number): + cpt = 0 + while number: + cpt += number & 1 + number //= 2 + return cpt diff --git a/livrables/1/pacmap_maze1.png b/livrables/1/pacmap_maze1.png new file mode 100644 index 0000000..503e9b9 Binary files /dev/null and b/livrables/1/pacmap_maze1.png differ diff --git a/livrables/1/pacmap_rule.txt b/livrables/1/pacmap_rule.txt new file mode 100644 index 0000000..2966272 --- /dev/null +++ b/livrables/1/pacmap_rule.txt @@ -0,0 +1,9 @@ +each wall (excluding the exterior one) have a width of at least 2 (can be filled with FIT tiles) +RGB code: +GRD 0 0 0 // ground tile +GCF 255 0 255 // ghost cell floor tile +WAL 0 0 255 // wall tile +GSD 0 255 0 // ghost door tile +GWL 0 255 255 // ghost wall tile +TPT 255 0 0 // teleporter tile +FIT 255 255 255 // fully inaccessible tile (tiles between walls) diff --git a/livrables/1/physic_engine.py b/livrables/1/physic_engine.py new file mode 100755 index 0000000..5947932 --- /dev/null +++ b/livrables/1/physic_engine.py @@ -0,0 +1,21 @@ +from pacmap import * +from pacman import * + +class PhysicEngine: + def __init__(self, c_pacmap: Map, c_pacman: Pacman): + self.pacmap = c_pacmap + self.pacman = c_pacman + self.entities = [] # ghosts + + def move_all(self): + # pacman movement + next_pac_tile = self.pacman.next_matrix_position() + pac_res = self.pacman.resolution + if self.pacman.is_at_center_tile(): + if self.pacman.next_direction != direction.none and self.pacmap.get_tile(*self.pacman.get_next_dir_tile()) in (PhysTile.GRD, PhysTile.TPT): + self.pacman.change_to_next_dir() + self.pacman.move() + elif self.pacmap.get_tile(*next_pac_tile) in (PhysTile.GRD, PhysTile.TPT): + self.pacman.move() + else: + self.pacman.move() \ No newline at end of file diff --git a/livrables/1/test_connexity.py b/livrables/1/test_connexity.py new file mode 100755 index 0000000..e9553eb --- /dev/null +++ b/livrables/1/test_connexity.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import pacmap + +maps = [ + [ + [4, 1, 1, 1, 3, 1, 1, 4], + [1, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 0, 1, 0, 1], + [3, 0, 1, 0, 0, 1, 0, 3], + [1, 0, 0, 0, 1, 0, 0, 1], + [1, 1, 1, 0, 0, 0, 1, 1], + [4, 4, 1, 1, 3, 1, 1, 4], + ], + [ + [4, 1, 1, 1, 3, 1, 1, 4], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [4, 1, 1, 1, 3, 1, 1, 4], + ], + [ + [4, 1, 1, 1, 3, 1, 1, 4], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [4, 1, 1, 1, 3, 1, 1, 4], + ], + [ + [4, 1, 1, 1, 3, 1, 1, 4], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 0, 0, 1], + [1, 1, 1, 1, 1, 1, 0, 1], + [3, 0, 0, 0, 0, 1, 0, 3], + [1, 1, 1, 1, 0, 1, 1, 1], + [4, 1, 1, 1, 3, 1, 1, 4], + ] +] + +for pac_map in maps: + is_connex = pacmap.connex(pac_map) + print(is_connex) \ No newline at end of file diff --git a/livrables/1/test_load_picture.py b/livrables/1/test_load_picture.py new file mode 100755 index 0000000..4266aef --- /dev/null +++ b/livrables/1/test_load_picture.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from pacmap import decode_map +import sys + +if __name__ == '__main__': + if len(sys.argv) > 1: + matrix = decode_map(sys.argv[1]) + else: + matrix = decode_map("../pacmap_maze1.png") + for row in matrix: + print() + for el in row: + print(el.value, end=' ') \ No newline at end of file