diff --git a/Tp5/minesweeper.zip b/Tp5/minesweeper.zip new file mode 100644 index 0000000000000000000000000000000000000000..687e5f0b46b39c599e0898f6bbf214e5d9e55113 Binary files /dev/null and b/Tp5/minesweeper.zip differ diff --git a/Tp5/minesweeper/apl1test.py b/Tp5/minesweeper/apl1test.py new file mode 100644 index 0000000000000000000000000000000000000000..8533ccaca5d99e7cfb83d6d86aa9334bb6a73a40 --- /dev/null +++ b/Tp5/minesweeper/apl1test.py @@ -0,0 +1,89 @@ +import thonnycontrib +from thonnycontrib.backend.evaluator import Evaluator +import thonnycontrib.backend.l1test_backend +from thonny.plugins.cpython_backend.cp_back import MainCPythonBackend +import thonnycontrib.backend.doctest_parser +from thonnycontrib.backend.doctest_parser import ExampleWithExpected, ExampleWithoutExpected +import thonnycontrib.backend.ast_parser +from thonnycontrib.backend.ast_parser import L1DocTest +import thonnycontrib.backend.verdicts +from thonnycontrib.backend.verdicts.ExceptionVerdict import ExceptionVerdict + +import inspect +import tempfile +import os +import sys + +class MockBackend(MainCPythonBackend): + """ + Fake backend. + """ + def __init__(self): + ... + + def send_message(self, msg) -> None: + ... + +# register backend +thonnycontrib.backend.l1test_backend.BACKEND = MockBackend() + +def l1test_to_org(filename: str, source: str=""): + """ + Return an org abstract of the tests presents in `filename` file. + """ + abstract = {'total': 0, + 'success': 0, + 'failures': 0, + 'errors': 0, + 'empty': 0} + + if source == "": + with open(filename, 'rt') as fin: + source = fin.read() + evaluator = Evaluator(filename=filename, + source=source) + tests = evaluator.evaluate() + n = len(tests) + abstract['total'] = n + res = "" + for test in tests: + examples = test.get_examples() + res_examples = "" + nb_test, nb_test_ok = 0, 0 + empty = True + for example in examples: + verdict = test.get_verdict_from_example(example) + if isinstance(example, ExampleWithExpected): + nb_test += 1 + if verdict.isSuccess(): + nb_test_ok += 1 + abstract['success'] += 1 + else: + abstract['failures'] += 1 + empty = False + if isinstance(verdict, ExceptionVerdict): + abstract['errors'] += 1 + empty = False + res_examples += f"** {verdict}\n\n" + if not verdict.isSuccess(): + res_examples += f" {verdict.get_details()}\n\n" + if not empty: + res += f"* {test.get_name()} ~ {nb_test_ok}/{nb_test} réussis\n\n" + else: + abstract['empty'] += 1 + res += f"* {test.get_name()}\n\n Aucun test trouvé !\n\n" + res += res_examples + res = f"Tests exécutés : {abstract['total']}\nSuccès: {abstract['success']}, \ +Echecs: {abstract['failures']}, Erreurs: {abstract['errors']}, \ +Vide: {abstract['empty']}\n\n" + res + return res + + +def testmod(modulename: str): + """ + mimic the doctest.testmod function + for `modulename` module + """ + print(l1test_to_org(modulename)) + + diff --git a/Tp5/minesweeper/cell.py b/Tp5/minesweeper/cell.py new file mode 100755 index 0000000000000000000000000000000000000000..d18269849a5efe260a8f1df7aebfbb5750a8bf73 --- /dev/null +++ b/Tp5/minesweeper/cell.py @@ -0,0 +1,125 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +:mod:`cell` module + +:author: + +:date: + + +""" + +class Cell: + + def __init__(self): + """ + initialize a new hidden cell of a minesweeper's grid. + existence of a bomb, number of bombs in neighborhood + have to be stated later. + + precondition: none + Examples: + + $$$ cel = Cell() + $$$ cel.is_bomb + False + $$$ cel.is_revealed + False + $$$ cel.nbombs_in_neighborhood + 0 + """ + + + self.is_bomb=False + self.is_revealed=False + self.nbombs_in_neighborhood=0 + + def incr_number_of_bombs_in_neighborhood(self): + """ + :return: None + :side effect: increment the number of bombs in neighborhood of self + precondition: none + Examples: + + $$$ cel = Cell() + $$$ cel.nbombs_in_neighborhood + 0 + $$$ cel.incr_number_of_bombs_in_neighborhood() + $$$ cel.nbombs_in_neighborhood + 1 + """ + + + self.nbombs_in_neighborhood+=1 + + def reveal(self): + """ + modify reveal state of self + precondition: none + Examples: + + $$$ cel = Cell() + $$$ cel.is_revealed + False + $$$ cel.reveal() + $$$ cel.is_revealed + True + """ + + self.is_revealed=True + + def set_bomb(self): + """ + put a bomb in self + + precondition: none + Examples: + + $$$ cel = Cell() + $$$ cel.is_bomb + False + $$$ cel.set_bomb() + $$$ cel.is_bomb + True + """ + + self.is_bomb=True + + def __str__(self): + """ + :return: a string representation of self state + :rtype: str + precondition: none + Examples: + + $$$ cel = Cell() + $$$ str(cel) == ' ' + True + $$$ cel.reveal() + $$$ str(cel) == '0' + True + $$$ cel.incr_number_of_bombs_in_neighborhood() + $$$ str(cel) == '1' + True + $$$ cel.set_bomb() + $$$ str(cel) == 'B' + True + """ + + + + if self.is_revealed: + if self.is_bomb: + return'B' + else: + return str(self.nbombs_in_neighborhood) + else: + + return' ' + +if (__name__ == '__main__'): + import apl1test + apl1test.testmod('cell.py') + diff --git a/Tp5/minesweeper/graphicalboard.py b/Tp5/minesweeper/graphicalboard.py new file mode 100644 index 0000000000000000000000000000000000000000..3ec294118a48acdd99b37dac365c852c6d55f1f0 --- /dev/null +++ b/Tp5/minesweeper/graphicalboard.py @@ -0,0 +1,174 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +:mod:`graphicalboard` module + +:author: `FIL - IEEA - Univ. Lille1.fr <http://portail.fil.univ-lille1.fr>`_ + +:date: 2015, september, last revision: 2024, february + +This module implements some functions to draw a minesweeper game. The +graphical board uses buttons to draw each cell and maps the left-click +and right-click events to interact with the minesweeper. + +This module uses from :mod:`minesweeper`: + +* :method:`Minesweeper.reveal_all_cells_from` + +To draw and run a minesweeper game, one has to: + +* create a minesweeper game g +* create a graphical board from the minesweeper g + +""" + +import os +import tkinter as tk +from functools import partial +from cell import Cell +from minesweeper import * + +# the list of icons +img = [] + +def create(game: Minesweeper): + """ + This function creates the graphical board from a game. It also + launches the event loop. Thus, this is the only function to run to + have a functional graphical board. + """ + global img + # create a new Tk window + win = tk.Tk() + # define the window title + win.title('Minesweeper ({:d} bombs)'.format(game.nbombs)) + # load images + iconpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icons") + img = [ + tk.PhotoImage(file=os.path.join(iconpath, "0.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "1.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "2.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "3.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "4.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "5.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "6.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "7.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "8.gif")), + tk.PhotoImage(file=os.path.join(iconpath, "9.gif")), # unrevealed + tk.PhotoImage(file=os.path.join(iconpath, "10.gif")), # bomb explosed + tk.PhotoImage(file=os.path.join(iconpath, "11.gif")), # bomb discovered + tk.PhotoImage(file=os.path.join(iconpath, "12.gif")), # flag + tk.PhotoImage(file=os.path.join(iconpath, "13.gif")) # question + ] + # create the graphical board made of Tk buttons + width, height = game.width, game.height + board = [] + for i in range(width): + board.insert(i, []) + for j in range(height): + button = tk.Button(win, padx=0, pady=0, width=19, height=19, image=img[9]) + button.grid(column=i, row=j) + board[i].insert(j, button) + # bind the right-click event + button.bind("<Button-3>", + partial(__changeflag, board=board, game=game, x=i, y=j)) + # bind the left-click event + button.config(command=partial(__changestate, board, game, i, j)) + # event loop + win.mainloop() + +def __test_end(board: list[list[tk.Button]], game: Minesweeper): + """ + This function tests if the game is finished or not. In the first + case, depending on the state of the game, all graphical cells are + disabled or events are unbinded. + """ + state = game.state + if state == GameState.losing: + __disable_game(board, game) + elif state == GameState.winning: + __block_game(board, game) + +def __changestate(board: list[list[tk.Button]], + game: Minesweeper, + x: int, y: int): + """ + This function is called on left-click on a button. + + """ + game.reveal_all_cells_from(x, y) + __redraw(board, game, x, y) + __test_end(board, game) + +def __changeflag(evt, board: list[list[tk.Button]], + game: Minesweeper, + x: int, y: int): + """ + This function is called on right-click on a button. + """ + cel = game.get_cell(x, y) + # if not cel.is_hypothetic(): + # cel.set_hypothetic() + # else: + # cel.unset_hypothetic() + __redraw(board, game, x, y) + __test_end(board, game) + + +def __block_game(board: list[list[tk.Button]], + game: Minesweeper): + """ + This function is called once the player wins. The chosen behavior + is to let the board as it and to unbind events. + """ + width, height = (game.width, game.height) + for i in range(width): + for j in range(height): + button = board[i][j] + game.get_cell(i, j).reveal() + button.config(command="") + button.bind("<Button-3>", "") + __redraw(board, game, -1, -1) + +def __disable_game(board: list[list[tk.Button]], + game: Minesweeper): + """ + This function is called once the player looses. The chosen behavior + is to shade the board and to unbind events. + """ + width, height = game.width, game.height + for i in range(width): + for j in range(height): + button = board[i][j] + button.config(state=tk.DISABLED) + button.bind("<Button-3>", "") + + +def __redraw(board: list[list[tk.Button]], + game: Minesweeper, + x: int, y: int): + """ + This function draws the board. Positions x and y are used to test + which bomb icon has to be drawn. + """ + width, height = (game.width, game.height) + for i in range(width): + for j in range(height): + cel = game.get_cell(i, j) + button = board[i][j] + if cel.is_revealed: + if cel.is_bomb: + new_img = img[10] + if x == j and y == i: + new_img = img[11] + else: + new_img = img[cel.nbombs_in_neighborhood] + button.config(relief=tk.FLAT, image=new_img, command="") + else: + button.config(image=img[9]) + + +if __name__ == "__main__": + import apl1test + doctest.testmod('graphicalboard.py') diff --git a/Tp5/minesweeper/icons/0.gif b/Tp5/minesweeper/icons/0.gif new file mode 100644 index 0000000000000000000000000000000000000000..26d8a1024c3e988cc71d1f44e5eb9785a24920e8 Binary files /dev/null and b/Tp5/minesweeper/icons/0.gif differ diff --git a/Tp5/minesweeper/icons/1.gif b/Tp5/minesweeper/icons/1.gif new file mode 100644 index 0000000000000000000000000000000000000000..fc817675c6f607b73904f16c369713edf2f914fc Binary files /dev/null and b/Tp5/minesweeper/icons/1.gif differ diff --git a/Tp5/minesweeper/icons/10.gif b/Tp5/minesweeper/icons/10.gif new file mode 100644 index 0000000000000000000000000000000000000000..10d928aa589648fbfd4e0c37a997cfa5e0eeaf2f Binary files /dev/null and b/Tp5/minesweeper/icons/10.gif differ diff --git a/Tp5/minesweeper/icons/11.gif b/Tp5/minesweeper/icons/11.gif new file mode 100644 index 0000000000000000000000000000000000000000..51fb6a928424a34021239bbb290456976666bcc1 Binary files /dev/null and b/Tp5/minesweeper/icons/11.gif differ diff --git a/Tp5/minesweeper/icons/12.gif b/Tp5/minesweeper/icons/12.gif new file mode 100644 index 0000000000000000000000000000000000000000..2854de27bbf73d5d8a9c21db979ab75f144a8a9f Binary files /dev/null and b/Tp5/minesweeper/icons/12.gif differ diff --git a/Tp5/minesweeper/icons/13.gif b/Tp5/minesweeper/icons/13.gif new file mode 100644 index 0000000000000000000000000000000000000000..947d01285d2ba0b83ccd14bf3fde61887798ad16 Binary files /dev/null and b/Tp5/minesweeper/icons/13.gif differ diff --git a/Tp5/minesweeper/icons/2.gif b/Tp5/minesweeper/icons/2.gif new file mode 100644 index 0000000000000000000000000000000000000000..2ef31056ccd581f2df4fe0f10e03f9ae831ac7ef Binary files /dev/null and b/Tp5/minesweeper/icons/2.gif differ diff --git a/Tp5/minesweeper/icons/3.gif b/Tp5/minesweeper/icons/3.gif new file mode 100644 index 0000000000000000000000000000000000000000..4abac7be126376b0b3c7a8dd202c91e51c401810 Binary files /dev/null and b/Tp5/minesweeper/icons/3.gif differ diff --git a/Tp5/minesweeper/icons/4.gif b/Tp5/minesweeper/icons/4.gif new file mode 100644 index 0000000000000000000000000000000000000000..4e942e211f7d29940e2f0868ad8888d6dc396064 Binary files /dev/null and b/Tp5/minesweeper/icons/4.gif differ diff --git a/Tp5/minesweeper/icons/5.gif b/Tp5/minesweeper/icons/5.gif new file mode 100644 index 0000000000000000000000000000000000000000..9b6b607be42cfbd7aed2dd30c2bafc1ce9d3e4c9 Binary files /dev/null and b/Tp5/minesweeper/icons/5.gif differ diff --git a/Tp5/minesweeper/icons/6.gif b/Tp5/minesweeper/icons/6.gif new file mode 100644 index 0000000000000000000000000000000000000000..2cc9004893ede6904937f1f6c3fdfea9d517e0de Binary files /dev/null and b/Tp5/minesweeper/icons/6.gif differ diff --git a/Tp5/minesweeper/icons/7.gif b/Tp5/minesweeper/icons/7.gif new file mode 100644 index 0000000000000000000000000000000000000000..aa755b7661dc088e2c73f8650f3b563bd3f2395e Binary files /dev/null and b/Tp5/minesweeper/icons/7.gif differ diff --git a/Tp5/minesweeper/icons/8.gif b/Tp5/minesweeper/icons/8.gif new file mode 100644 index 0000000000000000000000000000000000000000..283c520e31c108effb9d477e4359a3654ce8470e Binary files /dev/null and b/Tp5/minesweeper/icons/8.gif differ diff --git a/Tp5/minesweeper/icons/9.gif b/Tp5/minesweeper/icons/9.gif new file mode 100644 index 0000000000000000000000000000000000000000..50ac94d02f9e29ba36133da6281c3f0b9b2026ec Binary files /dev/null and b/Tp5/minesweeper/icons/9.gif differ diff --git a/Tp5/minesweeper/minesweeper.py b/Tp5/minesweeper/minesweeper.py new file mode 100755 index 0000000000000000000000000000000000000000..f0ab0dcb8e370c08a4a00e182274c54ec4f7b71e --- /dev/null +++ b/Tp5/minesweeper/minesweeper.py @@ -0,0 +1,204 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +:mod:`minesweeper` module + +:author: HERE YOUR NAME + +:date: + +This module provides functions and a class for minesweeper's game's management. + +""" + +import random +from enum import Enum +from cell import Cell + + +################################################ +# Type declaration +################################################ + +class GameState(Enum): + """ + A class to define an enumerated type with three values : + + * ``winning`` + * ``losing`` + * ``unfinished`` + + for the three state of minesweeper game. + """ + winning = 1 + losing = 2 + unfinished = 3 + + +############################################## +# Function for game's setup and management +############################################## + + +def neighborhood(x: int, y: int, width: int, height: int) -> list[tuple[int, int]]: + """ + return the list of coordinates of the neighbors of position (x,y) in a + grid of size width*height + + précondition: 0 <= x < width and 0 <= y < height + + examples: + + $$$ neighborhood(3, 3, 10, 10) + [(2, 2), (2, 3), (2, 4), (3, 2), (3, 4), (4, 2), (4, 3), (4, 4)] + $$$ neighborhood(0, 3, 10, 10) + [(0, 2), (0, 4), (1, 2), (1, 3), (1, 4)] + $$$ neighborhood(0, 0, 10, 10) + [(0, 1), (1, 0), (1, 1)] + $$$ neighborhood(9, 9, 10, 10) + [(8, 8), (8, 9), (9, 8)] + $$$ neighborhood(3, 9, 10, 10) + [(2, 8), (2, 9), (3, 8), (4, 8), (4, 9)] + """ + res =[] + if x>0: + res.append(x-1,y) + if x< width-1: + res.append(x+1,y) + if y>0: + res.append(x,y-1) + if y<height-1: + res.append(x,y+1) + return res +class Minesweeper(): + """ + $$$ game = Minesweeper(20, 10, 4) + $$$ game.width + 20 + $$$ game.height + 10 + $$$ game.nbombs + 4 + $$$ game == GameState.unfinished + True + $$$ cel = game.get_cell(1, 2) + $$$ cel.is_revealed + False + $$$ + """ + + def __init__(self, width: int=30, height: int=20, nbombs: int=99): + """ + build a minesweeper grid of size `width*height` of cells + with `nbombs` bombs randomly placed. + + for each build cell, this constructor sets the number of bombs + in neighborhood. + + precondition: width and height must be positive integers, and + nbombs <= width * height + + example: + + $$$ game = Minesweeper(20, 10, 4) + $$$ game.width + 20 + $$$ game.height + 10 + $$$ game.nbombs + 4 + $$$ game.state == GameState.unfinished + True + """ + ... + + def get_cell(self, x: int, y: int) -> Cell: + """ + return: the cell of coordinates (x,y) in the game's grid + + precondition: 0 <= x < width of game and O <= y < height of game + + $$$ game = Minesweeper(20, 10, 4) + $$$ sum(1 for x in range(20) for y in range(10) if game.get_cell(x, y).is_bomb) + 4 + """ + ... + + def _put_a_bomb_at(self, x: int, y: int): + """ + this method change the cells's state at x, y to a bomb, + + if the cell is already a bomb, nothing append. + otherwise it change cell's state and increments by one every + cells in its neighborhood. + + precondition: 0 <= x < self.width and 0 <= y < self.height + + exemples: + + $$$ game = Minesweeper(10, 5, 0) + $$$ voisins = neighborhood(1, 1, game.width, game.height) + $$$ all(game.get_cell(x, y).nbombs_in_neighborhood == 0 for x, y in voisins) + True + $$$ game._put_a_bomb_at(1, 1) + $$$ all(game.get_cell(x, y).nbombs_in_neighborhood == 1 for x, y in voisins) + True + $$$ game._put_a_bomb_at(1, 1) + $$$ all(game.get_cell(x, y).nbombs_in_neighborhood == 1 for x, y in voisins) + True + """ + ... + + def all_cells_are_revealed_or_bomb(self) -> bool: + """ + return True iff all cells are revealed or bomb. + + précondition: none + + """ + ... + + def reveal_all_cells_from(self, x, y): + """ + recursively reveal all cells of game game from the initial cell (x,y). + + * if the cell is a bomb one, update game's state to losing. + * otherwise if the cell's neighborhood doesn't contains bomb, + recursively reveal all neighboors. + * and finally, if all cell's are revealed, update game's state to + winning + + precondition: 0 <= x < width of game and O <= y < height of game + + exemples: + + $$$ game = Minesweeper(20, 10, 0) + $$$ game._put_a_bomb_at(1, 1) + $$$ game.state + GameState.unfinished + $$$ game.all_cells_are_revealed_or_bomb() + False + $$$ game.reveal_all_cells_from(5, 5) + $$$ game.all_cells_are_revealed_or_bomb() + False + $$$ game.reveal_all_cells_from(0, 0) + $$$ game.reveal_all_cells_from(1, 0) + $$$ game.reveal_all_cells_from(0, 1) + $$$ game.all_cells_are_revealed_or_bomb() + True + $$$ game.state + GameState.winning + $$$ game = Minesweeper(20, 10, 0) + $$$ game._put_a_bomb_at(1, 1) + $$$ game.reveal_all_cells_from(1, 1) + $$$ game.state + GameState.losing + """ + ... + + +if (__name__ == '__main__'): + import apl1test + apl1test.testmod('minesweeper.py') +