From e785dbd4f726c5716f21071ed25dc35ac87c0c74 Mon Sep 17 00:00:00 2001 From: Aineopintojen-harjoitustyo-Algoritmit-j Date: Sat, 17 Feb 2024 09:41:48 +0200 Subject: Dev tools and directory structure rework. --- .github/workflows/auto.yml | 12 +- README.md | 3 +- __main__.py | 60 +------- app.py | 42 ----- board/__init__.py | 4 - board/board.py | 252 ------------------------------ board/static.py | 34 ----- bots/__init__.py | 4 - bots/bot.py | 142 ----------------- bots/dssp.py | 74 --------- bots/simple.py | 29 ---- cmdline.py | 117 -------------- dev/README.md | 18 +++ dev/__init__.py | 8 + dev/commands.py | 36 +++++ doc/DEV.md | 16 -- doc/viikkoraportti5.pdf | Bin 0 -> 26328 bytes game/__init__.py | 2 - game/game.py | 41 ----- pyproject.toml | 17 ++- src/miinaharava/__main__.py | 57 +++++++ src/miinaharava/app.py | 42 +++++ src/miinaharava/board/__init__.py | 4 + src/miinaharava/board/board.py | 252 ++++++++++++++++++++++++++++++ src/miinaharava/board/static.py | 34 +++++ src/miinaharava/bots/__init__.py | 4 + src/miinaharava/bots/bot.py | 142 +++++++++++++++++ src/miinaharava/bots/dssp.py | 75 +++++++++ src/miinaharava/bots/simple.py | 30 ++++ src/miinaharava/cmdline.py | 117 ++++++++++++++ src/miinaharava/game/__init__.py | 2 + src/miinaharava/game/game.py | 41 +++++ src/miinaharava/tests/__init__.py | 0 src/miinaharava/tests/test_app.py | 241 +++++++++++++++++++++++++++++ src/miinaharava/tests/test_board.py | 295 ++++++++++++++++++++++++++++++++++++ src/miinaharava/tests/test_bot.py | 54 +++++++ src/miinaharava/tui/__init__.py | 3 + src/miinaharava/tui/ansi.py | 46 ++++++ src/miinaharava/tui/ansi_draw.py | 58 +++++++ src/miinaharava/tui/kbd.py | 86 +++++++++++ src/miinaharava/tui/static.py | 77 ++++++++++ src/miinaharava/tui/tui.py | 109 +++++++++++++ tests/__init__.py | 0 tests/test_app.py | 241 ----------------------------- tests/test_board.py | 295 ------------------------------------ tests/test_bot.py | 54 ------- tui/__init__.py | 3 - tui/ansi.py | 46 ------ tui/ansi_draw.py | 58 ------- tui/kbd.py | 86 ----------- tui/static.py | 76 ---------- tui/tui.py | 109 ------------- 52 files changed, 1851 insertions(+), 1797 deletions(-) delete mode 100644 app.py delete mode 100644 board/__init__.py delete mode 100644 board/board.py delete mode 100644 board/static.py delete mode 100644 bots/__init__.py delete mode 100644 bots/bot.py delete mode 100644 bots/dssp.py delete mode 100644 bots/simple.py delete mode 100644 cmdline.py create mode 100644 dev/README.md create mode 100644 dev/__init__.py create mode 100644 dev/commands.py delete mode 100644 doc/DEV.md create mode 100644 doc/viikkoraportti5.pdf delete mode 100644 game/__init__.py delete mode 100644 game/game.py create mode 100644 src/miinaharava/__main__.py create mode 100644 src/miinaharava/app.py create mode 100644 src/miinaharava/board/__init__.py create mode 100644 src/miinaharava/board/board.py create mode 100644 src/miinaharava/board/static.py create mode 100644 src/miinaharava/bots/__init__.py create mode 100644 src/miinaharava/bots/bot.py create mode 100644 src/miinaharava/bots/dssp.py create mode 100644 src/miinaharava/bots/simple.py create mode 100644 src/miinaharava/cmdline.py create mode 100644 src/miinaharava/game/__init__.py create mode 100644 src/miinaharava/game/game.py create mode 100644 src/miinaharava/tests/__init__.py create mode 100644 src/miinaharava/tests/test_app.py create mode 100644 src/miinaharava/tests/test_board.py create mode 100644 src/miinaharava/tests/test_bot.py create mode 100644 src/miinaharava/tui/__init__.py create mode 100644 src/miinaharava/tui/ansi.py create mode 100644 src/miinaharava/tui/ansi_draw.py create mode 100644 src/miinaharava/tui/kbd.py create mode 100644 src/miinaharava/tui/static.py create mode 100644 src/miinaharava/tui/tui.py delete mode 100644 tests/__init__.py delete mode 100644 tests/test_app.py delete mode 100644 tests/test_board.py delete mode 100644 tests/test_bot.py delete mode 100644 tui/__init__.py delete mode 100644 tui/ansi.py delete mode 100644 tui/ansi_draw.py delete mode 100644 tui/kbd.py delete mode 100644 tui/static.py delete mode 100644 tui/tui.py diff --git a/.github/workflows/auto.yml b/.github/workflows/auto.yml index 01f65bb..adec8b0 100644 --- a/.github/workflows/auto.yml +++ b/.github/workflows/auto.yml @@ -33,13 +33,9 @@ jobs: - name: Install poetry dependencies run: poetry install - # Run unittests - - name: Run unittests with coverage - run: poetry run python3 -m coverage run --branch -m pytest -v - - # coverage -> xml - - name: Generate coverage report - run: poetry run python3 -m coverage xml + # Run coverage xml + - name: Run unittests with coverage and generate xml + run: poetry run covxml # xml -> codecov - name: Upload coverage reports to Codecov @@ -49,4 +45,4 @@ jobs: # pylint - name: Run pylint against the code - run: poetry run python3 -m pylint -v . + run: poetry run pylint diff --git a/README.md b/README.md index f14cd76..4939406 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Miinaharava ratkaisijalla - [viikko 2](doc/viikkoraportti2.pdf) - [viikko 3](doc/viikkoraportti3.pdf) - [viikko 4](doc/viikkoraportti4.pdf) +- [viikko 5](doc/viikkoraportti5.pdf) ## Ohjeita: @@ -22,7 +23,7 @@ Miinaharava ratkaisijalla `python3 miinaharava` #### Lataamalla jokin tietty versio `wget -O - -https://github.com/Aineopintojen-harjoitustyo-Algoritmit-j/miinaharava/archive/refs/tags/v0.1-gamma.tar.gz | tar xz --strip-components=1 --one-top-level=miinaharava` +https://github.com/Aineopintojen-harjoitustyo-Algoritmit-j/miinaharava/archive/refs/tags/v0.1-delta.tar.gz | tar xz --strip-components=1 --one-top-level=miinaharava` ### Käyttöohje - [käyttöohje](doc/käyttöohje.pdf) diff --git a/__main__.py b/__main__.py index 253e699..4a91b1e 100644 --- a/__main__.py +++ b/__main__.py @@ -1,57 +1,3 @@ -""" __main__.py - Tästä suoritus alkaa """ -import sys -from app import App - -from cmdline import args - -vars(args)['board'] = None - -if args.count is None and args.file is None: - app = App(args) - IS_WIN = app.run() - del app - sys.exit(not IS_WIN) # Exit koodeissa 0 on onnistunut suoritus - -WIN_COUNT = 0 - -if args.file is None: - args.autoplay = 2 - RUN_COUNT = args.count - for i in range(RUN_COUNT): - print(end=f" \rSuoritus {i+1:>6}/{RUN_COUNT} ") - print(end=f"({100*WIN_COUNT/(i if i else 1):.1f}%)..") - if not args.quiet: - print() - app = App(args) - WIN_COUNT+=app.run() - del app -else: - RUN_COUNT = 0 - with open(args.file, "r", encoding="utf-8") as bfile: - board = [] - while True: - line = bfile.readline() - if not line or (line[0]!='.' and line[0]!='@'): - if board: - WIN_PERCENT = (100*WIN_COUNT/RUN_COUNT) if RUN_COUNT else 0 - print(end= - f" \rAjo ...{args.file[-18:]:} ({RUN_COUNT+1}): " - f"({WIN_PERCENT:.1f}%).." - ) - if not args.quiet: - print() - args.board = board - app = App(args) - WIN_COUNT += app.run() - RUN_COUNT += 1 - del app - board = [] - if not line: - break - continue - board.append([x=='@' for x in line if x in ('.', '@')]) - -print( - f"\n## Voittoja {WIN_COUNT}/{RUN_COUNT} " - f"({(100*WIN_COUNT/RUN_COUNT) if RUN_COUNT else 0:.1f}%)" -) +""" __main__.py - Käynnistellään ohjelma src/miinaharava kansiosta """ +from runpy import run_path +run_path("src/miinaharava", run_name="miinaharava") diff --git a/app.py b/app.py deleted file mode 100644 index 8553fe5..0000000 --- a/app.py +++ /dev/null @@ -1,42 +0,0 @@ -""" app.py - pääohjelma """ -from board import Board, Level -from tui import Tui -from game import Game -from bots import SimpleBot, DSSPBot - -# pylint: disable = too-few-public-methods -class App: - """ App - Luokka pääohjelmalle""" - def __init__(self, args=None): - board_opts = {'level': Level.BEGINNER} - tui_opts = {'bot': DSSPBot} - if args: - # pylint: disable = multiple-statements - if args.intermediate: board_opts['level'] = Level.INTERMEDIATE - if args.expert: board_opts['level'] = Level.EXPERT - if args.board: - board_opts['board'] = args.board - else: - if args.mines: board_opts['mines'] = args.mines - if args.size: - board_opts['width'] = args.size[0] - board_opts['height'] = args.size[1] - - if args.bot==0: tui_opts['bot'] = None - if args.bot==1: tui_opts['bot'] = SimpleBot - tui_opts['autoplay'] = args.autoplay > 0 - tui_opts['interactive'] = args.autoplay != 2 - tui_opts['suppress'] = args.quiet - tui_opts['delay'] = args.delay - - self.board = Board(**board_opts) - tui_opts['level_name'] = self.board.get_level_name() - tui_opts['height'] = self.board.get_height() - self.ui = Tui(**tui_opts) - self.game = Game(self.board,self.ui) - - def run(self): - """ käynnistää pelin """ - while self.game.next(): - pass - return self.board.is_winning() diff --git a/board/__init__.py b/board/__init__.py deleted file mode 100644 index ba87812..0000000 --- a/board/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" board - tämä alimoduli hoitaa pelilaudan käsittelyn sääntöjen mukaan """ - -from .board import Board -from .static import Level, LevelSpecs, Tile diff --git a/board/board.py b/board/board.py deleted file mode 100644 index 36ceb62..0000000 --- a/board/board.py +++ /dev/null @@ -1,252 +0,0 @@ -""" board/board.py - Pelilaudan käsittelyyn tarkoitetut jutut """ -from random import randrange -from sys import stderr -from copy import deepcopy - -from .static import Level, Tile, LevelSpecs - - -class Board(): - """ Board - Luokka joka pitää huolen pelilaudasta ja siihen kohdistuvista - siirroista. - """ - def __init__(self, - level = Level.BEGINNER, - width = None, - height = None, - mines = None, - board = None): - # pylint: disable = too-many-arguments - - if ( not width or not height or - width not in range(2,51) or - height not in range(2,51) ): - width, height = None, None - - self.__level = level - self.__width, self.__height, self.__mines =LevelSpecs[self.__level][:3] - - if width: - self.__width = width - if height: - self.__height = height - if height or width or mines: - self.__mines = mines - - if self.__mines not in range(1,self.__width*self.__height): - self.__mines = self.__width - - - if board and self.__validate_board(board): - self.__width, self.__height = self.__get_board_dimensions(board) - self.__mines = self.__get_board_mines(board) - else: - board = None - - for _, specs in LevelSpecs.items(): - if (self.__width, self.__height, self.__mines) == specs[:3]: - self.__level_name = specs[3] - break - else: - self.__level_name = "Mukautettu" - - self.__level_name += ( f" ({self.__width}x{self.__height}" - f", {self.__mines} miinaa)" ) - - self.__tiles = None - self.__masked = None - - self.__initialize_tiles() - if board: - self.__populate_with_board(board) - else: - self.__randomize_mines() - self.__calculate_neighbours() - - def __validate_board(self, board): - w = len(board[0]) - h = len(board) - if w not in range(2,51) or h not in range(2,51): - return False - for line in board: - if len(line)!=w: - return False - if self.__get_board_mines(board) not in range (1, w*h): - return False - return True - - - def __get_board_dimensions(self, board): - return len(board[0]), len(board) - - - def __get_board_mines(self, board): - return sum((sum(x) for x in board)) - - - def __populate_with_board(self, board): - for y in range(self.__height): - for x in range(self.__width): - if board[y][x]: - self.__tiles[x][y] = Tile.MINE - - def __initialize_tiles(self): - """ alustaa pelilaudan matriisit """ - w, h = self.__width, self.__height - self.__tiles = [[Tile.BLANK for _ in range(h)] for _ in range(w)] - self.__masked = [[Tile.UNOPENED for _ in range(h)] for _ in range(w)] - - - def __randomize_mines(self): - """ arpoo pelilaudalle pommit """ - for _ in range(self.__mines): - while True: - x, y = randrange(0,self.__width), randrange(0,self.__height) - if self.__tiles[x][y] != Tile.BLANK: - continue - self.__tiles[x][y] = Tile.MINE - break - - - def __calculate_neighbours(self): - """ laskee naapurissa olevien pommien määrät valmiiksi laudalle """ - for y in range(self.__height): - for x in range(self.__width): - if self.__tiles[x][y] == Tile.MINE: - continue - neighbouring_mines = 0 - for nx, ny in self.get_neighbours_coords(x,y): - if self.__tiles[nx][ny] == Tile.MINE: - neighbouring_mines += 1 - self.__tiles[x][y] = neighbouring_mines - - - def invalid_coordinates(self, x, y): - """ onko koordinaatit pelilaudan ulkopuolella """ - return x < 0 or x >= self.__width or y < 0 or y >= self.__height - - - def get_neighbours_coords(self, x, y, include_home = False): - """ antaa listan naapureiden koordinaateista """ - offsets = ( - (-1,-1), (0,-1), (1,-1), - (-1, 0), (0, 0), (1, 0), - (-1, 1), (0, 1), (1, 1) - ) if include_home else ( - (-1,-1), (0,-1), (1,-1), - (-1, 0), (1, 0), - (-1, 1), (0, 1), (1, 1) - ) - coordinates=[] - for dx,dy in offsets: - if not self.invalid_coordinates(x+dx, y+dy): - coordinates.append( (x+dx, y+dy) ) - return coordinates - - - def get_view(self): - """ antaa matriisin nykyisestä pelinäkymästä """ - view = deepcopy(self.__masked) - for y in range(self.__height): - for x in range(self.__width): - if not view[x][y]: - view[x][y]=self.__tiles[x][y] - return view - - - def is_winning(self): - """ tarkistaa onko peli voitettu """ - for y in range(self.__height): - for x in range(self.__width): - if self.__tiles[x][y] == Tile.MINE: - if not self.__masked[x][y]: - return False - else: - if self.__masked[x][y]: - return False - return True - - - def collect_area(self, x, y, area = None): - """ tunnustelee ja palauttaa tyhjän alueen koordinaatit """ - if area is None: - area = {(x,y)} - to_test = [] - for nx, ny in self.get_neighbours_coords(x, y): - if self.__tiles[nx][ny] == Tile.BLANK and (nx,ny) not in area: - to_test.append((nx, ny)) - area.add((nx, ny)) - for tx, ty in to_test: - area=area.union(self.collect_area(tx, ty, area)) - return area - - - def get_mask(self, x, y): - """ onko ruutu vielä piilossa """ - return self.__masked[x][y] - - - def flag(self, x, y, flag=-1): - """ aseta lippu peitetylle ruudulle""" - if self.invalid_coordinates(x, y): - print("Koordinaatit on pelilaudan ulkopuolella", file=stderr) - return False - - if self.__masked[x][y] not in range(10,14): - print("Ruudulla odottamaton lippu tai se on avattu", file=stderr) - return False - - if flag == -1: - self.__masked[x][y] += 1 if self.__masked[x][y] < 13 else -3 - return True - - if flag not in range(10,14): - print("Lippua jota asetat ei ole olemassa", file=stderr) - return False - - self.__masked[x][y]=flag - return True - - - def guess(self, x, y): - """ tee arvaus """ - if self.invalid_coordinates(x, y): - print("Koordinaatit on pelilaudan ulkopuolella", file=stderr) - return False - - if not self.__masked[x][y]: - print("Ruutu on jo avattu", file=stderr) - return False - - self.__masked[x][y] = 0 - - if self.__tiles[x][y] == Tile.MINE: - return False - - if self.__tiles[x][y] == Tile.BLANK: - for cx, cy in self.collect_area( x, y ): - for nx, ny in self.get_neighbours_coords(cx, cy, True): - self.__masked[nx][ny] = 0 - - return True - - def reveal(self): - """ näytä koko lauta """ - w, h = self.__width, self.__height - self.__masked = [[0 for _ in range(h)] for _ in range(w)] - - def get_width(self): - """ palauttaa laudan leveyden """ - return self.__width - - def get_height(self): - """ palauttaa laudan korkeuden """ - return self.__height - - def get_mines(self): - """ palauttaa pommien määrän """ - return self.__mines - - def get_level_name(self): - """ palauttaa vaikesutason nimen""" - return self.__level_name diff --git a/board/static.py b/board/static.py deleted file mode 100644 index 4804ded..0000000 --- a/board/static.py +++ /dev/null @@ -1,34 +0,0 @@ -""" board/static.py - määrittelyjä pelilaudan muuttumattomille asoille """ - -from enum import Enum, IntEnum - -class Level(Enum): - """ vaikeustasot """ - BEGINNER = 0 - INTERMEDIATE = 1 - EXPERT = 2 - - -class Tile(IntEnum): - """ alueiden selitteet """ - BLANK = 0 - ONE = 1 - TWO = 2 - THREE = 3 - FOUR = 4 - FIVE = 5 - SIX = 6 - SEVEN = 7 - EIGHT = 8 - MINE = 9 - FLAG_MINE = 10 - FLAG_FREE = 11 - UNOPENED = 12 - FLAG_UNKNOWN = 13 - - -LevelSpecs = { - Level.BEGINNER: ( 9, 9, 10, "Aloittelija"), - Level.INTERMEDIATE: ( 16, 16, 40, "Keskivaikea"), - Level.EXPERT: ( 30, 16, 99, "Edistynyt") -} diff --git a/bots/__init__.py b/bots/__init__.py deleted file mode 100644 index f346d07..0000000 --- a/bots/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" bots - tämä alimoduli tarjoaa tekoälyn """ - -from .simple import SimpleBot -from .dssp import DSSPBot diff --git a/bots/bot.py b/bots/bot.py deleted file mode 100644 index 2f3baae..0000000 --- a/bots/bot.py +++ /dev/null @@ -1,142 +0,0 @@ -""" bots/bot.py - bottien kantaisä """ -from tui import Action -from board import Tile - -class Bot(): - """ Bot - perusluokka perittäväksi """ - def __init__(self, **opts): - self.uncertain = opts['uncertain'] if 'uncertain' in opts else False - self.safe_tiles = set() - self.mine_tiles = set() - self.matrix = [] - self.w = 0 - self.h = 0 - - def search(self): - """ search - etsii pommeja tai vapaita ko joukkoihin """ - return False - - def lucky_guess(self): - """ lucky_guess - lisää yhden arvatun vapaan vapaiden joukkoon """ - return Action.NOOP, 0, 0 - - def get_hint_from_list(self): - """ palauttaa vihjeen suoraan listalta """ - if self.safe_tiles: - x, y = self.safe_tiles.pop() - return Action.SAFE, x, y - if self.mine_tiles: - x, y = self.mine_tiles.pop() - return Action.MINE, x, y - return Action.NOOP, 0, 0 - - def saved_hints(self): - """ poistetaan auenneet laatat ja palautetaan onko muuveja """ - for tile in list(self.safe_tiles): - if self.known_tile(tile): - self.safe_tiles.remove(tile) - for tile in list(self.mine_tiles): - if self.known_tile(tile): - self.mine_tiles.remove(tile) - return self.safe_tiles or self.mine_tiles - - def hint(self, matrix, cursor_x, cursor_y): - """ antaa vinkin. tässä tapauksessa ei mitään """ - self.matrix = matrix - self.w, self.h = self.get_dimensions() - - if self.saved_hints(): - return self.get_hint_from_list() - if self.search(): - return self.get_hint_from_list() - if self.uncertain and self.lucky_guess(): - return self.get_hint_from_list() - return Action.NOOP, cursor_x, cursor_y - - def get_dimensions(self): - """ palauttaa matriisin dimensiot """ - return len(self.matrix), len(self.matrix[0]) - - def get_neighbours(self, tile): - """ palauttaa viereiset koordinaatit joukkona """ - x, y = tile - offsets = ( - (-1, -1), ( 0, -1), ( 1, -1), - (-1, 0), ( 1, 0), - (-1, 1), ( 0, 1), ( 1, 1), - ) - tiles=set() - for ox, oy in offsets: - if ox+x in range(self.w): - if oy+y in range(self.h): - tiles.add((ox+x, oy+y)) - return tiles - - def get_value(self, tile): - """ palauttaa laatan arvon """ - return self.matrix[tile[0]][tile[1]] - - def remove_number_tiles(self, tiles): - """ poistaa vapaat ja vapaaksi merkityt alueet ja numerolaatat """ - for tile in list(tiles): - if self.matrix[tile[0]][tile[1]] < Tile.FLAG_MINE: - tiles.remove(tile) - - def remove_mine_tiles(self, tiles): - """ poistaa pommit ja pommiksi merkityt """ - count=0 - for tile in list(tiles): - if self.matrix[tile[0]][tile[1]] in (Tile.MINE, Tile.FLAG_MINE): - tiles.remove(tile) - count+=1 - return count - - def known_tile(self, tile): - """ tutkii onko laatta tiedetty """ - return self.matrix[tile[0]][tile[1]] < Tile.UNOPENED - - def number_tile(self, tile): - """ tutkii onko numerolaatta """ - return 0 < self.matrix[tile[0]][tile[1]] < Tile.MINE - - def count_unknowns(self, tiles): - """ laskee tunnistamattomat laatat """ - count=0 - for tile in list(tiles): - if not self.known_tile(tile): - count+=1 - return count - - - def get_interesting_tiles(self): - """ palauttaa laatat joiden naapureissa on vaihtelua """ - tiles = set() - for x in range(self.w): - for y in range(self.h): - if self.number_tile((x,y)): - n = self.get_neighbours((x,y)) - l = len(n) - r = self.count_unknowns(n) - if r in range(1,l-1): - tiles.add((x,y)) - return tiles - - def get_border_tiles(self): - """ palauttaa palauttaa numerolaatat joiden vieressä avaamaton """ - tiles = set() - for x in range(self.w): - for y in range(self.h): - if self.number_tile((x,y)): - n = self.get_neighbours((x,y)) - if self.count_unknowns(n): - tiles.add((x,y)) - return tiles - - def get_unknown_tiles(self): - """ palauttaa kaikki tuntemattomat laatat """ - tiles = set() - for x in range(self.w): - for y in range(self.h): - if not self.known_tile((x,y)): - tiles.add((x,y)) - return tiles diff --git a/bots/dssp.py b/bots/dssp.py deleted file mode 100644 index 0cace12..0000000 --- a/bots/dssp.py +++ /dev/null @@ -1,74 +0,0 @@ -""" bots/dssp.py - päättelee kahden vierekkäisen laatan perusteella """ -from random import sample -from .simple import SimpleBot - -class DSSPBot(SimpleBot): - """ DSSPBot - perustyhmä botti """ - - def search(self): - """ search - etsii kahden vierekkäisen laatan perusteella""" - if super().search(): - return True - tiles = list(self.get_interesting_tiles()) - pairs = [] - # pylint: disable = consider-using-enumerate - for i in range(len(tiles)): - for j in range(i+1,len(tiles)): - if abs(tiles[i][0]-tiles[j][0])==1 or abs(tiles[i][1]-tiles[j][1])==1: - pairs.append((tiles[i],tiles[j])) - pairs.append((tiles[j],tiles[i])) - - for tile1, tile2 in pairs: - c1 = self.get_value(tile1) - c2 = self.get_value(tile2) - n1 = self.get_neighbours(tile1) - n2 = self.get_neighbours(tile2) - self.remove_number_tiles(n1) - self.remove_number_tiles(n2) - c1 -= self.remove_mine_tiles(n1) - c2 -= self.remove_mine_tiles(n2) - - # otetaan vain alue1:n laatat pois vähennetään se pommeista - # näin tiedetään montako pommia on jäätävä yhteiselle alueelle - nc = n1 & n2 - n1 = n1 - nc - n2 = n2 - nc - cc = c1 - len(n1) - - # jos yhteiselle alueelle ei jääkkään pommeja - if cc < 1: - continue - - # vähennetään yhteinen alue ja sen pommit alueesta 2 - # jos jäljelle ei jää miinoja merkataan alueet seiffeiks - c2 -= cc - - if c2 == 0: - for safe in n2: - self.safe_tiles.add(safe) - - return self.saved_hints() - - def lucky_guess(self): - heatmap = dict.fromkeys(self.get_unknown_tiles(), float(0)) - tiles = self.get_border_tiles() - for tile in tiles: - n = self.get_neighbours(tile) - c = self.get_value(tile) - self.remove_mine_tiles(n) - self.remove_number_tiles(n) - for ntile in n: - heatmap[ntile] += c/len(n) - - for tile in heatmap: - if tile[0] in range(1,self.w-1): - heatmap[tile]+=0.005 - if tile[1] in range(1,self.h-1): - heatmap[tile]+=0.005 - - best = min((x for _, x in heatmap.items())) - best_tiles = [x for x,y in heatmap.items() if y == best] - - if best_tiles: - self.safe_tiles.add(sample(best_tiles,1)[0]) - return True - return False diff --git a/bots/simple.py b/bots/simple.py deleted file mode 100644 index 54e2d05..0000000 --- a/bots/simple.py +++ /dev/null @@ -1,29 +0,0 @@ -""" bots/simple.py - yksinkertainen botti joka etsii vain yhdeltä laatalta """ -from random import sample -from .bot import Bot - -class SimpleBot(Bot): - """ SimpleBot - perustyhmä botti """ - - def search(self): - """ simple_search - jos viereisten avaamattomien määrä tästmää """ - tiles = self.get_interesting_tiles() - for tile in tiles: - c = self.get_value(tile) - n = self.get_neighbours(tile) - self.remove_number_tiles(n) - c -= self.remove_mine_tiles(n) - if c == 0: - for safe in n: - self.safe_tiles.add(safe) - if c == len(n): - for mine in n: - self.mine_tiles.add(mine) - return self.saved_hints() - - def lucky_guess(self): - tiles = self.get_unknown_tiles() - if tiles: - self.safe_tiles.add(sample(sorted(tiles),1)[0]) - return True - return False diff --git a/cmdline.py b/cmdline.py deleted file mode 100644 index 533c40f..0000000 --- a/cmdline.py +++ /dev/null @@ -1,117 +0,0 @@ -""" cmdline.py - komentorivin käsittely argparse moduulilla """ -import sys -from argparse import ArgumentParser - -from tui import KEY_DESCRIPTIONS - -parser = ArgumentParser( - prog='miinaharava', - description='Klassisen miinaharavapelin terminaali toteutus.', - add_help=False -) -level_group = parser.add_argument_group('Vaikeustaso') -level_group.add_argument( - '-i', '--intermediate', - help='keskivaikea (oletus on aloittelija)', - action='store_true' -) -level_group.add_argument( - '-e', '--expert', - help='edistynyt (vaatii 100 merkkiä leveän terminaalin)', - action='store_true' -) - - -custom_group = parser.add_argument_group('Mukautettu vaikeustaso') -def board_size(wxh_string): - """ parsija laudan koolle, että on varmasti muotoa {leveys}x{korkeys} """ - w, h = wxh_string.split('x') - return (int(w), int(h)) -custom_group.add_argument( - '-s', '--size', - metavar='', - type= board_size, - dest='size', - help='Pelikentän koko, missä on {leveys}x{korkeus}.' -) -custom_group.add_argument( - '-m', '--mines', - metavar='', - type=int, - dest='mines', - help='Säätää pelilaulla olevien miinojen määrän :ksi.', -) - - -hint_group = parser.add_argument_group('Tekoäly') -hint_group.add_argument( - '-a', '--auto', - dest='autoplay', - default=0, - action='count', - help='Pelaa tekoälyn vihjeet. [-aa] Pelaa myös epävarmat.' -) -hint_group.add_argument( - '-b', '--bot', metavar='', - choices=range(3), - type=int, - default=2, - help='Valitsee tekoälyn , missä: 0: Ei tekoälyä 1: Yksinkertainen, 2: DSSP (oletus)', -) -hint_group.add_argument( - '-d', '--delay', metavar='', - type=float, - help='Odottaa ennen tekoälyn siirtoa sadasosasekuntia.', -) - -batch_group = parser.add_argument_group('Automatisointi') -batch_group.add_argument( - '-q', '--quiet', - help='Tulostaa minimaalisesti (asettaa myös [-aa])', - action='store_true' -) -batch_group.add_argument( - '-c', '--count', - metavar='', - type=int, - dest='count', - help='Suorittaa ohelmaa kertaa ja tulostaa voitto-osuuden.', -) - -def filename( fn_string ): - """ filename - parser tyyppi joka testaa saako tiedoston auki """ - try: - with open(fn_string, "r", encoding="utf-8"): - pass - except FileNotFoundError as e: - raise ValueError from e - return fn_string -batch_group.add_argument( - '-f', '--file', - metavar='', - type=filename, - dest='file', - help='Pelaa tiedostossa olevat miinaharavakentät.', -) - -misc_group = parser.add_argument_group('Sekalaista') -misc_group.add_argument( - '-h', '--help', - help='Tulostaa tämän viestin', - action='store_true' -) -misc_group.add_argument( - '-k', '--keys', - help='Tulostaa pelin näppäinkartan.', - action='store_true' -) - -args = parser.parse_args() - -if args.help: - parser.print_help() - sys.exit() - -if args.keys: - print(end=KEY_DESCRIPTIONS) - sys.exit() diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..dd0b2dc --- /dev/null +++ b/dev/README.md @@ -0,0 +1,18 @@ +# Ohjeita kehitykseen +## Riippuvuuksien asennus: +`PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring poetry install` + +## Aja pytest: +`poetry run pytest` + +## Generoi haarakattavuusraportti: +`poetry run covhtml` + +## Avaa haarakattavuusraportti Firefoxilla: +`poetry run covff` + +## Linttaus: +`poetry run pylint` + +## Kaikki +`poetry run all` diff --git a/dev/__init__.py b/dev/__init__.py new file mode 100644 index 0000000..52cc7d9 --- /dev/null +++ b/dev/__init__.py @@ -0,0 +1,8 @@ +from .commands import ( + dev_pylint, + dev_pytest, + dev_coverage, + dev_covhtml, + dev_covxml, + dev_covff, + dev_all) diff --git a/dev/commands.py b/dev/commands.py new file mode 100644 index 0000000..ed9341a --- /dev/null +++ b/dev/commands.py @@ -0,0 +1,36 @@ +import pytest +import pylint +from subprocess import run + +def dev_pylint(): + return pylint.run_pylint(argv=["-v", "src/miinaharava"]) + +def dev_pytest(): + return pytest.main(["-v"]) + +def dev_coverage(): + return run( + "poetry run python3 -m coverage run --branch -m pytest -v ".split() + ).returncode + +def dev_covhtml(): + if e := dev_coverage(): return e + return run( + "poetry run python3 -m coverage html".split() + ).returncode + +def dev_covxml(): + if e := dev_coverage(): return e + return run( + "poetry run python3 -m coverage xml".split() + ).returncode + +def dev_covff(): + if e := dev_covhtml(): return e + return run( + "firefox htmlcov/index.html".split() + ).returncode + +def dev_all(): + if e := dev_covff(): return e + return dev_pylint() diff --git a/doc/DEV.md b/doc/DEV.md deleted file mode 100644 index 6f3eec4..0000000 --- a/doc/DEV.md +++ /dev/null @@ -1,16 +0,0 @@ -# Ohjeita kehitykseen -## Riippuvuuksien asennus: -`PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring poetry install --no-root` - -## Aja pytest: -`poetry run python3 -m pytest` - -## Generoi haarakattavuusraportti: -`poetry run python3 -m coverage run --branch -m pytest -v && poetry run python3 -m coverage html && firefox htmlcov/index.html` - -## Linttaus: -`poetry run python3 -m pylint -v .` - -## Kaikki samassa: -`poetry run python3 -m coverage run --branch -m pytest -v && poetry run python3 -m coverage html && firefox htmlcov/index.html && poetry run python3 -m pylint -v .` - diff --git a/doc/viikkoraportti5.pdf b/doc/viikkoraportti5.pdf new file mode 100644 index 0000000..1aadf0a Binary files /dev/null and b/doc/viikkoraportti5.pdf differ diff --git a/game/__init__.py b/game/__init__.py deleted file mode 100644 index 066e2e3..0000000 --- a/game/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -""" game - pelin kulkuun liittyvä logiikka """ -from .game import Game diff --git a/game/game.py b/game/game.py deleted file mode 100644 index fefcf8f..0000000 --- a/game/game.py +++ /dev/null @@ -1,41 +0,0 @@ -""" game/game.py - pelin etenemiseen liittyvä ohjaus """ -from tui import Action - -class Game: - """ Game - peli """ - def __init__(self, board, ui): - self.board = board - self.ui = ui - self.x, self.y = board.get_width()//2, board.get_height()//2 - - - def __del__(self): - self.board.reveal() - self.ui.game_end(self.board.get_view()) - - - def next(self): - """ seuraava kiitos vai jotain muuta? """ - action, self.x, self.y = self.ui.matrix_selector( - self.board.get_view(), self.x, self.y - ) - match action: - case Action.QUIT: - return False - case Action.OPEN: - if self.board.get_mask(self.x, self.y): - if not self.board.guess(self.x, self.y): - self.ui.game_over( - self.board.get_view(), self.x, self.y - ) - return False - if self.board.is_winning(): - self.ui.game_win(self.board.get_view(), self.x, self.y) - return False - case Action.FLAG: - self.board.flag(self.x, self.y) - case Action.MINE: - self.board.flag(self.x, self.y, 10) - case Action.SAFE: - self.board.flag(self.x, self.y, 11) - return True diff --git a/pyproject.toml b/pyproject.toml index fd1ea78..ca26e7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [tool.poetry] name = "miinaharava" -version = "0.1.0" +version = "0.1" description = "Miinaharava ratkaisijalla" authors = ["Aineopintojen-harjoitustyo-Algoritmit-j "] readme = "README.md" +packages = [ { include = "miinaharava", from = "src" } ] [tool.poetry.dependencies] python = "^3.10" @@ -13,14 +14,18 @@ pytest = "^7.4.4" coverage = "^7.4.0" pylint = "^3.0.3" +[tool.poetry.scripts] +pylint = "dev:dev_pylint" +pytest = "dev:dev_pytest" +covhtml = "dev:dev_covhtml" +covxml = "dev:dev_covxml" +covff = "dev:dev_covff" +all = "dev:dev_all" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.pylint.main] recursive = true -source-roots = ["./"] - -[tool.pylint.basic] - -[tool.pylint.messages] +source-roots = ["src/miinaharava/"] diff --git a/src/miinaharava/__main__.py b/src/miinaharava/__main__.py new file mode 100644 index 0000000..ba7a25f --- /dev/null +++ b/src/miinaharava/__main__.py @@ -0,0 +1,57 @@ +""" __main__.py - Tästä suoritus alkaa """ +import sys + +from app import App +from cmdline import args + +vars(args)['board'] = None + +if args.count is None and args.file is None: + app = App(args) + IS_WIN = app.run() + del app + sys.exit(not IS_WIN) # Exit koodeissa 0 on onnistunut suoritus + +WIN_COUNT = 0 + +if args.file is None: + args.autoplay = 2 + RUN_COUNT = args.count + for i in range(RUN_COUNT): + print(end=f" \rSuoritus {i+1:>6}/{RUN_COUNT} ") + print(end=f"({100*WIN_COUNT/(i if i else 1):.1f}%)..") + if not args.quiet: + print() + app = App(args) + WIN_COUNT+=app.run() + del app +else: + RUN_COUNT = 0 + with open(args.file, "r", encoding="utf-8") as bfile: + board = [] + while True: + line = bfile.readline() + if not line or (line[0]!='.' and line[0]!='@'): + if board: + WIN_PERCENT = (100*WIN_COUNT/RUN_COUNT) if RUN_COUNT else 0 + print(end= + f" \rAjo ...{args.file[-18:]:} ({RUN_COUNT+1}): " + f"({WIN_PERCENT:.1f}%).." + ) + if not args.quiet: + print() + args.board = board + app = App(args) + WIN_COUNT += app.run() + RUN_COUNT += 1 + del app + board = [] + if not line: + break + continue + board.append([x=='@' for x in line if x in ('.', '@')]) + +print( + f"\n## Voittoja {WIN_COUNT}/{RUN_COUNT} " + f"({(100*WIN_COUNT/RUN_COUNT) if RUN_COUNT else 0:.1f}%)" +) diff --git a/src/miinaharava/app.py b/src/miinaharava/app.py new file mode 100644 index 0000000..8553fe5 --- /dev/null +++ b/src/miinaharava/app.py @@ -0,0 +1,42 @@ +""" app.py - pääohjelma """ +from board import Board, Level +from tui import Tui +from game import Game +from bots import SimpleBot, DSSPBot + +# pylint: disable = too-few-public-methods +class App: + """ App - Luokka pääohjelmalle""" + def __init__(self, args=None): + board_opts = {'level': Level.BEGINNER} + tui_opts = {'bot': DSSPBot} + if args: + # pylint: disable = multiple-statements + if args.intermediate: board_opts['level'] = Level.INTERMEDIATE + if args.expert: board_opts['level'] = Level.EXPERT + if args.board: + board_opts['board'] = args.board + else: + if args.mines: board_opts['mines'] = args.mines + if args.size: + board_opts['width'] = args.size[0] + board_opts['height'] = args.size[1] + + if args.bot==0: tui_opts['bot'] = None + if args.bot==1: tui_opts['bot'] = SimpleBot + tui_opts['autoplay'] = args.autoplay > 0 + tui_opts['interactive'] = args.autoplay != 2 + tui_opts['suppress'] = args.quiet + tui_opts['delay'] = args.delay + + self.board = Board(**board_opts) + tui_opts['level_name'] = self.board.get_level_name() + tui_opts['height'] = self.board.get_height() + self.ui = Tui(**tui_opts) + self.game = Game(self.board,self.ui) + + def run(self): + """ käynnistää pelin """ + while self.game.next(): + pass + return self.board.is_winning() diff --git a/src/miinaharava/board/__init__.py b/src/miinaharava/board/__init__.py new file mode 100644 index 0000000..ba87812 --- /dev/null +++ b/src/miinaharava/board/__init__.py @@ -0,0 +1,4 @@ +""" board - tämä alimoduli hoitaa pelilaudan käsittelyn sääntöjen mukaan """ + +from .board import Board +from .static import Level, LevelSpecs, Tile diff --git a/src/miinaharava/board/board.py b/src/miinaharava/board/board.py new file mode 100644 index 0000000..36ceb62 --- /dev/null +++ b/src/miinaharava/board/board.py @@ -0,0 +1,252 @@ +""" board/board.py - Pelilaudan käsittelyyn tarkoitetut jutut """ +from random import randrange +from sys import stderr +from copy import deepcopy + +from .static import Level, Tile, LevelSpecs + + +class Board(): + """ Board - Luokka joka pitää huolen pelilaudasta ja siihen kohdistuvista + siirroista. + """ + def __init__(self, + level = Level.BEGINNER, + width = None, + height = None, + mines = None, + board = None): + # pylint: disable = too-many-arguments + + if ( not width or not height or + width not in range(2,51) or + height not in range(2,51) ): + width, height = None, None + + self.__level = level + self.__width, self.__height, self.__mines =LevelSpecs[self.__level][:3] + + if width: + self.__width = width + if height: + self.__height = height + if height or width or mines: + self.__mines = mines + + if self.__mines not in range(1,self.__width*self.__height): + self.__mines = self.__width + + + if board and self.__validate_board(board): + self.__width, self.__height = self.__get_board_dimensions(board) + self.__mines = self.__get_board_mines(board) + else: + board = None + + for _, specs in LevelSpecs.items(): + if (self.__width, self.__height, self.__mines) == specs[:3]: + self.__level_name = specs[3] + break + else: + self.__level_name = "Mukautettu" + + self.__level_name += ( f" ({self.__width}x{self.__height}" + f", {self.__mines} miinaa)" ) + + self.__tiles = None + self.__masked = None + + self.__initialize_tiles() + if board: + self.__populate_with_board(board) + else: + self.__randomize_mines() + self.__calculate_neighbours() + + def __validate_board(self, board): + w = len(board[0]) + h = len(board) + if w not in range(2,51) or h not in range(2,51): + return False + for line in board: + if len(line)!=w: + return False + if self.__get_board_mines(board) not in range (1, w*h): + return False + return True + + + def __get_board_dimensions(self, board): + return len(board[0]), len(board) + + + def __get_board_mines(self, board): + return sum((sum(x) for x in board)) + + + def __populate_with_board(self, board): + for y in range(self.__height): + for x in range(self.__width): + if board[y][x]: + self.__tiles[x][y] = Tile.MINE + + def __initialize_tiles(self): + """ alustaa pelilaudan matriisit """ + w, h = self.__width, self.__height + self.__tiles = [[Tile.BLANK for _ in range(h)] for _ in range(w)] + self.__masked = [[Tile.UNOPENED for _ in range(h)] for _ in range(w)] + + + def __randomize_mines(self): + """ arpoo pelilaudalle pommit """ + for _ in range(self.__mines): + while True: + x, y = randrange(0,self.__width), randrange(0,self.__height) + if self.__tiles[x][y] != Tile.BLANK: + continue + self.__tiles[x][y] = Tile.MINE + break + + + def __calculate_neighbours(self): + """ laskee naapurissa olevien pommien määrät valmiiksi laudalle """ + for y in range(self.__height): + for x in range(self.__width): + if self.__tiles[x][y] == Tile.MINE: + continue + neighbouring_mines = 0 + for nx, ny in self.get_neighbours_coords(x,y): + if self.__tiles[nx][ny] == Tile.MINE: + neighbouring_mines += 1 + self.__tiles[x][y] = neighbouring_mines + + + def invalid_coordinates(self, x, y): + """ onko koordinaatit pelilaudan ulkopuolella """ + return x < 0 or x >= self.__width or y < 0 or y >= self.__height + + + def get_neighbours_coords(self, x, y, include_home = False): + """ antaa listan naapureiden koordinaateista """ + offsets = ( + (-1,-1), (0,-1), (1,-1), + (-1, 0), (0, 0), (1, 0), + (-1, 1), (0, 1), (1, 1) + ) if include_home else ( + (-1,-1), (0,-1), (1,-1), + (-1, 0), (1, 0), + (-1, 1), (0, 1), (1, 1) + ) + coordinates=[] + for dx,dy in offsets: + if not self.invalid_coordinates(x+dx, y+dy): + coordinates.append( (x+dx, y+dy) ) + return coordinates + + + def get_view(self): + """ antaa matriisin nykyisestä pelinäkymästä """ + view = deepcopy(self.__masked) + for y in range(self.__height): + for x in range(self.__width): + if not view[x][y]: + view[x][y]=self.__tiles[x][y] + return view + + + def is_winning(self): + """ tarkistaa onko peli voitettu """ + for y in range(self.__height): + for x in range(self.__width): + if self.__tiles[x][y] == Tile.MINE: + if not self.__masked[x][y]: + return False + else: + if self.__masked[x][y]: + return False + return True + + + def collect_area(self, x, y, area = None): + """ tunnustelee ja palauttaa tyhjän alueen koordinaatit """ + if area is None: + area = {(x,y)} + to_test = [] + for nx, ny in self.get_neighbours_coords(x, y): + if self.__tiles[nx][ny] == Tile.BLANK and (nx,ny) not in area: + to_test.append((nx, ny)) + area.add((nx, ny)) + for tx, ty in to_test: + area=area.union(self.collect_area(tx, ty, area)) + return area + + + def get_mask(self, x, y): + """ onko ruutu vielä piilossa """ + return self.__masked[x][y] + + + def flag(self, x, y, flag=-1): + """ aseta lippu peitetylle ruudulle""" + if self.invalid_coordinates(x, y): + print("Koordinaatit on pelilaudan ulkopuolella", file=stderr) + return False + + if self.__masked[x][y] not in range(10,14): + print("Ruudulla odottamaton lippu tai se on avattu", file=stderr) + return False + + if flag == -1: + self.__masked[x][y] += 1 if self.__masked[x][y] < 13 else -3 + return True + + if flag not in range(10,14): + print("Lippua jota asetat ei ole olemassa", file=stderr) + return False + + self.__masked[x][y]=flag + return True + + + def guess(self, x, y): + """ tee arvaus """ + if self.invalid_coordinates(x, y): + print("Koordinaatit on pelilaudan ulkopuolella", file=stderr) + return False + + if not self.__masked[x][y]: + print("Ruutu on jo avattu", file=stderr) + return False + + self.__masked[x][y] = 0 + + if self.__tiles[x][y] == Tile.MINE: + return False + + if self.__tiles[x][y] == Tile.BLANK: + for cx, cy in self.collect_area( x, y ): + for nx, ny in self.get_neighbours_coords(cx, cy, True): + self.__masked[nx][ny] = 0 + + return True + + def reveal(self): + """ näytä koko lauta """ + w, h = self.__width, self.__height + self.__masked = [[0 for _ in range(h)] for _ in range(w)] + + def get_width(self): + """ palauttaa laudan leveyden """ + return self.__width + + def get_height(self): + """ palauttaa laudan korkeuden """ + return self.__height + + def get_mines(self): + """ palauttaa pommien määrän """ + return self.__mines + + def get_level_name(self): + """ palauttaa vaikesutason nimen""" + return self.__level_name diff --git a/src/miinaharava/board/static.py b/src/miinaharava/board/static.py new file mode 100644 index 0000000..4804ded --- /dev/null +++ b/src/miinaharava/board/static.py @@ -0,0 +1,34 @@ +""" board/static.py - määrittelyjä pelilaudan muuttumattomille asoille """ + +from enum import Enum, IntEnum + +class Level(Enum): + """ vaikeustasot """ + BEGINNER = 0 + INTERMEDIATE = 1 + EXPERT = 2 + + +class Tile(IntEnum): + """ alueiden selitteet """ + BLANK = 0 + ONE = 1 + TWO = 2 + THREE = 3 + FOUR = 4 + FIVE = 5 + SIX = 6 + SEVEN = 7 + EIGHT = 8 + MINE = 9 + FLAG_MINE = 10 + FLAG_FREE = 11 + UNOPENED = 12 + FLAG_UNKNOWN = 13 + + +LevelSpecs = { + Level.BEGINNER: ( 9, 9, 10, "Aloittelija"), + Level.INTERMEDIATE: ( 16, 16, 40, "Keskivaikea"), + Level.EXPERT: ( 30, 16, 99, "Edistynyt") +} diff --git a/src/miinaharava/bots/__init__.py b/src/miinaharava/bots/__init__.py new file mode 100644 index 0000000..f346d07 --- /dev/null +++ b/src/miinaharava/bots/__init__.py @@ -0,0 +1,4 @@ +""" bots - tämä alimoduli tarjoaa tekoälyn """ + +from .simple import SimpleBot +from .dssp import DSSPBot diff --git a/src/miinaharava/bots/bot.py b/src/miinaharava/bots/bot.py new file mode 100644 index 0000000..2f3baae --- /dev/null +++ b/src/miinaharava/bots/bot.py @@ -0,0 +1,142 @@ +""" bots/bot.py - bottien kantaisä """ +from tui import Action +from board import Tile + +class Bot(): + """ Bot - perusluokka perittäväksi """ + def __init__(self, **opts): + self.uncertain = opts['uncertain'] if 'uncertain' in opts else False + self.safe_tiles = set() + self.mine_tiles = set() + self.matrix = [] + self.w = 0 + self.h = 0 + + def search(self): + """ search - etsii pommeja tai vapaita ko joukkoihin """ + return False + + def lucky_guess(self): + """ lucky_guess - lisää yhden arvatun vapaan vapaiden joukkoon """ + return Action.NOOP, 0, 0 + + def get_hint_from_list(self): + """ palauttaa vihjeen suoraan listalta """ + if self.safe_tiles: + x, y = self.safe_tiles.pop() + return Action.SAFE, x, y + if self.mine_tiles: + x, y = self.mine_tiles.pop() + return Action.MINE, x, y + return Action.NOOP, 0, 0 + + def saved_hints(self): + """ poistetaan auenneet laatat ja palautetaan onko muuveja """ + for tile in list(self.safe_tiles): + if self.known_tile(tile): + self.safe_tiles.remove(tile) + for tile in list(self.mine_tiles): + if self.known_tile(tile): + self.mine_tiles.remove(tile) + return self.safe_tiles or self.mine_tiles + + def hint(self, matrix, cursor_x, cursor_y): + """ antaa vinkin. tässä tapauksessa ei mitään """ + self.matrix = matrix + self.w, self.h = self.get_dimensions() + + if self.saved_hints(): + return self.get_hint_from_list() + if self.search(): + return self.get_hint_from_list() + if self.uncertain and self.lucky_guess(): + return self.get_hint_from_list() + return Action.NOOP, cursor_x, cursor_y + + def get_dimensions(self): + """ palauttaa matriisin dimensiot """ + return len(self.matrix), len(self.matrix[0]) + + def get_neighbours(self, tile): + """ palauttaa viereiset koordinaatit joukkona """ + x, y = tile + offsets = ( + (-1, -1), ( 0, -1), ( 1, -1), + (-1, 0), ( 1, 0), + (-1, 1), ( 0, 1), ( 1, 1), + ) + tiles=set() + for ox, oy in offsets: + if ox+x in range(self.w): + if oy+y in range(self.h): + tiles.add((ox+x, oy+y)) + return tiles + + def get_value(self, tile): + """ palauttaa laatan arvon """ + return self.matrix[tile[0]][tile[1]] + + def remove_number_tiles(self, tiles): + """ poistaa vapaat ja vapaaksi merkityt alueet ja numerolaatat """ + for tile in list(tiles): + if self.matrix[tile[0]][tile[1]] < Tile.FLAG_MINE: + tiles.remove(tile) + + def remove_mine_tiles(self, tiles): + """ poistaa pommit ja pommiksi merkityt """ + count=0 + for tile in list(tiles): + if self.matrix[tile[0]][tile[1]] in (Tile.MINE, Tile.FLAG_MINE): + tiles.remove(tile) + count+=1 + return count + + def known_tile(self, tile): + """ tutkii onko laatta tiedetty """ + return self.matrix[tile[0]][tile[1]] < Tile.UNOPENED + + def number_tile(self, tile): + """ tutkii onko numerolaatta """ + return 0 < self.matrix[tile[0]][tile[1]] < Tile.MINE + + def count_unknowns(self, tiles): + """ laskee tunnistamattomat laatat """ + count=0 + for tile in list(tiles): + if not self.known_tile(tile): + count+=1 + return count + + + def get_interesting_tiles(self): + """ palauttaa laatat joiden naapureissa on vaihtelua """ + tiles = set() + for x in range(self.w): + for y in range(self.h): + if self.number_tile((x,y)): + n = self.get_neighbours((x,y)) + l = len(n) + r = self.count_unknowns(n) + if r in range(1,l-1): + tiles.add((x,y)) + return tiles + + def get_border_tiles(self): + """ palauttaa palauttaa numerolaatat joiden vieressä avaamaton """ + tiles = set() + for x in range(self.w): + for y in range(self.h): + if self.number_tile((x,y)): + n = self.get_neighbours((x,y)) + if self.count_unknowns(n): + tiles.add((x,y)) + return tiles + + def get_unknown_tiles(self): + """ palauttaa kaikki tuntemattomat laatat """ + tiles = set() + for x in range(self.w): + for y in range(self.h): + if not self.known_tile((x,y)): + tiles.add((x,y)) + return tiles diff --git a/src/miinaharava/bots/dssp.py b/src/miinaharava/bots/dssp.py new file mode 100644 index 0000000..1815b49 --- /dev/null +++ b/src/miinaharava/bots/dssp.py @@ -0,0 +1,75 @@ +""" bots/dssp.py - päättelee kahden vierekkäisen laatan perusteella """ +from random import sample + +from .simple import SimpleBot + +class DSSPBot(SimpleBot): + """ DSSPBot - perustyhmä botti """ + + def search(self): + """ search - etsii kahden vierekkäisen laatan perusteella""" + if super().search(): + return True + tiles = list(self.get_interesting_tiles()) + pairs = [] + # pylint: disable = consider-using-enumerate + for i in range(len(tiles)): + for j in range(i+1,len(tiles)): + if abs(tiles[i][0]-tiles[j][0])==1 or abs(tiles[i][1]-tiles[j][1])==1: + pairs.append((tiles[i],tiles[j])) + pairs.append((tiles[j],tiles[i])) + + for tile1, tile2 in pairs: + c1 = self.get_value(tile1) + c2 = self.get_value(tile2) + n1 = self.get_neighbours(tile1) + n2 = self.get_neighbours(tile2) + self.remove_number_tiles(n1) + self.remove_number_tiles(n2) + c1 -= self.remove_mine_tiles(n1) + c2 -= self.remove_mine_tiles(n2) + + # otetaan vain alue1:n laatat pois vähennetään se pommeista + # näin tiedetään montako pommia on jäätävä yhteiselle alueelle + nc = n1 & n2 + n1 = n1 - nc + n2 = n2 - nc + cc = c1 - len(n1) + + # jos yhteiselle alueelle ei jääkkään pommeja + if cc < 1: + continue + + # vähennetään yhteinen alue ja sen pommit alueesta 2 + # jos jäljelle ei jää miinoja merkataan alueet seiffeiks + c2 -= cc + + if c2 == 0: + for safe in n2: + self.safe_tiles.add(safe) + + return self.saved_hints() + + def lucky_guess(self): + heatmap = dict.fromkeys(self.get_unknown_tiles(), float(0)) + tiles = self.get_border_tiles() + for tile in tiles: + n = self.get_neighbours(tile) + c = self.get_value(tile) - self.remove_mine_tiles(n) + self.remove_number_tiles(n) + for ntile in n: + heatmap[ntile] += c/len(n) + + for tile in heatmap: + if tile[0] in range(1,self.w-1): + heatmap[tile]+=0.005 + if tile[1] in range(1,self.h-1): + heatmap[tile]+=0.005 + + best = min((x for _, x in heatmap.items())) + best_tiles = [x for x,y in heatmap.items() if y == best] + + if best_tiles: + self.safe_tiles.add(sample(best_tiles,1)[0]) + return True + return False diff --git a/src/miinaharava/bots/simple.py b/src/miinaharava/bots/simple.py new file mode 100644 index 0000000..46b9506 --- /dev/null +++ b/src/miinaharava/bots/simple.py @@ -0,0 +1,30 @@ +""" bots/simple.py - yksinkertainen botti joka etsii vain yhdeltä laatalta """ +from random import sample + +from .bot import Bot + +class SimpleBot(Bot): + """ SimpleBot - perustyhmä botti """ + + def search(self): + """ simple_search - jos viereisten avaamattomien määrä tästmää """ + tiles = self.get_interesting_tiles() + for tile in tiles: + c = self.get_value(tile) + n = self.get_neighbours(tile) + self.remove_number_tiles(n) + c -= self.remove_mine_tiles(n) + if c == 0: + for safe in n: + self.safe_tiles.add(safe) + if c == len(n): + for mine in n: + self.mine_tiles.add(mine) + return self.saved_hints() + + def lucky_guess(self): + tiles = self.get_unknown_tiles() + if tiles: + self.safe_tiles.add(sample(sorted(tiles),1)[0]) + return True + return False diff --git a/src/miinaharava/cmdline.py b/src/miinaharava/cmdline.py new file mode 100644 index 0000000..533c40f --- /dev/null +++ b/src/miinaharava/cmdline.py @@ -0,0 +1,117 @@ +""" cmdline.py - komentorivin käsittely argparse moduulilla """ +import sys +from argparse import ArgumentParser + +from tui import KEY_DESCRIPTIONS + +parser = ArgumentParser( + prog='miinaharava', + description='Klassisen miinaharavapelin terminaali toteutus.', + add_help=False +) +level_group = parser.add_argument_group('Vaikeustaso') +level_group.add_argument( + '-i', '--intermediate', + help='keskivaikea (oletus on aloittelija)', + action='store_true' +) +level_group.add_argument( + '-e', '--expert', + help='edistynyt (vaatii 100 merkkiä leveän terminaalin)', + action='store_true' +) + + +custom_group = parser.add_argument_group('Mukautettu vaikeustaso') +def board_size(wxh_string): + """ parsija laudan koolle, että on varmasti muotoa {leveys}x{korkeys} """ + w, h = wxh_string.split('x') + return (int(w), int(h)) +custom_group.add_argument( + '-s', '--size', + metavar='', + type= board_size, + dest='size', + help='Pelikentän koko, missä on {leveys}x{korkeus}.' +) +custom_group.add_argument( + '-m', '--mines', + metavar='', + type=int, + dest='mines', + help='Säätää pelilaulla olevien miinojen määrän :ksi.', +) + + +hint_group = parser.add_argument_group('Tekoäly') +hint_group.add_argument( + '-a', '--auto', + dest='autoplay', + default=0, + action='count', + help='Pelaa tekoälyn vihjeet. [-aa] Pelaa myös epävarmat.' +) +hint_group.add_argument( + '-b', '--bot', metavar='', + choices=range(3), + type=int, + default=2, + help='Valitsee tekoälyn , missä: 0: Ei tekoälyä 1: Yksinkertainen, 2: DSSP (oletus)', +) +hint_group.add_argument( + '-d', '--delay', metavar='', + type=float, + help='Odottaa ennen tekoälyn siirtoa sadasosasekuntia.', +) + +batch_group = parser.add_argument_group('Automatisointi') +batch_group.add_argument( + '-q', '--quiet', + help='Tulostaa minimaalisesti (asettaa myös [-aa])', + action='store_true' +) +batch_group.add_argument( + '-c', '--count', + metavar='', + type=int, + dest='count', + help='Suorittaa ohelmaa kertaa ja tulostaa voitto-osuuden.', +) + +def filename( fn_string ): + """ filename - parser tyyppi joka testaa saako tiedoston auki """ + try: + with open(fn_string, "r", encoding="utf-8"): + pass + except FileNotFoundError as e: + raise ValueError from e + return fn_string +batch_group.add_argument( + '-f', '--file', + metavar='', + type=filename, + dest='file', + help='Pelaa tiedostossa olevat miinaharavakentät.', +) + +misc_group = parser.add_argument_group('Sekalaista') +misc_group.add_argument( + '-h', '--help', + help='Tulostaa tämän viestin', + action='store_true' +) +misc_group.add_argument( + '-k', '--keys', + help='Tulostaa pelin näppäinkartan.', + action='store_true' +) + +args = parser.parse_args() + +if args.help: + parser.print_help() + sys.exit() + +if args.keys: + print(end=KEY_DESCRIPTIONS) + sys.exit() diff --git a/src/miinaharava/game/__init__.py b/src/miinaharava/game/__init__.py new file mode 100644 index 0000000..066e2e3 --- /dev/null +++ b/src/miinaharava/game/__init__.py @@ -0,0 +1,2 @@ +""" game - pelin kulkuun liittyvä logiikka """ +from .game import Game diff --git a/src/miinaharava/game/game.py b/src/miinaharava/game/game.py new file mode 100644 index 0000000..fefcf8f --- /dev/null +++ b/src/miinaharava/game/game.py @@ -0,0 +1,41 @@ +""" game/game.py - pelin etenemiseen liittyvä ohjaus """ +from tui import Action + +class Game: + """ Game - peli """ + def __init__(self, board, ui): + self.board = board + self.ui = ui + self.x, self.y = board.get_width()//2, board.get_height()//2 + + + def __del__(self): + self.board.reveal() + self.ui.game_end(self.board.get_view()) + + + def next(self): + """ seuraava kiitos vai jotain muuta? """ + action, self.x, self.y = self.ui.matrix_selector( + self.board.get_view(), self.x, self.y + ) + match action: + case Action.QUIT: + return False + case Action.OPEN: + if self.board.get_mask(self.x, self.y): + if not self.board.guess(self.x, self.y): + self.ui.game_over( + self.board.get_view(), self.x, self.y + ) + return False + if self.board.is_winning(): + self.ui.game_win(self.board.get_view(), self.x, self.y) + return False + case Action.FLAG: + self.board.flag(self.x, self.y) + case Action.MINE: + self.board.flag(self.x, self.y, 10) + case Action.SAFE: + self.board.flag(self.x, self.y, 11) + return True diff --git a/src/miinaharava/tests/__init__.py b/src/miinaharava/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/miinaharava/tests/test_app.py b/src/miinaharava/tests/test_app.py new file mode 100644 index 0000000..1d13a71 --- /dev/null +++ b/src/miinaharava/tests/test_app.py @@ -0,0 +1,241 @@ +"""test_app.py - Testaa pelin suoritusta""" +# pylint: disable = missing-class-docstring, too-few-public-methods + +from io import StringIO +import unittest +from unittest.mock import patch + +from app import App + +from tui import Action + + +class KbdTest: + # pylint: disable = unused-argument, missing-function-docstring + def __init__(self, actions): + self.actions = actions + def read_action(self): + if self.actions: + action, _, _ = self.actions.pop(0) + else: + action = Action.NOOP + return action + def read_matrix_action(self, w, h, x, y): + return self.actions.pop(0) if self.actions else (Action.NOOP,x,y) + + +class TestAppClass(unittest.TestCase): + """ Testit itse appille """ + class DefaultArgs: + autoplay = 2 + intermediate = None + expert = None + board = None + mines = None + size = None + bot = None + quiet = None + delay = None + + sure_win_board = [ + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,1,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0] + ] + + sure_lose_board = [ + [1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1], + [1,1,1,1,0,1,1,1,1], + [1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1] + ] + + dssp_win_board = [ + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0], + [0,1,1,0,1,1,0,1,0] + ] + + mini_board = [ + [0,0], + [0,1] + ] + + + def test_run(self): + """ Testataan että edes pyörähtää """ + app = App(self.DefaultArgs) + app.run() + del app + + def test_quit(self): + """ Testataan Quittaamista """ + app = App() + app.ui.kbd=KbdTest([ + (Action.QUIT,0,0), + (Action.OPEN,0,0) + ]) + app.run() + del app + + def test_many_games(self): + """ Varman voiton lauta palauttaa true """ + class Args(self.DefaultArgs): + quiet = True + for _ in range(50): + app = App(Args) + app.run() + del app + Args.intermediate = True + for _ in range(20): + app = App(Args) + app.run() + del app + Args.expert = True + for _ in range(10): + app = App(Args) + app.run() + del app + + def test_sure_win(self): + """ Varman voiton lauta palauttaa true """ + class Args(self.DefaultArgs): + board = self.sure_win_board + quiet = True + app = App(Args) + self.assertTrue(app.run()) + del app + + def test_dssp_win(self): + """ Varman voiton lauta palauttaa true """ + class Args(self.DefaultArgs): + board = self.dssp_win_board + app = App(Args) + self.assertTrue(app.run()) + del app + + def test_no_dssp_win_with_simple(self): + """ Varman voiton lauta palauttaa true """ + class Args(self.DefaultArgs): + board = self.dssp_win_board + quiet = True + bot = 1 + while True: + app = App(Args) + if not app.run(): + break + del app + + def test_sure_lose(self): + """ Varman häviön lauta palauttaa false """ + class Args(self.DefaultArgs): + board = self.sure_lose_board + app = App(Args) + self.assertFalse(app.run()) + del app + + def test_custom_size(self): + """ Varman häviön lauta palauttaa false """ + class Args(self.DefaultArgs): + size = (4, 4) + with patch('sys.stdout', new = StringIO()) as captured: + app = App(Args) + app.run() + self.assertIn("Mukautettu (4x4", captured.getvalue()) + del app + + def test_sure_win_with_actions(self): + """ Varman voiton lauta palauttaa true """ + class Args(self.DefaultArgs): + board = self.sure_win_board + autoplay = 0 + bot = 0 + app = App(Args) + app.ui.kbd=KbdTest([ + (Action.SAFE,0,0), + (Action.OPEN,0,0) + ]) + self.assertTrue(app.run()) + del app + + def test_sure_lose_with_actions(self): + """ Varman voiton lauta palauttaa true """ + class Args(self.DefaultArgs): + board = self.sure_lose_board + autoplay = 0 + app = App(Args) + app.ui.kbd=KbdTest([ + (Action.FLAG,0,0), + (Action.MINE,0,0), + (Action.OPEN,0,0) + ]) + self.assertFalse(app.run()) + del app + + def test_auto_play_hints(self): + """ Vihjeiden automaattipelaaminen toimii """ + class Args(self.DefaultArgs): + board = self.dssp_win_board + autoplay = 1 + app = App(Args) + app.ui.kbd=KbdTest([ + (Action.OPEN,0,0), + (Action.HINT,0,0), + ]) + self.assertTrue(app.run()) + del app + + def test_delay(self): + """ Hidastus toimii """ + class Args(self.DefaultArgs): + board = self.dssp_win_board + delay = 5 + app = App(Args) + with patch('time.sleep') as patched_sleep: + self.assertTrue(app.run()) + del app + patched_sleep.assert_called() + + def test_delay_can_be_off(self): + """ Hidastus ei ole aina päälle """ + class Args(self.DefaultArgs): + board = self.dssp_win_board + app = App(Args) + with patch('time.sleep') as patched_sleep: + self.assertTrue(app.run()) + del app + patched_sleep.assert_not_called() + + def test_botless_play(self): + """ Hidastus toimii """ + class Args(self.DefaultArgs): + board = self.mini_board + autoplay = 0 + delay = 50000 + app = App(Args) + app.ui.kbd=KbdTest([ + (Action.OPEN,0,0), + (Action.HINT,0,0), + (Action.OPEN,1,0), + (Action.HINT,0,0), + (Action.OPEN,0,1) + ]) + self.assertTrue(app.run()) + del app diff --git a/src/miinaharava/tests/test_board.py b/src/miinaharava/tests/test_board.py new file mode 100644 index 0000000..8aeac89 --- /dev/null +++ b/src/miinaharava/tests/test_board.py @@ -0,0 +1,295 @@ +""" tests/test_board.py - Testit pelilaudalle """ + +import unittest + +from board import Board, Level, LevelSpecs + +def matrix_equals(m1, m2): + """ matrix_equals - apufunktio testaa onko matriisit samat """ + if len(m1)!=len(m2): + return False + # pylint: disable = consider-using-enumerate + for i in range(len(m1)): + if m1[i] != m2[i]: + return False + return True + +def matrix_swap_xy(m): + """ matrix_swap_xy - palauttaa matriisin korvattu x -> y ja y -> x """ + if not m: + return None + w, h = len(m[0]), len(m) + ret_m = [[0 for _ in range(h)] for _ in range(w)] + for y in range(h): + for x in range(w): + ret_m[x][y]=m[y][x] + return ret_m + + +class TestBoardClass(unittest.TestCase): + """ pelilauden testit kattava luokka """ + + def test_init_works_and_defaults_beginner(self): + """ pelilautaolion luominen onnistuu ja defaulttaa aloittelijaksi """ + b = Board() + self.assertEqual(b.get_width(), LevelSpecs[Level.BEGINNER][0]) + self.assertEqual(b.get_height(), LevelSpecs[Level.BEGINNER][1]) + self.assertEqual(b.get_mines(), LevelSpecs[Level.BEGINNER][2]) + self.assertTrue(b.get_width()>0) + + + def test_init_with_level(self): + """ olion luominen onnistuu vaikeustasolla""" + b = Board(level=Level.EXPERT) + self.assertEqual(b.get_width(), LevelSpecs[Level.EXPERT][0]) + self.assertEqual(b.get_height(), LevelSpecs[Level.EXPERT][1]) + self.assertEqual(b.get_mines(), LevelSpecs[Level.EXPERT][2]) + + + def test_init_with_custom_dimentions(self): + """ mukautetun kentän luominen onnistuu """ + b = Board(width=13, height=14, mines=15) + self.assertEqual(b.get_width(), 13) + self.assertEqual(b.get_height(), 14) + self.assertEqual(b.get_mines(), 15) + b = Board(width=22, height=12) + self.assertEqual(b.get_width(), 22) + self.assertEqual(b.get_height(), 12) + self.assertEqual(b.get_mines(), 22) + + + def test_init_with_incorect_dimensions(self): + """ luominen ei saa onnitua mahdottomilla mitoilla """ + b = Board(width=1, height=999, mines=0) + self.assertEqual(b.get_width(), LevelSpecs[Level.BEGINNER][0]) + self.assertEqual(b.get_height(), LevelSpecs[Level.BEGINNER][1]) + self.assertEqual(b.get_mines(), LevelSpecs[Level.BEGINNER][2]) + + + def test_init_with_valid_board(self): + """ Pelilaudan luominen onnistuu kelvollisella asettelulla """ + t = [ + [0,0,0,0], + [0,0,0,1], + [0,0,0,0] + ] + b = Board(board = t) + self.assertEqual(b.get_width(), 4) + self.assertEqual(b.get_height(), 3) + self.assertEqual(b.get_mines(), 1) + + + def test_init_with_invalid_board(self): + """ Yritetään luoda peli kelvottomalla laudalla """ + t = [ + [0,0,0,0,0], + [0,0,0,1], + [0,0,0,0,0] + ] + b = Board(board = t) + # Resetoituihan aloittelijan lauta + self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) + + t = [ + [0,1,0,0,0], + ] + b = Board(board = t) + self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) + + t = [ + [0,0,0,0], + [0,0,0,0], + [0,0,0,0] + ] + b = Board(board = t) + self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) + + t = [ + [1,1,1], + [1,1,1], + [1,1,1] + ] + b = Board(board = t) + self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) + + + def test_tiles_and_masks_ok(self): + """ Luohan luokka sisäiset laatat ja maskit oikein """ + # pylint: disable = protected-access + t = [ + [0,0,0,0], + [0,0,0,1], + [0,0,0,0] + ] + b = Board(board = t) + self.assertEqual(b.get_width(), 4) + self.assertEqual(b.get_height(), 3) + self.assertEqual(b.get_mines(), 1) + + # testataan onko laatat tallennettu oikein luokkaan + t = matrix_swap_xy([ + [0,0,1,1], + [0,0,1,9], + [0,0,1,1] + ]) + self.assertTrue(matrix_equals(b._Board__tiles, t)) + + # onko maksit asetettu oikein + t = matrix_swap_xy([ + [12,12,12,12], + [12,12,12,12], + [12,12,12,12] + ]) + self.assertTrue(matrix_equals(b._Board__masked, t)) + + + def test_get_view_and_guess(self): + """ laudan näkymä on oikein senkin jälkeen kun on arvattu """ + + t = [ + [0,0,1], + [0,0,0], + [0,0,0] + ] + b = Board(board=t) + + # Antaahan pelikenttä pelkkää maskia aluksi + t = matrix_swap_xy([ + [12,12,12], + [12,12,12], + [12,12,12] + ]) + self.assertTrue(matrix_equals(b.get_view(), t)) + + # avataan yläkulma -> palatuu True + self.assertTrue(b.guess(0,0)) + + # onko näkymä nyt oikein + t = matrix_swap_xy([ + [0,1,12], + [0,1,1], + [0,0,0] + ]) + self.assertTrue(matrix_equals(b.get_view(), t)) + + # avataan alakulma jossa miina -> palautuu False + self.assertFalse(b.guess(2,0)) + + + def test_is_winning(self): + """ toimiiko voittotilanteen tunnistus """ + + t = [ + [0,1], + [0,0], + ] + b = Board(board=t) + self.assertFalse(b.is_winning()) + + # Avataan ruutu jolla ei tule viellä voittoa + t = matrix_swap_xy([ + [1,12], + [12,12] + ]) + self.assertTrue(b.guess(0,0)) + self.assertTrue(matrix_equals(b.get_view(), t)) + self.assertFalse(b.is_winning()) + + # Avataan loputkin ruudut, jolloin pitäisi voittaa + t = matrix_swap_xy([ + [1,12], + [1,1] + ]) + self.assertTrue(b.guess(0,1)) + self.assertTrue(b.guess(1,1)) + self.assertTrue(matrix_equals(b.get_view(), t)) + self.assertTrue(b.is_winning()) + + # Lupuksi avataan miina jolloin voittoa ei enää pitäisi olla + t = matrix_swap_xy([ + [1,9], + [1,1] + ]) + self.assertFalse(b.guess(1,0)) + self.assertTrue(matrix_equals(b.get_view(), t)) + self.assertFalse(b.is_winning()) + + + def test_error_conditions_in_guess(self): + """ ruudun avaus alueen ulkopuolelta tai avatussa ruudussa ei onnistu""" + t = [ + [0,1], + [0,0], + ] + b = Board(board=t) + self.assertFalse(b.guess(2,2)) + self.assertTrue(b.guess(0,0)) + self.assertFalse(b.guess(0,0)) + + + def test_get_mask(self): + """ maski annetaan oikein """ + t = [ + [1,0], + [0,0], + ] + b = Board(board=t) + self.assertEqual(b.get_mask(1,0), 12) + self.assertTrue(b.guess(1,0)) + self.assertFalse(b.get_mask(1,0)) + + + def test_flag(self): + """ ruudun lipun vaihto ja asetus toimii """ + t = [ + [0,0], + [0,1], + ] + b = Board(board=t) + self.assertEqual(b.get_mask(0,0), 12) + self.assertTrue(b.flag(0,0)) + self.assertEqual(b.get_mask(0,0), 13) + self.assertTrue(b.flag(0,0)) + self.assertEqual(b.get_mask(0,0), 10) + self.assertTrue(b.flag(0,0)) + self.assertEqual(b.get_mask(0,0), 11) + self.assertTrue(b.flag(0,0)) + self.assertEqual(b.get_mask(0,0), 12) + self.assertTrue(b.flag(0,0,10)) + self.assertEqual(b.get_mask(0,0), 10) + + + def test_flag_error_conditions(self): + """ liputus ei onnistu jos avattu, alueen ulkopuolella, outo arvo """ + t = [ + [0,1], + [1,1], + ] + b = Board(board=t) + self.assertFalse(b.flag(0,0,6)) # Lippu jota ei ole + self.assertFalse(b.flag(2,2)) # Alueen ulkopuolella + self.assertTrue(b.guess(0,0)) + self.assertFalse(b.flag(0,0)) # Avattu laatta + + + def test_reveal(self): + """ paljastuksen jälkeen näkyy laatat sellaisenaan """ + t = [ + [0,1], + [1,1], + ] + b = Board(board=t) + b.reveal() + t = matrix_swap_xy([ + [3,9], + [9,9] + ]) + self.assertTrue(matrix_equals(b.get_view(), t)) + + + def test_get_level_name(self): + """ Testataan että nykyinen vaikeustaso palautuu oikein """ + b = Board(level=Level.INTERMEDIATE) + self.assertIn(LevelSpecs[Level.INTERMEDIATE][3], b.get_level_name()) + b = Board(level=Level.INTERMEDIATE, width=25, mines=2) + self.assertIn("Mukautettu", b.get_level_name()) diff --git a/src/miinaharava/tests/test_bot.py b/src/miinaharava/tests/test_bot.py new file mode 100644 index 0000000..4dab148 --- /dev/null +++ b/src/miinaharava/tests/test_bot.py @@ -0,0 +1,54 @@ +""" tests/test_bot.py - Testaa botin toimintaa""" +# pylint: disable = missing-class-docstring, too-few-public-methods, protected-access + +import unittest + +from board import Board, Tile +from bots import DSSPBot, SimpleBot +from tui import Action + +class TestBotClass(unittest.TestCase): + """ botin testit""" + def test_init(self): + """ olioden luominen onnistuu """ + DSSPBot() + SimpleBot() + + def correctly_marking(self, open_free=False, bot_type=DSSPBot): + """ Testaa onko miinat miinoja ja vapaat vapaita alkuun avatusta """ + for _ in range(500): + brd = Board() + # jos ei aukea ylälaidasta otetaan seuraava + if not brd.guess(0,0): + continue + # vain varmat liikut + bot = bot_type(uncertain=False) + + tested = set() + while True: + action, x, y = bot.hint(brd.get_view(), 0, 0) + if (x,y) in tested: + break + tested.add((x,y)) + if action == Action.SAFE: + self.assertTrue( brd._Board__tiles[x][y] < Tile.MINE ) + if open_free: + brd.guess(x,y) + if action == Action.MINE: + self.assertTrue( brd._Board__tiles[x][y] == Tile.MINE ) + + def test_dssp_marks_correctly_with_open(self): + """ Testaa onko dssp:n miinat miinoja ja avaa vapaat """ + self.correctly_marking(True, DSSPBot) + + def test_simple_marks_correctly_with_open(self): + """ Testaa onko dssp:n miinat miinoja ja avaa vapaat """ + self.correctly_marking(True, SimpleBot) + + def test_dssp_marks_correctly(self): + """ Testaa onko dssp:n miinat miinoja ja vapaat vapaita """ + self.correctly_marking(False, DSSPBot) + + def test_simple_marks_correctly(self): + """ Testaa onko simple:n miinat miinoja ja vapaat vapaita """ + self.correctly_marking(False, SimpleBot) diff --git a/src/miinaharava/tui/__init__.py b/src/miinaharava/tui/__init__.py new file mode 100644 index 0000000..0c8d632 --- /dev/null +++ b/src/miinaharava/tui/__init__.py @@ -0,0 +1,3 @@ +""" tui - hoitaa käyttäjälle katseltavaa ja havaitsee syötteet """ +from .tui import Tui +from .static import Action, KEY_DESCRIPTIONS diff --git a/src/miinaharava/tui/ansi.py b/src/miinaharava/tui/ansi.py new file mode 100644 index 0000000..c25ff6c --- /dev/null +++ b/src/miinaharava/tui/ansi.py @@ -0,0 +1,46 @@ +""" ansi.py - ansi ohjauskomentoja. värit jne """ + +class Ansi: + """ Ansi - Luokallinen staattisia metodeja ansi komennoille """ + + BLACK = 0 + RED = 1 + GREEN = 2 + YELLOW = 3 + BLUE = 4 + MAGENTA = 5 + CYAN = 6 + WHITE = 7 + GRAY = 8 + BRIGHT_RED = 9 + BRIGHT_GREEN = 0xA + BRIGHT_YELLOW = 0xB + BRIGHT_BLUE = 0xC + BRIGHT_MAGENTA = 0xD + BRIGHT_CYAN = 0xE + BRIGHT_WHITE = 0xF + + @staticmethod + def color(color): + """ asettaa tekstin värin """ + if color in range(16): + print(end=f"\033[{'1;' if color//8 else ''}3{color%8}m") + + + @staticmethod + def bg(color): + """ asettaa tekstin taustan värin""" + if color in range(8): + print(end=f"\033[4{color}m") + + + @staticmethod + def cup(lines): + """ liikuttaa kursoria ylöspäin""" + print(end=f"\033[{lines}F") + + + @staticmethod + def reset(): + """ resetoi tekstin värin ja muut attribuutit perusarvoille """ + print(end="\033[0m") diff --git a/src/miinaharava/tui/ansi_draw.py b/src/miinaharava/tui/ansi_draw.py new file mode 100644 index 0000000..ba71fdb --- /dev/null +++ b/src/miinaharava/tui/ansi_draw.py @@ -0,0 +1,58 @@ +""" tui/ansi_draw.py - perustukset ansi tulostelulle """ +# pylint: disable = multiple-imports +from .ansi import Ansi +from .static import TileTypes + +class AnsiDraw(): + """ AnsiDraw - "piirtelee" näytölle kirjailmilla """ + def __init__(self, height = 9, name = ""): + print(end='\n'*height+name+": Peli alkaa.") + + def __del__(self): + print() + + def __tile(self, tile, hilighted): + """ "piirtää" yhden ruudun """ + for ch, colors in zip(TileTypes[tile].text, TileTypes[tile].colors): + color, bg = colors + Ansi.color(Ansi.BLACK if hilighted else color) + Ansi.bg(Ansi.CYAN if hilighted else bg) + print(end=ch) + Ansi.reset() + + + def matrix(self, matrix, hx, hy): + """ "piirtää" ruudukon """ + Ansi.cup(len(matrix[0])) + # pylint: disable=consider-using-enumerate + for y in range(len(matrix[0])): + for x in range(len(matrix)): + hilight = matrix[x][y] != 9 and x == hx and y == hy + self.__tile(matrix[x][y], hilight) + print() + + + def status_line(self, text): + """ draw_status_line - tulostaa pelitietorivin""" + print(end=text+'\r') + +class SuppressDraw(): + """ SuppressDraw - vain status """ + # pylint: disable = unused-argument + + def matrix(self, matrix, hx, hy): + """ "piirtää" ruudukon """ + + def status_line(self, text): + """ draw_status_line - tulostaa pelitietorivin""" + print(end=text+'\r') + +class NoDraw(): + """ NoDraw - ei mitään """ + # pylint: disable = unused-argument + + def matrix(self, matrix, hx, hy): + """ "piirtää" ruudukon """ + + def status_line(self, text): + """ draw_status_line - tulostaa pelitietorivin""" diff --git a/src/miinaharava/tui/kbd.py b/src/miinaharava/tui/kbd.py new file mode 100644 index 0000000..a31e56f --- /dev/null +++ b/src/miinaharava/tui/kbd.py @@ -0,0 +1,86 @@ +""" tui/kbd.py - näppäimistön käsittellijä """ +# pylint: disable = multiple-imports +import termios, fcntl, sys, os, io +from time import sleep +from .static import ActionKeys, Action + +class NoKbd(): + """ NoKbd - näppis-ei-käsittelijä """ + # pylint: disable = unused-argument + def read_action(self): + """ read_action - ilman näppistä -> loppu """ + return Action.QUIT + + def read_matrix_action(self, w, h, x, y): + """ read_matrix_action - ilman näppistä -> loppu """ + return Action.QUIT, 0, 0 + +class Kbd(): + """ Kbd - näppiskäsittelijä """ + def __init__(self): + # Vaatii hieman terminaaliasetusten muokkaamista jotta yksittäiset + # napin painallukset voidaan lukea + # https://stackoverflow.com/questions/983354/how-do-i-wait-for-a-pressed-key + try: + fd = sys.stdin.fileno() + self.oldterm = termios.tcgetattr(fd) + + newattr = termios.tcgetattr(fd) + newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, newattr) + + self.oldflags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK) + # Testeissä ei voi mukata termilaalia + except io.UnsupportedOperation: + pass + + def __del__(self): + # palautetaan terminaali takaisin alkupetäiseen uskoon + try: + fd = sys.stdin.fileno() + termios.tcsetattr(fd, termios.TCSAFLUSH, self.oldterm) + fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags) + # Testeissä ei voi mukata termilaalia + except io.UnsupportedOperation: + pass + + def read_action(self): + """ lukee näppäimistölä käyttäjän toiminnon """ + while True: + # Ehkä riittää jos näppäimiä luetaan 50x sekunnissa + sleep(0.02) + try: + keycode = sys.stdin.read(16) + except KeyboardInterrupt: + return Action.QUIT + if keycode: + for key, action in ActionKeys.items(): + if keycode.startswith(key): + return action + + def read_matrix_action(self, w, h, x, y): + """ read_matrix_action - lukee actionit ja pitää huolen koordinaat""" + action = self.read_action() + match action: + case Action.QUIT | Action.HINT: + return (action, x, y) + case Action.OPEN | Action.FLAG | Action.MINE | Action.SAFE: + return (action, x, y) + case Action.UP: + y = y-1 if y > 0 else 0 + case Action.LEFT: + x = x-1 if x > 0 else 0 + case Action.DOWN: + y = y+1 if y < h-1 else y + case Action.RIGHT: + x = x+1 if x < w-1 else x + case Action.TOP: + y = 0 + case Action.BOTTOM: + y = h-1 + case Action.BEGIN: + x = 0 + case Action.END: + x = w-1 + return (Action.NOOP, x, y) diff --git a/src/miinaharava/tui/static.py b/src/miinaharava/tui/static.py new file mode 100644 index 0000000..50dbb44 --- /dev/null +++ b/src/miinaharava/tui/static.py @@ -0,0 +1,77 @@ +""" tui/static.py - Staattiset määritykset tui:ssa tarvittaville jutuille. """ +from enum import Enum +from dataclasses import dataclass + +from board import Tile + +class Action(Enum): + """ tominnot, joita voidaan saada palautusrvona """ + QUIT = 0 # Pelin lopetus + OPEN = 1 # Ruudun avaaminen + FLAG = 2 # Ruudun liputus + HINT = 3 # Anna vihjeet + AUTO = 4 # Pelaa automaattisesti + LEFT = 5 # Liikkumiset... + RIGHT = 6 + UP = 7 + DOWN = 8 + TOP = 9 + BOTTOM = 10 + BEGIN = 11 + END = 12 + NOOP = 13 # ei mitään - tarvitaan, ettei mätsää ansikoodeja esciin + MINE = 14 # merkkaa pommi + SAFE = 15 # merkkaa turvallinen + +# ActionKeys - Ohjelma vertaa syötteen alkua näihin ja palauttaa ekan +ActionKeys = { + "\033[A": Action.UP, "\033[D": Action.LEFT, + "\033[C": Action.RIGHT, '\033[B': Action.DOWN, "\033[5~": Action.TOP, + "\033[6~": Action.BOTTOM, "\033[7~": Action.BEGIN,"\033[8~": Action.END, + "\033[": Action.NOOP, "\033": Action.QUIT, "t": Action.SAFE, + "w": Action.UP, "a": Action.LEFT, "s": Action.DOWN, + "d": Action.RIGHT, " ": Action.OPEN, "\n": Action.OPEN, + "l": Action.QUIT, "?": Action.HINT, "b": Action.HINT, + "f": Action.FLAG, "q": Action.QUIT, "m": Action.MINE, + "\t": Action.FLAG, "9": Action.MINE, "0": Action.SAFE +} + +KEY_DESCRIPTIONS = """Näppäinasettelu: + + YLÖS, ALAS, VASEN, OIKEA, PGDN, PGUP, HOME, END, w, a, s, d + Kursorin liikuttaminen pelilaudalla + + ENTER, SPACE Avaa laatta + + f, TAB Vaihda laatan merkintää + m, 9 Merkitse miinaksi + t, 0 Merkitse turvalliseksi + + ?, b Vihje tekoälyltä + + l, q, ESC Pelin lopetus +""" + +@dataclass +class TileType: + """ ruututyyppien tallennusmuotojen kuvaus""" + text: str # Teksti + colors: [] # Lista (väri, tausta) pareja tekstin kaunistamiseen + + +TileTypes = { + Tile.BLANK: TileType( "[ ]", [(0x7,0), (0x7,0), (0x7,0)] ), + Tile.ONE: TileType( "[1]", [(0xA,0), (0xA,0), (0xA,0)] ), + Tile.TWO: TileType( "[2]", [(0xB,0), (0xB,0), (0xB,0)] ), + Tile.THREE: TileType( "[3]", [(0xD,0), (0xD,0), (0xD,0)] ), + Tile.FOUR: TileType( "[4]", [(0x9,0), (0x9,0), (0x9,0)] ), + Tile.FIVE: TileType( "[5]", [(0x9,0), (0x9,0), (0x9,0)] ), + Tile.SIX: TileType( "[6]", [(0x9,0), (0x9,0), (0x9,0)] ), + Tile.SEVEN: TileType( "[7]", [(0x9,0), (0x9,0), (0x9,0)] ), + Tile.EIGHT: TileType( "[8]", [(0x9,0), (0x9,0), (0x9,0)] ), + Tile.MINE: TileType( "[@]", [(0xF,1), (0xF,1), (0xF,1)] ), + Tile.FLAG_MINE: TileType( "[×]", [(0x8,7), (0x1,7), (0x8,7)] ), + Tile.FLAG_FREE: TileType( "[•]", [(0x8,7), (0x2,7), (0x8,7)] ), + Tile.UNOPENED: TileType( "[#]", [(0x8,7), (0x8,7), (0x8,7)] ), + Tile.FLAG_UNKNOWN: TileType( "[?]", [(0x8,7), (0x0,7), (0x8,7)] ) +} diff --git a/src/miinaharava/tui/tui.py b/src/miinaharava/tui/tui.py new file mode 100644 index 0000000..d7f7fb3 --- /dev/null +++ b/src/miinaharava/tui/tui.py @@ -0,0 +1,109 @@ +""" tui/tui.py - runko käyttöliittymälle """ +import time +from .static import Action +from .kbd import Kbd, NoKbd +from .ansi_draw import AnsiDraw, SuppressDraw + + +class Tui(): + """ Tui - Luokka käyttäjän interaktiota varten """ + # pylint: disable = too-many-arguments, too-many-instance-attributes + def __init__(self, + bot = None, + autoplay = False, + interactive = True, + suppress = False, + height = 9, + level_name = "outo lauta", + delay = 0): + + # jos ei ole bottia pitää olla interaktiivinen + if bot is None: + autoplay = False + interactive = True + suppress = False + + # jos ei mitään näytetä ei voi olla interaktiivinen + if suppress: + interactive = False + + # automaattipeli pitää olla päällä jos ei interaktiivinen + if not interactive: + autoplay = True + + if delay and delay not in range(0,500): + delay = 50 + + self.autoplay = autoplay + self.interactive = interactive + self.suppress = suppress + self.height = height + self.level_name = level_name + self.delay = delay + + self.bot = bot(uncertain=not self.interactive) if bot else None + + self.kbd = Kbd() if self.interactive else NoKbd() + + if self.suppress: + self.draw = SuppressDraw() + else: + self.draw = AnsiDraw(height=self.height, name=self.level_name) + + def matrix_selector(self, matrix, x, y): + """ valinta matriisita """ + + # automaattipeli avaa botin vinkit heti + if self.autoplay: + action, x, y = self.bot.hint(matrix, x, y) + if action != Action.NOOP: + if self.delay: + self.draw.matrix(matrix, x, y) + time.sleep(self.delay/100) + return Action.OPEN if action==Action.SAFE else action, x, y + + + # ilman näppiskäsittelijää voidaan lopettaa + if not self.interactive: + return Action.QUIT, 0, 0 + + w, h = len(matrix), len(matrix[0]) + while True: + self.draw.matrix(matrix, x, y) + action, x, y = self.kbd.read_matrix_action(w, h, x, y) + match action: + case Action.QUIT: + return (action, x, y) + case Action.OPEN | Action.FLAG | Action.MINE | Action.SAFE: + if matrix[x][y] >= 10: + return (action, x, y) + case Action.HINT: + if self.bot is not None: + return self.bot.hint(matrix, x, y) + + def game_over(self, matrix, x, y): + """ tehtävät kun kuolee """ + self.draw.matrix(matrix, x, y) + self.draw.status_line( + f"{self.level_name}: " + + ("K " if self.suppress else f"{'Kuolit!':<30}") + ) + self.kbd.read_action() + + def game_win(self, matrix, x, y): + """ tehtävät kun voittaa """ + self.draw.matrix(matrix, x, y) + self.draw.status_line( + f"{self.level_name}: " + + ("V " if self.suppress else f"{'Voitit!':<30}") + ) + self.kbd.read_action() + + def game_end(self, matrix): + """ tehtävät ihan pelin lopuksi """ + if self.interactive: + self.draw.matrix(matrix, -1, -1) + self.draw.status_line( + f"{self.level_name}: " + + f"{'Kiitos!':<30}" + ) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_app.py b/tests/test_app.py deleted file mode 100644 index 1d13a71..0000000 --- a/tests/test_app.py +++ /dev/null @@ -1,241 +0,0 @@ -"""test_app.py - Testaa pelin suoritusta""" -# pylint: disable = missing-class-docstring, too-few-public-methods - -from io import StringIO -import unittest -from unittest.mock import patch - -from app import App - -from tui import Action - - -class KbdTest: - # pylint: disable = unused-argument, missing-function-docstring - def __init__(self, actions): - self.actions = actions - def read_action(self): - if self.actions: - action, _, _ = self.actions.pop(0) - else: - action = Action.NOOP - return action - def read_matrix_action(self, w, h, x, y): - return self.actions.pop(0) if self.actions else (Action.NOOP,x,y) - - -class TestAppClass(unittest.TestCase): - """ Testit itse appille """ - class DefaultArgs: - autoplay = 2 - intermediate = None - expert = None - board = None - mines = None - size = None - bot = None - quiet = None - delay = None - - sure_win_board = [ - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,1,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0] - ] - - sure_lose_board = [ - [1,1,1,1,1,1,1,1,1], - [1,1,1,1,1,1,1,1,1], - [1,1,1,1,1,1,1,1,1], - [1,1,1,1,1,1,1,1,1], - [1,1,1,1,0,1,1,1,1], - [1,1,1,1,1,1,1,1,1], - [1,1,1,1,1,1,1,1,1], - [1,1,1,1,1,1,1,1,1], - [1,1,1,1,1,1,1,1,1] - ] - - dssp_win_board = [ - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,0,0,0,0,0,0,0,0], - [0,1,1,0,1,1,0,1,0] - ] - - mini_board = [ - [0,0], - [0,1] - ] - - - def test_run(self): - """ Testataan että edes pyörähtää """ - app = App(self.DefaultArgs) - app.run() - del app - - def test_quit(self): - """ Testataan Quittaamista """ - app = App() - app.ui.kbd=KbdTest([ - (Action.QUIT,0,0), - (Action.OPEN,0,0) - ]) - app.run() - del app - - def test_many_games(self): - """ Varman voiton lauta palauttaa true """ - class Args(self.DefaultArgs): - quiet = True - for _ in range(50): - app = App(Args) - app.run() - del app - Args.intermediate = True - for _ in range(20): - app = App(Args) - app.run() - del app - Args.expert = True - for _ in range(10): - app = App(Args) - app.run() - del app - - def test_sure_win(self): - """ Varman voiton lauta palauttaa true """ - class Args(self.DefaultArgs): - board = self.sure_win_board - quiet = True - app = App(Args) - self.assertTrue(app.run()) - del app - - def test_dssp_win(self): - """ Varman voiton lauta palauttaa true """ - class Args(self.DefaultArgs): - board = self.dssp_win_board - app = App(Args) - self.assertTrue(app.run()) - del app - - def test_no_dssp_win_with_simple(self): - """ Varman voiton lauta palauttaa true """ - class Args(self.DefaultArgs): - board = self.dssp_win_board - quiet = True - bot = 1 - while True: - app = App(Args) - if not app.run(): - break - del app - - def test_sure_lose(self): - """ Varman häviön lauta palauttaa false """ - class Args(self.DefaultArgs): - board = self.sure_lose_board - app = App(Args) - self.assertFalse(app.run()) - del app - - def test_custom_size(self): - """ Varman häviön lauta palauttaa false """ - class Args(self.DefaultArgs): - size = (4, 4) - with patch('sys.stdout', new = StringIO()) as captured: - app = App(Args) - app.run() - self.assertIn("Mukautettu (4x4", captured.getvalue()) - del app - - def test_sure_win_with_actions(self): - """ Varman voiton lauta palauttaa true """ - class Args(self.DefaultArgs): - board = self.sure_win_board - autoplay = 0 - bot = 0 - app = App(Args) - app.ui.kbd=KbdTest([ - (Action.SAFE,0,0), - (Action.OPEN,0,0) - ]) - self.assertTrue(app.run()) - del app - - def test_sure_lose_with_actions(self): - """ Varman voiton lauta palauttaa true """ - class Args(self.DefaultArgs): - board = self.sure_lose_board - autoplay = 0 - app = App(Args) - app.ui.kbd=KbdTest([ - (Action.FLAG,0,0), - (Action.MINE,0,0), - (Action.OPEN,0,0) - ]) - self.assertFalse(app.run()) - del app - - def test_auto_play_hints(self): - """ Vihjeiden automaattipelaaminen toimii """ - class Args(self.DefaultArgs): - board = self.dssp_win_board - autoplay = 1 - app = App(Args) - app.ui.kbd=KbdTest([ - (Action.OPEN,0,0), - (Action.HINT,0,0), - ]) - self.assertTrue(app.run()) - del app - - def test_delay(self): - """ Hidastus toimii """ - class Args(self.DefaultArgs): - board = self.dssp_win_board - delay = 5 - app = App(Args) - with patch('time.sleep') as patched_sleep: - self.assertTrue(app.run()) - del app - patched_sleep.assert_called() - - def test_delay_can_be_off(self): - """ Hidastus ei ole aina päälle """ - class Args(self.DefaultArgs): - board = self.dssp_win_board - app = App(Args) - with patch('time.sleep') as patched_sleep: - self.assertTrue(app.run()) - del app - patched_sleep.assert_not_called() - - def test_botless_play(self): - """ Hidastus toimii """ - class Args(self.DefaultArgs): - board = self.mini_board - autoplay = 0 - delay = 50000 - app = App(Args) - app.ui.kbd=KbdTest([ - (Action.OPEN,0,0), - (Action.HINT,0,0), - (Action.OPEN,1,0), - (Action.HINT,0,0), - (Action.OPEN,0,1) - ]) - self.assertTrue(app.run()) - del app diff --git a/tests/test_board.py b/tests/test_board.py deleted file mode 100644 index 37a2d54..0000000 --- a/tests/test_board.py +++ /dev/null @@ -1,295 +0,0 @@ -""" tests/test_board.py - Testit pelilaudalle """ - -import unittest -from board import Board, Level, LevelSpecs - - -def matrix_equals(m1, m2): - """ matrix_equals - apufunktio testaa onko matriisit samat """ - if len(m1)!=len(m2): - return False - # pylint: disable = consider-using-enumerate - for i in range(len(m1)): - if m1[i] != m2[i]: - return False - return True - -def matrix_swap_xy(m): - """ matrix_swap_xy - palauttaa matriisin korvattu x -> y ja y -> x """ - if not m: - return None - w, h = len(m[0]), len(m) - ret_m = [[0 for _ in range(h)] for _ in range(w)] - for y in range(h): - for x in range(w): - ret_m[x][y]=m[y][x] - return ret_m - - -class TestBoardClass(unittest.TestCase): - """ pelilauden testit kattava luokka """ - - def test_init_works_and_defaults_beginner(self): - """ pelilautaolion luominen onnistuu ja defaulttaa aloittelijaksi """ - b = Board() - self.assertEqual(b.get_width(), LevelSpecs[Level.BEGINNER][0]) - self.assertEqual(b.get_height(), LevelSpecs[Level.BEGINNER][1]) - self.assertEqual(b.get_mines(), LevelSpecs[Level.BEGINNER][2]) - self.assertTrue(b.get_width()>0) - - - def test_init_with_level(self): - """ olion luominen onnistuu vaikeustasolla""" - b = Board(level=Level.EXPERT) - self.assertEqual(b.get_width(), LevelSpecs[Level.EXPERT][0]) - self.assertEqual(b.get_height(), LevelSpecs[Level.EXPERT][1]) - self.assertEqual(b.get_mines(), LevelSpecs[Level.EXPERT][2]) - - - def test_init_with_custom_dimentions(self): - """ mukautetun kentän luominen onnistuu """ - b = Board(width=13, height=14, mines=15) - self.assertEqual(b.get_width(), 13) - self.assertEqual(b.get_height(), 14) - self.assertEqual(b.get_mines(), 15) - b = Board(width=22, height=12) - self.assertEqual(b.get_width(), 22) - self.assertEqual(b.get_height(), 12) - self.assertEqual(b.get_mines(), 22) - - - def test_init_with_incorect_dimensions(self): - """ luominen ei saa onnitua mahdottomilla mitoilla """ - b = Board(width=1, height=999, mines=0) - self.assertEqual(b.get_width(), LevelSpecs[Level.BEGINNER][0]) - self.assertEqual(b.get_height(), LevelSpecs[Level.BEGINNER][1]) - self.assertEqual(b.get_mines(), LevelSpecs[Level.BEGINNER][2]) - - - def test_init_with_valid_board(self): - """ Pelilaudan luominen onnistuu kelvollisella asettelulla """ - t = [ - [0,0,0,0], - [0,0,0,1], - [0,0,0,0] - ] - b = Board(board = t) - self.assertEqual(b.get_width(), 4) - self.assertEqual(b.get_height(), 3) - self.assertEqual(b.get_mines(), 1) - - - def test_init_with_invalid_board(self): - """ Yritetään luoda peli kelvottomalla laudalla """ - t = [ - [0,0,0,0,0], - [0,0,0,1], - [0,0,0,0,0] - ] - b = Board(board = t) - # Resetoituihan aloittelijan lauta - self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) - - t = [ - [0,1,0,0,0], - ] - b = Board(board = t) - self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) - - t = [ - [0,0,0,0], - [0,0,0,0], - [0,0,0,0] - ] - b = Board(board = t) - self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) - - t = [ - [1,1,1], - [1,1,1], - [1,1,1] - ] - b = Board(board = t) - self.assertIn(LevelSpecs[Level.BEGINNER][3], b.get_level_name()) - - - def test_tiles_and_masks_ok(self): - """ Luohan luokka sisäiset laatat ja maskit oikein """ - # pylint: disable = protected-access - t = [ - [0,0,0,0], - [0,0,0,1], - [0,0,0,0] - ] - b = Board(board = t) - self.assertEqual(b.get_width(), 4) - self.assertEqual(b.get_height(), 3) - self.assertEqual(b.get_mines(), 1) - - # testataan onko laatat tallennettu oikein luokkaan - t = matrix_swap_xy([ - [0,0,1,1], - [0,0,1,9], - [0,0,1,1] - ]) - self.assertTrue(matrix_equals(b._Board__tiles, t)) - - # onko maksit asetettu oikein - t = matrix_swap_xy([ - [12,12,12,12], - [12,12,12,12], - [12,12,12,12] - ]) - self.assertTrue(matrix_equals(b._Board__masked, t)) - - - def test_get_view_and_guess(self): - """ laudan näkymä on oikein senkin jälkeen kun on arvattu """ - - t = [ - [0,0,1], - [0,0,0], - [0,0,0] - ] - b = Board(board=t) - - # Antaahan pelikenttä pelkkää maskia aluksi - t = matrix_swap_xy([ - [12,12,12], - [12,12,12], - [12,12,12] - ]) - self.assertTrue(matrix_equals(b.get_view(), t)) - - # avataan yläkulma -> palatuu True - self.assertTrue(b.guess(0,0)) - - # onko näkymä nyt oikein - t = matrix_swap_xy([ - [0,1,12], - [0,1,1], - [0,0,0] - ]) - self.assertTrue(matrix_equals(b.get_view(), t)) - - # avataan alakulma jossa miina -> palautuu False - self.assertFalse(b.guess(2,0)) - - - def test_is_winning(self): - """ toimiiko voittotilanteen tunnistus """ - - t = [ - [0,1], - [0,0], - ] - b = Board(board=t) - self.assertFalse(b.is_winning()) - - # Avataan ruutu jolla ei tule viellä voittoa - t = matrix_swap_xy([ - [1,12], - [12,12] - ]) - self.assertTrue(b.guess(0,0)) - self.assertTrue(matrix_equals(b.get_view(), t)) - self.assertFalse(b.is_winning()) - - # Avataan loputkin ruudut, jolloin pitäisi voittaa - t = matrix_swap_xy([ - [1,12], - [1,1] - ]) - self.assertTrue(b.guess(0,1)) - self.assertTrue(b.guess(1,1)) - self.assertTrue(matrix_equals(b.get_view(), t)) - self.assertTrue(b.is_winning()) - - # Lupuksi avataan miina jolloin voittoa ei enää pitäisi olla - t = matrix_swap_xy([ - [1,9], - [1,1] - ]) - self.assertFalse(b.guess(1,0)) - self.assertTrue(matrix_equals(b.get_view(), t)) - self.assertFalse(b.is_winning()) - - - def test_error_conditions_in_guess(self): - """ ruudun avaus alueen ulkopuolelta tai avatussa ruudussa ei onnistu""" - t = [ - [0,1], - [0,0], - ] - b = Board(board=t) - self.assertFalse(b.guess(2,2)) - self.assertTrue(b.guess(0,0)) - self.assertFalse(b.guess(0,0)) - - - def test_get_mask(self): - """ maski annetaan oikein """ - t = [ - [1,0], - [0,0], - ] - b = Board(board=t) - self.assertEqual(b.get_mask(1,0), 12) - self.assertTrue(b.guess(1,0)) - self.assertFalse(b.get_mask(1,0)) - - - def test_flag(self): - """ ruudun lipun vaihto ja asetus toimii """ - t = [ - [0,0], - [0,1], - ] - b = Board(board=t) - self.assertEqual(b.get_mask(0,0), 12) - self.assertTrue(b.flag(0,0)) - self.assertEqual(b.get_mask(0,0), 13) - self.assertTrue(b.flag(0,0)) - self.assertEqual(b.get_mask(0,0), 10) - self.assertTrue(b.flag(0,0)) - self.assertEqual(b.get_mask(0,0), 11) - self.assertTrue(b.flag(0,0)) - self.assertEqual(b.get_mask(0,0), 12) - self.assertTrue(b.flag(0,0,10)) - self.assertEqual(b.get_mask(0,0), 10) - - - def test_flag_error_conditions(self): - """ liputus ei onnistu jos avattu, alueen ulkopuolella, outo arvo """ - t = [ - [0,1], - [1,1], - ] - b = Board(board=t) - self.assertFalse(b.flag(0,0,6)) # Lippu jota ei ole - self.assertFalse(b.flag(2,2)) # Alueen ulkopuolella - self.assertTrue(b.guess(0,0)) - self.assertFalse(b.flag(0,0)) # Avattu laatta - - - def test_reveal(self): - """ paljastuksen jälkeen näkyy laatat sellaisenaan """ - t = [ - [0,1], - [1,1], - ] - b = Board(board=t) - b.reveal() - t = matrix_swap_xy([ - [3,9], - [9,9] - ]) - self.assertTrue(matrix_equals(b.get_view(), t)) - - - def test_get_level_name(self): - """ Testataan että nykyinen vaikeustaso palautuu oikein """ - b = Board(level=Level.INTERMEDIATE) - self.assertIn(LevelSpecs[Level.INTERMEDIATE][3], b.get_level_name()) - b = Board(level=Level.INTERMEDIATE, width=25, mines=2) - self.assertIn("Mukautettu", b.get_level_name()) diff --git a/tests/test_bot.py b/tests/test_bot.py deleted file mode 100644 index 4dab148..0000000 --- a/tests/test_bot.py +++ /dev/null @@ -1,54 +0,0 @@ -""" tests/test_bot.py - Testaa botin toimintaa""" -# pylint: disable = missing-class-docstring, too-few-public-methods, protected-access - -import unittest - -from board import Board, Tile -from bots import DSSPBot, SimpleBot -from tui import Action - -class TestBotClass(unittest.TestCase): - """ botin testit""" - def test_init(self): - """ olioden luominen onnistuu """ - DSSPBot() - SimpleBot() - - def correctly_marking(self, open_free=False, bot_type=DSSPBot): - """ Testaa onko miinat miinoja ja vapaat vapaita alkuun avatusta """ - for _ in range(500): - brd = Board() - # jos ei aukea ylälaidasta otetaan seuraava - if not brd.guess(0,0): - continue - # vain varmat liikut - bot = bot_type(uncertain=False) - - tested = set() - while True: - action, x, y = bot.hint(brd.get_view(), 0, 0) - if (x,y) in tested: - break - tested.add((x,y)) - if action == Action.SAFE: - self.assertTrue( brd._Board__tiles[x][y] < Tile.MINE ) - if open_free: - brd.guess(x,y) - if action == Action.MINE: - self.assertTrue( brd._Board__tiles[x][y] == Tile.MINE ) - - def test_dssp_marks_correctly_with_open(self): - """ Testaa onko dssp:n miinat miinoja ja avaa vapaat """ - self.correctly_marking(True, DSSPBot) - - def test_simple_marks_correctly_with_open(self): - """ Testaa onko dssp:n miinat miinoja ja avaa vapaat """ - self.correctly_marking(True, SimpleBot) - - def test_dssp_marks_correctly(self): - """ Testaa onko dssp:n miinat miinoja ja vapaat vapaita """ - self.correctly_marking(False, DSSPBot) - - def test_simple_marks_correctly(self): - """ Testaa onko simple:n miinat miinoja ja vapaat vapaita """ - self.correctly_marking(False, SimpleBot) diff --git a/tui/__init__.py b/tui/__init__.py deleted file mode 100644 index 0c8d632..0000000 --- a/tui/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" tui - hoitaa käyttäjälle katseltavaa ja havaitsee syötteet """ -from .tui import Tui -from .static import Action, KEY_DESCRIPTIONS diff --git a/tui/ansi.py b/tui/ansi.py deleted file mode 100644 index c25ff6c..0000000 --- a/tui/ansi.py +++ /dev/null @@ -1,46 +0,0 @@ -""" ansi.py - ansi ohjauskomentoja. värit jne """ - -class Ansi: - """ Ansi - Luokallinen staattisia metodeja ansi komennoille """ - - BLACK = 0 - RED = 1 - GREEN = 2 - YELLOW = 3 - BLUE = 4 - MAGENTA = 5 - CYAN = 6 - WHITE = 7 - GRAY = 8 - BRIGHT_RED = 9 - BRIGHT_GREEN = 0xA - BRIGHT_YELLOW = 0xB - BRIGHT_BLUE = 0xC - BRIGHT_MAGENTA = 0xD - BRIGHT_CYAN = 0xE - BRIGHT_WHITE = 0xF - - @staticmethod - def color(color): - """ asettaa tekstin värin """ - if color in range(16): - print(end=f"\033[{'1;' if color//8 else ''}3{color%8}m") - - - @staticmethod - def bg(color): - """ asettaa tekstin taustan värin""" - if color in range(8): - print(end=f"\033[4{color}m") - - - @staticmethod - def cup(lines): - """ liikuttaa kursoria ylöspäin""" - print(end=f"\033[{lines}F") - - - @staticmethod - def reset(): - """ resetoi tekstin värin ja muut attribuutit perusarvoille """ - print(end="\033[0m") diff --git a/tui/ansi_draw.py b/tui/ansi_draw.py deleted file mode 100644 index ba71fdb..0000000 --- a/tui/ansi_draw.py +++ /dev/null @@ -1,58 +0,0 @@ -""" tui/ansi_draw.py - perustukset ansi tulostelulle """ -# pylint: disable = multiple-imports -from .ansi import Ansi -from .static import TileTypes - -class AnsiDraw(): - """ AnsiDraw - "piirtelee" näytölle kirjailmilla """ - def __init__(self, height = 9, name = ""): - print(end='\n'*height+name+": Peli alkaa.") - - def __del__(self): - print() - - def __tile(self, tile, hilighted): - """ "piirtää" yhden ruudun """ - for ch, colors in zip(TileTypes[tile].text, TileTypes[tile].colors): - color, bg = colors - Ansi.color(Ansi.BLACK if hilighted else color) - Ansi.bg(Ansi.CYAN if hilighted else bg) - print(end=ch) - Ansi.reset() - - - def matrix(self, matrix, hx, hy): - """ "piirtää" ruudukon """ - Ansi.cup(len(matrix[0])) - # pylint: disable=consider-using-enumerate - for y in range(len(matrix[0])): - for x in range(len(matrix)): - hilight = matrix[x][y] != 9 and x == hx and y == hy - self.__tile(matrix[x][y], hilight) - print() - - - def status_line(self, text): - """ draw_status_line - tulostaa pelitietorivin""" - print(end=text+'\r') - -class SuppressDraw(): - """ SuppressDraw - vain status """ - # pylint: disable = unused-argument - - def matrix(self, matrix, hx, hy): - """ "piirtää" ruudukon """ - - def status_line(self, text): - """ draw_status_line - tulostaa pelitietorivin""" - print(end=text+'\r') - -class NoDraw(): - """ NoDraw - ei mitään """ - # pylint: disable = unused-argument - - def matrix(self, matrix, hx, hy): - """ "piirtää" ruudukon """ - - def status_line(self, text): - """ draw_status_line - tulostaa pelitietorivin""" diff --git a/tui/kbd.py b/tui/kbd.py deleted file mode 100644 index a31e56f..0000000 --- a/tui/kbd.py +++ /dev/null @@ -1,86 +0,0 @@ -""" tui/kbd.py - näppäimistön käsittellijä """ -# pylint: disable = multiple-imports -import termios, fcntl, sys, os, io -from time import sleep -from .static import ActionKeys, Action - -class NoKbd(): - """ NoKbd - näppis-ei-käsittelijä """ - # pylint: disable = unused-argument - def read_action(self): - """ read_action - ilman näppistä -> loppu """ - return Action.QUIT - - def read_matrix_action(self, w, h, x, y): - """ read_matrix_action - ilman näppistä -> loppu """ - return Action.QUIT, 0, 0 - -class Kbd(): - """ Kbd - näppiskäsittelijä """ - def __init__(self): - # Vaatii hieman terminaaliasetusten muokkaamista jotta yksittäiset - # napin painallukset voidaan lukea - # https://stackoverflow.com/questions/983354/how-do-i-wait-for-a-pressed-key - try: - fd = sys.stdin.fileno() - self.oldterm = termios.tcgetattr(fd) - - newattr = termios.tcgetattr(fd) - newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, newattr) - - self.oldflags = fcntl.fcntl(fd, fcntl.F_GETFL) - fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK) - # Testeissä ei voi mukata termilaalia - except io.UnsupportedOperation: - pass - - def __del__(self): - # palautetaan terminaali takaisin alkupetäiseen uskoon - try: - fd = sys.stdin.fileno() - termios.tcsetattr(fd, termios.TCSAFLUSH, self.oldterm) - fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags) - # Testeissä ei voi mukata termilaalia - except io.UnsupportedOperation: - pass - - def read_action(self): - """ lukee näppäimistölä käyttäjän toiminnon """ - while True: - # Ehkä riittää jos näppäimiä luetaan 50x sekunnissa - sleep(0.02) - try: - keycode = sys.stdin.read(16) - except KeyboardInterrupt: - return Action.QUIT - if keycode: - for key, action in ActionKeys.items(): - if keycode.startswith(key): - return action - - def read_matrix_action(self, w, h, x, y): - """ read_matrix_action - lukee actionit ja pitää huolen koordinaat""" - action = self.read_action() - match action: - case Action.QUIT | Action.HINT: - return (action, x, y) - case Action.OPEN | Action.FLAG | Action.MINE | Action.SAFE: - return (action, x, y) - case Action.UP: - y = y-1 if y > 0 else 0 - case Action.LEFT: - x = x-1 if x > 0 else 0 - case Action.DOWN: - y = y+1 if y < h-1 else y - case Action.RIGHT: - x = x+1 if x < w-1 else x - case Action.TOP: - y = 0 - case Action.BOTTOM: - y = h-1 - case Action.BEGIN: - x = 0 - case Action.END: - x = w-1 - return (Action.NOOP, x, y) diff --git a/tui/static.py b/tui/static.py deleted file mode 100644 index 60442c0..0000000 --- a/tui/static.py +++ /dev/null @@ -1,76 +0,0 @@ -""" tui/static.py - Staattiset määritykset tui:ssa tarvittaville jutuille. """ -from enum import Enum -from dataclasses import dataclass -from board import Tile - -class Action(Enum): - """ tominnot, joita voidaan saada palautusrvona """ - QUIT = 0 # Pelin lopetus - OPEN = 1 # Ruudun avaaminen - FLAG = 2 # Ruudun liputus - HINT = 3 # Anna vihjeet - AUTO = 4 # Pelaa automaattisesti - LEFT = 5 # Liikkumiset... - RIGHT = 6 - UP = 7 - DOWN = 8 - TOP = 9 - BOTTOM = 10 - BEGIN = 11 - END = 12 - NOOP = 13 # ei mitään - tarvitaan, ettei mätsää ansikoodeja esciin - MINE = 14 # merkkaa pommi - SAFE = 15 # merkkaa turvallinen - -# ActionKeys - Ohjelma vertaa syötteen alkua näihin ja palauttaa ekan -ActionKeys = { - "\033[A": Action.UP, "\033[D": Action.LEFT, - "\033[C": Action.RIGHT, '\033[B': Action.DOWN, "\033[5~": Action.TOP, - "\033[6~": Action.BOTTOM, "\033[7~": Action.BEGIN,"\033[8~": Action.END, - "\033[": Action.NOOP, "\033": Action.QUIT, "t": Action.SAFE, - "w": Action.UP, "a": Action.LEFT, "s": Action.DOWN, - "d": Action.RIGHT, " ": Action.OPEN, "\n": Action.OPEN, - "l": Action.QUIT, "?": Action.HINT, "b": Action.HINT, - "f": Action.FLAG, "q": Action.QUIT, "m": Action.MINE, - "\t": Action.FLAG, "9": Action.MINE, "0": Action.SAFE -} - -KEY_DESCRIPTIONS = """Näppäinasettelu: - - YLÖS, ALAS, VASEN, OIKEA, PGDN, PGUP, HOME, END, w, a, s, d - Kursorin liikuttaminen pelilaudalla - - ENTER, SPACE Avaa laatta - - f, TAB Vaihda laatan merkintää - m, 9 Merkitse miinaksi - t, 0 Merkitse turvalliseksi - - ?, b Vihje tekoälyltä - - l, q, ESC Pelin lopetus -""" - -@dataclass -class TileType: - """ ruututyyppien tallennusmuotojen kuvaus""" - text: str # Teksti - colors: [] # Lista (väri, tausta) pareja tekstin kaunistamiseen - - -TileTypes = { - Tile.BLANK: TileType( "[ ]", [(0x7,0), (0x7,0), (0x7,0)] ), - Tile.ONE: TileType( "[1]", [(0xA,0), (0xA,0), (0xA,0)] ), - Tile.TWO: TileType( "[2]", [(0xB,0), (0xB,0), (0xB,0)] ), - Tile.THREE: TileType( "[3]", [(0xD,0), (0xD,0), (0xD,0)] ), - Tile.FOUR: TileType( "[4]", [(0x9,0), (0x9,0), (0x9,0)] ), - Tile.FIVE: TileType( "[5]", [(0x9,0), (0x9,0), (0x9,0)] ), - Tile.SIX: TileType( "[6]", [(0x9,0), (0x9,0), (0x9,0)] ), - Tile.SEVEN: TileType( "[7]", [(0x9,0), (0x9,0), (0x9,0)] ), - Tile.EIGHT: TileType( "[8]", [(0x9,0), (0x9,0), (0x9,0)] ), - Tile.MINE: TileType( "[@]", [(0xF,1), (0xF,1), (0xF,1)] ), - Tile.FLAG_MINE: TileType( "[×]", [(0x8,7), (0x1,7), (0x8,7)] ), - Tile.FLAG_FREE: TileType( "[•]", [(0x8,7), (0x2,7), (0x8,7)] ), - Tile.UNOPENED: TileType( "[#]", [(0x8,7), (0x8,7), (0x8,7)] ), - Tile.FLAG_UNKNOWN: TileType( "[?]", [(0x8,7), (0x0,7), (0x8,7)] ) -} diff --git a/tui/tui.py b/tui/tui.py deleted file mode 100644 index d7f7fb3..0000000 --- a/tui/tui.py +++ /dev/null @@ -1,109 +0,0 @@ -""" tui/tui.py - runko käyttöliittymälle """ -import time -from .static import Action -from .kbd import Kbd, NoKbd -from .ansi_draw import AnsiDraw, SuppressDraw - - -class Tui(): - """ Tui - Luokka käyttäjän interaktiota varten """ - # pylint: disable = too-many-arguments, too-many-instance-attributes - def __init__(self, - bot = None, - autoplay = False, - interactive = True, - suppress = False, - height = 9, - level_name = "outo lauta", - delay = 0): - - # jos ei ole bottia pitää olla interaktiivinen - if bot is None: - autoplay = False - interactive = True - suppress = False - - # jos ei mitään näytetä ei voi olla interaktiivinen - if suppress: - interactive = False - - # automaattipeli pitää olla päällä jos ei interaktiivinen - if not interactive: - autoplay = True - - if delay and delay not in range(0,500): - delay = 50 - - self.autoplay = autoplay - self.interactive = interactive - self.suppress = suppress - self.height = height - self.level_name = level_name - self.delay = delay - - self.bot = bot(uncertain=not self.interactive) if bot else None - - self.kbd = Kbd() if self.interactive else NoKbd() - - if self.suppress: - self.draw = SuppressDraw() - else: - self.draw = AnsiDraw(height=self.height, name=self.level_name) - - def matrix_selector(self, matrix, x, y): - """ valinta matriisita """ - - # automaattipeli avaa botin vinkit heti - if self.autoplay: - action, x, y = self.bot.hint(matrix, x, y) - if action != Action.NOOP: - if self.delay: - self.draw.matrix(matrix, x, y) - time.sleep(self.delay/100) - return Action.OPEN if action==Action.SAFE else action, x, y - - - # ilman näppiskäsittelijää voidaan lopettaa - if not self.interactive: - return Action.QUIT, 0, 0 - - w, h = len(matrix), len(matrix[0]) - while True: - self.draw.matrix(matrix, x, y) - action, x, y = self.kbd.read_matrix_action(w, h, x, y) - match action: - case Action.QUIT: - return (action, x, y) - case Action.OPEN | Action.FLAG | Action.MINE | Action.SAFE: - if matrix[x][y] >= 10: - return (action, x, y) - case Action.HINT: - if self.bot is not None: - return self.bot.hint(matrix, x, y) - - def game_over(self, matrix, x, y): - """ tehtävät kun kuolee """ - self.draw.matrix(matrix, x, y) - self.draw.status_line( - f"{self.level_name}: " + - ("K " if self.suppress else f"{'Kuolit!':<30}") - ) - self.kbd.read_action() - - def game_win(self, matrix, x, y): - """ tehtävät kun voittaa """ - self.draw.matrix(matrix, x, y) - self.draw.status_line( - f"{self.level_name}: " + - ("V " if self.suppress else f"{'Voitit!':<30}") - ) - self.kbd.read_action() - - def game_end(self, matrix): - """ tehtävät ihan pelin lopuksi """ - if self.interactive: - self.draw.matrix(matrix, -1, -1) - self.draw.status_line( - f"{self.level_name}: " + - f"{'Kiitos!':<30}" - ) -- cgit v1.2.3