#!/usr/bin/env python3 """ Labyrinth game. """ import tkinter as tk from random import randrange, shuffle RIGHT = tk.RIGHT LEFT = tk.LEFT BOTH = tk.BOTH HEIGHT = 19 WIDTH = 37 V_SIZE = 16 H_SIZE = 16 NB_TLPS = 6 NB_ENMS = 15 SCORE_MAX = 1000 X_START, Y_START = 3, 9 X_OUT, Y_OUT = WIDTH-1, 9 CHARS = (' ', '\u2588', '\u2592', 'x', '@') #CHARS values are, in order : # 0 - empty # 1 - wall # 2 - teleport # 3 - ennemy # 4 - player BLOCKS = ( lambda canvas, x, y: canvas.create_rectangle(x, y, x+H_SIZE, y+V_SIZE, fill='white', outline=''), lambda canvas, x, y: canvas.create_rectangle(x, y, x+H_SIZE, y+V_SIZE, fill='black', outline='', tags='wall'), lambda canvas, x, y: canvas.create_oval(x+1, y, x+H_SIZE-1, y+V_SIZE, fill='cyan', outline='', tags='tlp'), lambda canvas, x, y: canvas.create_rectangle(x+3, y+3, x+H_SIZE-3, y+V_SIZE-3, fill='red', outline='', tags='enm'), lambda canvas, x, y: canvas.create_rectangle(x+3, y+3, x+H_SIZE-3, y+V_SIZE-3, fill='green', outline='', tags='plr') ) def constr_laby(): """ Generate the labyrinth as a 2 dimensions list of ints. There is 4 rules for the generation : 1. all the exterior border is a wall 2. 1 out of 2 cell vertically and horizontally are walls 3. all cells at the bottom right of the previous tiling cannot be walls 4. all other cells have 1/3 chances to be a wall. """ #initialisation laby = [] for i in range(HEIGHT): laby.append([]) for j in range(WIDTH): if i in (0, HEIGHT-1) or j in (0, WIDTH-1): #des murs sur tous les bords content = 1 elif not i%2 and not j%2: #1. 1 case sur 2 verticalement et # horizontalement est un mur (-> 1) content = 1 elif i%2 and j%2: #2. les cases diagonales bas-droite du pavage 1. # ne peuvent pas être un mur (-> 0) content = 0 else: #3. toutes les cases non concernées par 1. et 2. # ont 3/10 chances d'être un mur content = 1 if randrange(10) < 3 else 0 laby[i].append(content) for i in range(1, 6): #on "creuse" artificiellement la zone de début et la sortie laby[Y_START][i] = 0 laby[Y_OUT][31+i] = 0 return laby def place_teleports(laby): """ Randomly places NB_TLPS on empty cells of the labyrinth. """ teleports = [] height, length = len(laby), len(laby[0]) empty_cells = [(x, y) for x in range(length) for y in range(height) if laby[y][x] == 0] shuffle(empty_cells) for _ in range(NB_TLPS): tlp = empty_cells.pop() teleports.append(tlp) laby[tlp[1]][tlp[0]] = 2 #laby étant une liste, il est passé par référence #donc modifié en place, pas besoin de le renvoyer return teleports def place_enemies(laby): """ Randomly places NB_ENMS on empty cells of the labyrinth. """ enemies = [] height, length = len(laby), len(laby[0]) empty_cells = [(x, y) for x in range(length) for y in range(height) if laby[y][x] == 0] shuffle(empty_cells) for _ in range(NB_ENMS): enm = empty_cells.pop() enemies.append(enm) laby[enm[1]][enm[0]] = 3 #laby étant une liste, il est passé par référence #donc modifié en place, pas besoin de le renvoyer return enemies def afficher(laby): """ Prints the labyrinth in terminal, in ascii style """ print('\n'.join([''.join([CHARS[val] for val in line]) for line in laby])) def laby_to_str(laby): """ Returns a string representing the labyrinth. """ laby_str = '' for line in laby: str_line = '' for value in line: str_line += CHARS[value] laby_str += str_line + '\n' return laby_str[:-1] #without the last '\n' def sgn(val): """Sign function.""" if val < 0: return -1 elif val > 0: return 1 return 0 def move_enemies(game_data): """ Moves all the enemies towards the player if possible. """ player = game_data['player'] #game_data['enms_mvd'] = [None for i in range(NB_ENMS)] for idx, enm in enumerate(game_data['enms']): diff_x = sgn(player[0] - enm[0]) diff_y = sgn(player[1] - enm[1]) if game_data['laby'][enm[1]+diff_y][enm[0]+diff_x] == 0: game_data['laby'][enm[1]][enm[0]] = 0 new_enm = (enm[0] + diff_x, enm[1] + diff_y) game_data['enms_mvd'][idx] = game_data['enms'][idx] game_data['enms'][idx] = new_enm game_data['laby'][new_enm[1]][new_enm[0]] = 3 def move_player(game_data, direction): """ Moves the player in the asked direction if possible, and manage teleportation. """ if not direction: return False player = game_data['player'] tlps = game_data['tlps'] if direction == 'UP': new_cell = (player[0], player[1]-1) elif direction == 'DOWN': new_cell = (player[0], player[1]+1) elif direction == 'LEFT': new_cell = (player[0]-1, player[1]) elif direction == 'RIGHT': new_cell = (player[0]+1, player[1]) else: new_cell = player content = game_data['laby'][new_cell[1]][new_cell[0]] move = False if content == 0: #empty cell move = True elif content == 2: #teleport new_cell = tlps[tlps.index(new_cell)-1] move = True else: pass if move: game_data['laby'][new_cell[1]][new_cell[0]] = 4 game_data['laby'][player[1]][player[0]] = 2 if player in tlps else 0 game_data['player'] = new_cell return move def explode(game_data): """ Deletes the walls and teleporters in cross around the player if not a border. """ player = game_data['player'] for cell in ((player[0]-1, player[1]), (player[0]+1, player[1]), (player[0], player[1]-1), (player[0], player[1]+1)): if (not (cell[0] in (0, WIDTH-1) or cell[1] in (0, HEIGHT-1) or cell in game_data['enms'])): if game_data['laby'][cell[1]][cell[0]] == 1: game_data['walls_rmd'].append(cell) elif cell in game_data['tlps']: game_data['tlps'].remove(cell) game_data['tlps_rmd'].append(cell) game_data['laby'][cell[1]][cell[0]] = 0 def check_out(game_data): """ Returns if the the player had reached the output of the level. """ return game_data['player'] == (X_OUT, Y_OUT) def replay(game_data, win_obj): """ Generates clean game data and update the interface. """ game_data.update(init_laby()) update_ui(game_data, win_obj) def init_laby(score=0): """ Generate a new labyrinth with its teleporters and monsters. """ laby = constr_laby() tlps = place_teleports(laby) plyr = (X_START, Y_START) laby[Y_START][X_START] = 4 enms = place_enemies(laby) return {'laby':laby, 'is_new':True,#for drawing optimisation, set to False once it is drawn the first time 'tlps':tlps, 'enms':enms, 'walls_rmd':[],#list of exploded walls for each game turn 'tlps_rmd':[],#list of exploded teleporter for each game turn 'enms_mvd':[None for i in range(NB_ENMS)],#list of moved enemies for each game turn 'player':plyr, 'score':score, 'turn':0} def close_top(top, game_data, win_obj, do_next=lambda: None): """ Destroys a tk.Toplevel window, re-actives the key events listening, and performs the potential do_next function. """ top.destroy() win_obj['fenetre'].bind('', lambda evt: key_callback(game_data, win_obj, evt)) do_next() def abandon(game_data, win_obj): """ Creates a tk.Toplevel window which asks the player whether he wants to quit the game or continue playing. """ win_obj['fenetre'].bind('', lambda evt: None) #stop listening to events top = tk.Toplevel() top.title(win_obj['fenetre'].title() + ' - Quit') tk.Label(top, text='Quit the game ?').pack() tk.Button(top, text='Continue', command=lambda: close_top(top, game_data, win_obj) ).pack(side=LEFT, padx=2, pady=2) tk.Button(top, text=' Quit ', command=win_obj['fenetre'].quit ).pack(side=RIGHT, padx=2, pady=2) def victory(game_data, win_obj): """ Creates a tk.Toplevel window which asks the player whether he wants to quit the game or replay. """ win_obj['fenetre'].bind('', lambda evt: None) #stop listening to events top = tk.Toplevel() top.title(win_obj['fenetre'].title() + ' - Victory') tk.Label(top, text='You are free !\nQuit the game ?').pack() tk.Button(top, text='Replay', command=lambda: close_top(top, game_data, win_obj, lambda: replay(game_data, win_obj)) ).pack(side=LEFT, padx=2, pady=2) tk.Button(top, text=' Quit ', command=win_obj['fenetre'].quit ).pack(side=RIGHT, padx=2, pady=2) def key_callback(game_data, win_obj, evt): """ Reacts to a key pressed and updates the interface if needed. """ update = False direction = None if evt.char in ('a', 'A'): #may quit the game or do nothing abandon(game_data, win_obj) elif evt.char in ('z', 'Z') or evt.keycode == 111: direction = 'UP' elif evt.char in ('s', 'S') or evt.keycode == 116: direction = 'DOWN' elif evt.char in ('q', 'Q') or evt.keycode == 113: direction = 'LEFT' elif evt.char in ('d', 'D') or evt.keycode == 114: direction = 'RIGHT' elif evt.char in ('r', 'R'): #(r)eload a labyrinth game_data.update(init_laby(game_data['score']-(100 + game_data['turn']))) update = True elif evt.char in ('e', 'E'): #(e)xplode around explode(game_data) game_data['score'] -= game_data['turn'] update = True if move_player(game_data, direction): move_enemies(game_data) update = True game_data['turn'] += 1 if update: if check_out(game_data): game_data['score'] += 2*game_data['turn'] if game_data['score'] < SCORE_MAX: #load a new labyrinth, with score updated game_data.update(init_laby(game_data['score'])) else: victory(game_data, win_obj) #TRICK : victory() may either quit or do nothing #if nothing was done (ie the game did not quit) # we launch a new one with a score of 0 update_ui(game_data, win_obj) def draw_full_laby(canvas, laby): """ Draws all the cells of the labyrinth. """ for item in canvas.find_all(): canvas.delete(item) for y, line in enumerate(laby): for x, value in enumerate(line): if value >= 2: #on place un bloc "vide" sous les téléporteurs, ennemis et le joueur BLOCKS[0](canvas, x*H_SIZE, y*V_SIZE) BLOCKS[value](canvas, x*H_SIZE, y*V_SIZE) def draw_changed_laby(canvas, game_data): """ Redraws the player cell, the moved enemies, the removed teleports and walls. """ enm_itms = canvas.find_withtag('enm') if game_data['enms_mvd'].count(None) < NB_ENMS else [] tlp_itms = canvas.find_withtag('tlp') if game_data['tlps_rmd'] else [] wall_itms = canvas.find_withtag('wall') if game_data['walls_rmd'] else [] plr_itm = canvas.find_withtag('plr')[0] #only one item is tagged 'plr' for idx, old in enumerate(game_data['enms_mvd']): if not old: continue old_itm = enm_itms[[[int(coord) for coord in canvas.coords(itm)] for itm in enm_itms].index([ old[0]*H_SIZE+3, old[1]*V_SIZE+3, old[0]*H_SIZE+H_SIZE-3, old[1]*V_SIZE+V_SIZE-3])] canvas.delete(old_itm) cur = game_data['enms'][idx] BLOCKS[3](canvas, cur[0]*H_SIZE, cur[1]*V_SIZE) for tlp in game_data['tlps_rmd']: #teleports may have been destroyed (with explosion) tlp_itm = tlp_itms[[[int(coord) for coord in canvas.coords(itm)] for itm in tlp_itms].index([ tlp[0]*H_SIZE+1, tlp[1]*V_SIZE, (tlp[0]+1)*H_SIZE-1, (tlp[1]+1)*V_SIZE])] canvas.delete(tlp_itm) for wall in game_data['walls_rmd']: #walls may have been destroyed (with explosion) wall_itm = wall_itms[[[int(coord) for coord in canvas.coords(itm)] for itm in wall_itms].index([ wall[0]*H_SIZE, wall[1]*V_SIZE, (wall[0]+1)*H_SIZE, (wall[1]+1)*V_SIZE])] canvas.delete(wall_itm) canvas.delete(plr_itm) BLOCKS[4](canvas, game_data['player'][0]*H_SIZE, game_data['player'][1]*V_SIZE) def update_ui(game_data, win_obj): """ Draws the labyrinth and writes the score on the window. """ #win_obj['stringvar']['laby_txt'].set(laby_to_str(game_data['laby'])) canvas = win_obj['laby_zone'] if game_data['is_new']: draw_full_laby(canvas, game_data['laby']) game_data['is_new'] = False else: draw_changed_laby(canvas, game_data) game_data['enms_mvd'] = [None for i in range(NB_ENMS)] game_data['tlps_rmd'] = [] game_data['walls_rmd'] = [] win_obj['stringvar']['score'].set("SCORE : {} / {}".format(str(game_data['score']), SCORE_MAX)) def win_create(game_data): """ Creates the window and returns a dict containing objects of the window. """ fenetre = tk.Tk() fenetre.title('Labyrinthe') win_obj = {} win_obj['fenetre'] = fenetre #Some StringVar useful to keep the interface updated win_obj['stringvar'] = {'score':tk.StringVar()} #'laby_txt':tk.StringVar() #laby_zone = tk.Label(fenetre, # textvariable=win_obj['stringvar']['laby_txt'], # font=('monospace', 16)) win_obj['laby_zone'] = tk.Canvas(fenetre, width=H_SIZE*WIDTH, height=V_SIZE*HEIGHT, background='white') score_zone = tk.Label(fenetre, textvariable=win_obj['stringvar']['score'], font=('monospace', 12)) help_zone = tk.Label(fenetre, text='ZQSD or arrows, [E]xplode, [R]eload, [A]bandon', font=('monospace', 12)) update_ui(game_data, win_obj) win_obj['laby_zone'].pack(fill=BOTH) score_zone.pack() help_zone.pack() fenetre.bind('', lambda evt: key_callback(game_data, win_obj, evt)) return win_obj if __name__ == '__main__': GAME_DATA = init_laby() WIN_OBJ = win_create(GAME_DATA) WIN_OBJ['fenetre'].mainloop()