diff options
author | Aineopintojen-harjoitustyo-Algoritmit-j <github-hy-tiralabra@v.hix.fi> | 2024-01-29 02:34:32 +0200 |
---|---|---|
committer | Aineopintojen-harjoitustyo-Algoritmit-j <github-hy-tiralabra@v.hix.fi> | 2024-01-29 02:34:32 +0200 |
commit | e591d15abc4943a74c39f0fcab065213828a1514 (patch) | |
tree | bc22931abb08d8c213ba4eb81a85298d69e88890 | |
parent | 0c034e6fbae0833f8524caf223deab450e9bb1b4 (diff) |
Updating too much. Renewed bots and tui.
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | __main__.py | 40 | ||||
-rw-r--r-- | app.py | 32 | ||||
-rw-r--r-- | batch/100-run-beginner-bad.sh | 9 | ||||
-rw-r--r-- | batch/1000-run-beginner-bad.sh | 9 | ||||
-rw-r--r-- | board/static.py | 2 | ||||
-rw-r--r-- | bots/__init__.py | 4 | ||||
-rw-r--r-- | bots/bad.py | 73 | ||||
-rw-r--r-- | bots/bot.py | 133 | ||||
-rw-r--r-- | bots/dssp.py | 80 | ||||
-rw-r--r-- | bots/idiot.py | 18 | ||||
-rw-r--r-- | bots/simple.py | 29 | ||||
-rw-r--r-- | doc/viikkoraportti3.pdf | bin | 0 -> 54141 bytes | |||
-rw-r--r-- | game/game.py | 5 | ||||
-rw-r--r-- | tui/__init__.py | 1 | ||||
-rw-r--r-- | tui/ansi_draw.py | 61 | ||||
-rw-r--r-- | tui/autotui.py | 27 | ||||
-rw-r--r-- | tui/kbd.py | 78 | ||||
-rw-r--r-- | tui/tui.py | 173 |
19 files changed, 493 insertions, 286 deletions
@@ -9,6 +9,7 @@ Miinaharava ratkaisijalla ### Viikkoraportit - [viikko 1](doc/viikkoraportti1.pdf) - [viikko 2](doc/viikkoraportti2.pdf) +- [viikko 3](doc/viikkoraportti3.pdf) ## Ohjeet: @@ -21,5 +22,5 @@ Miinaharava ratkaisijalla ### Käyttöohjeet `python3 miinaharava -h` -### Skripti joka pelaa automaattisesti 100 peliä aloittelijan vaikeustasolla -`bash miinaharava/batch/100-run-beginner-bad.sh` +### Automaattipelaa 10 peliä +`python3 miinaharava -w 10` diff --git a/__main__.py b/__main__.py index 627f724..0ac24de 100644 --- a/__main__.py +++ b/__main__.py @@ -23,20 +23,50 @@ parser.add_argument( action='store_true' ) parser.add_argument( + '-s', '--simple', + help='Käytä yksinkertaisempaa vain yhtä pistettä tutkivaa bottia', + action='store_true' +) +parser.add_argument( '-a', '--auto', help='Antaa botin pelata automaattisesti', action='store_true' ) parser.add_argument( '-u', '--uncertain', - help='Antaa botille luvan tehdä myös epävarmoja valintoja', + help='Antaa botille luvan tehdä myös epävarmoja valintoja (asettaa myös -a asetuksen)', + action='store_true' +) +parser.add_argument( + '-q', '--quiet', + help='Tulostaa minimaalisesti (asettaa myös -a ja -u asetukset)', action='store_true' ) +parser.add_argument( + '-w', + metavar='COUNT', + type=int, + help='Suorittaa ohelmaa COUNT kertaa ja tulostaa voitto-osuuden', +) args = parser.parse_args() -app = App(args) -is_win = app.run() -del app +if args.w 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 +args.uncertain=True +for i in range(args.w): + print(end=f" \rSuoritus {i+1:>6}/{args.w} ") + 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 -sys.exit(not is_win) # Exit koodeissa 0 on onnistunut suoritus +print(f"\n## Voittoja {win_count}/{args.w} ({100*win_count/args.w:.1f}%)") @@ -1,26 +1,40 @@ """ app.py - pääohjelma """ -from board import Board, Level -from tui import Tui, AutoTui +from board import Board, Level, LevelSpecs +from tui import Tui from game import Game -from bots import BadBot +from bots import SimpleBot, DSSPBot # pylint: disable = too-few-public-methods class App: """ App - Luokka pääohjelmalle""" def __init__(self, args=None): level = Level.BEGINNER - ui_class = Tui - uncertain = False + auto, uncertain, quiet = False, False, False if args: level = Level.EXPERT if args.expert else level level = Level.INTERMEDIATE if args.intermediate else level level = Level.BEGINNER if args.beginner else level - ui_class = AutoTui if args.auto else ui_class - uncertain = args.uncertain + width, height, bombs = LevelSpecs[level] + auto = args.auto + auto, uncertain = (True, True) if args.uncertain else (auto, False) + auto, uncertain, quiet = (True, True, True) \ + if args.quiet else (auto, uncertain, False) + self.bot = SimpleBot(uncertain=uncertain) if args.simple \ + else DSSPBot(uncertain=uncertain) + self.ui = Tui ( + bot=self.bot, + autoplay=auto, + interact=not uncertain, + suppress=quiet, + width=width, + height=height, + bombs=bombs + ) self.board = Board(level=level) - self.bot = BadBot(uncertain=uncertain) - self.ui = ui_class(self.bot) + self.bot = DSSPBot(uncertain=uncertain) if self.bot is None \ + else self.bot + self.ui = Tui(bot=self.bot) if self.ui is None else self.ui self.game = Game(self.board,self.ui) def run(self): diff --git a/batch/100-run-beginner-bad.sh b/batch/100-run-beginner-bad.sh deleted file mode 100644 index ab62e2e..0000000 --- a/batch/100-run-beginner-bad.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -OPTIONS="-b -a -u" -let COUNT=100 -let WINS=0 -for ((i=0;i<COUNT;i++)); do - python3 miinaharava $OPTIONS && let WINS++; -done -let PERCENT=100*WINS/COUNT -echo -ne "\n\n## Voittoja $WINS/$COUNT ($PERCENT%)\n\n" diff --git a/batch/1000-run-beginner-bad.sh b/batch/1000-run-beginner-bad.sh deleted file mode 100644 index 394aeb3..0000000 --- a/batch/1000-run-beginner-bad.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -OPTIONS="-b -a -u" -let COUNT=1000 -let WINS=0 -for ((i=0;i<COUNT;i++)); do - python3 miinaharava $OPTIONS && let WINS++; -done -let PERCENT=100*WINS/COUNT -echo -ne "\n\n## Voittoja $WINS/$COUNT ($PERCENT%)\n\n" diff --git a/board/static.py b/board/static.py index eea30fd..d6b48dd 100644 --- a/board/static.py +++ b/board/static.py @@ -29,6 +29,6 @@ class Tile(IntEnum): LevelSpecs = { Level.BEGINNER: ( 9, 9, 10 ), - Level.INTERMEDIATE: ( 15, 15, 40 ), + Level.INTERMEDIATE: ( 16, 16, 40 ), Level.EXPERT: ( 30, 16, 99 ) } diff --git a/bots/__init__.py b/bots/__init__.py index f180c3a..f346d07 100644 --- a/bots/__init__.py +++ b/bots/__init__.py @@ -1,4 +1,4 @@ """ bots - tämä alimoduli tarjoaa tekoälyn """ -from .bot import Bot -from .bad import BadBot +from .simple import SimpleBot +from .dssp import DSSPBot diff --git a/bots/bad.py b/bots/bad.py deleted file mode 100644 index 5197dc9..0000000 --- a/bots/bad.py +++ /dev/null @@ -1,73 +0,0 @@ -""" bots/bad.py - botti joka ehkä osaa merkata jonkun asian """ -from random import sample -from tui import Action -from .bot import Bot - -class BadBot(Bot): - """ IdiotBot - merkistsee kaikki turvallisiksi avata """ - # pylint: disable = too-few-public-methods - def missing_bombs(self, matrix, x, y): - """ test how many boms are not found at the coordinate """ - dx = len(matrix) - dy = len(matrix[0]) - bcount = 0 - for nx, ny in self.neighbours(dx, dy, x, y): - if matrix[nx][ny] in (9,11): - bcount+=1 - return matrix[x][y]-bcount - - def get_tiles_at_border(self, matrix): - """ get interesting tiles on the border of cleared and masked area """ - tiles = [] - w = len(matrix) - h = len(matrix[0]) - for y in range(h): - for x in range(w): - if matrix[x][y] < 12: - open_tiles=1 - masked_tiles=0 - else: - open_tiles=0 - masked_tiles=1 - for nx, ny in self.neighbours(w, h, x, y): - if matrix[nx][ny] < 12: - open_tiles+=1 - else: - masked_tiles+=1 - if open_tiles and masked_tiles: - tiles.append( (x,y) ) - return tiles - - def get_unopened_tiles(self, matrix): - """ get interesting tiles on the border of cleared and masked area """ - tiles = [] - w = len(matrix) - h = len(matrix[0]) - for y in range(h): - for x in range(w): - if matrix[x][y] == 12: - tiles.append( (x,y) ) - return tiles - - def hint(self, matrix, cursor_x, cursor_y): - """ merkitsee jonkin ruudun """ - super().hint(matrix, cursor_x, cursor_y) - w = len(matrix) - h = len(matrix[0]) - # pylint: disable = consider-using-enumerate - for x, y in self.get_tiles_at_border(matrix): - ncoords=self.neighbours(w,h,x,y) - ntiles=self.coordinates_to_tiles(matrix,ncoords) - unopened=ntiles.count(12) - bombs=ntiles.count(10) - if unopened: - if matrix[x][y]<9 and matrix[x][y]==bombs: - safe = ncoords[ntiles.index(12)] - return(Action.SAFE, safe[0], safe[1]) - if matrix[x][y]-bombs==unopened: - bomb = ncoords[ntiles.index(12)] - return(Action.BOMB, bomb[0], bomb[1]) - if self.uncertain: - x, y = sample(self.get_unopened_tiles(matrix),1)[0] - return (Action.OPEN, x, y) - return (Action.NOOP, cursor_x, cursor_y) diff --git a/bots/bot.py b/bots/bot.py index 5ad0bd2..49bd962 100644 --- a/bots/bot.py +++ b/bots/bot.py @@ -1,32 +1,133 @@ """ 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.hints = 0 + self.safe_tiles = set() + self.bomb_tiles = set() + self.matrix = [] + self.w = 0 + self.h = 0 - def neighbours(self,dx,dy,x,y): - """ palauttaa listana viereiset koordinaatit """ + 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.bomb_tiles: + x, y = self.bomb_tiles.pop() + return Action.BOMB, x, y + return Action.NOOP, 0, 0 + + def saved_hints(self): + """ onko muuveja varastossa """ + return self.safe_tiles or self.bomb_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), ) - coords=[] + tiles=set() for ox, oy in offsets: - if ox+x in range(dx): - if oy+y in range(dy): - coords.append((ox+x, oy+y)) - return coords + if ox+x in range(self.w): + if oy+y in range(self.h): + tiles.add((ox+x, oy+y)) + return tiles - def coordinates_to_tiles(self, matrix, coords): - """ lukee koordinaateissa olevien ruutujen arvot listaksi """ - return [matrix[x][y] for x,y in coords] + def get_value(self, tile): + """ palauttaa laatan arvon """ + return self.matrix[tile[0]][tile[1]] - def hint(self, matrix, cursor_x, cursor_y): - """ antaa vinkin. tässä tapauksessa ei mitään """ - # pylint: disable = unused-argument - self.hints += 1 - return Action.NOOP, cursor_x, cursor_y + 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_BOMB: + tiles.remove(tile) + + def remove_bomb_tiles(self, tiles): + """ poistaa pommit ja pommiksi merkityt """ + count=0 + for tile in list(tiles): + if self.matrix[tile[0]][tile[1]] in (Tile.BOMB, Tile.FLAG_BOMB): + 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.BOMB + + 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 remove_unknowns(self, tiles): + """ poistaa tunnistamattomat laatat """ + count=0 + for tile in list(tiles): + if not self.known_tile(tile): + tiles.remove(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_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 new file mode 100644 index 0000000..33578a0 --- /dev/null +++ b/bots/dssp.py @@ -0,0 +1,80 @@ +""" 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_bomb_tiles(n1) + c2 -= self.remove_bomb_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ää pommeja merkataan seiffeiks + # jos avaamattomia pommien määrä merkataan pommeiks + c2 -= cc + + if c2 == 0: + for safe in n2: + self.safe_tiles.add(safe) + if cc == len(nc) and c2 == len(n2): + for bomb in n2: + self.bomb_tiles.add(bomb) + + return self.saved_hints() + + def lucky_guess(self): + heatmap = dict.fromkeys(self.get_unknown_tiles(), 0) + tiles = self.get_interesting_tiles() + for tile in tiles: + n = self.get_neighbours(tile) + c = self.get_value(tile) - self.remove_bomb_tiles(n) + self.remove_number_tiles(n) + for tile in n: + heatmap[tile] = max( heatmap[tile], c/len(n) ) + + for tile, value in heatmap.items(): + if value>0: + continue + if tile[0] in range(1,self.w-1): + heatmap[tile]+=0.05 + if tile[1] in range(1,self.h-1): + heatmap[tile]+=0.05 + + 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/idiot.py b/bots/idiot.py deleted file mode 100644 index 933eee9..0000000 --- a/bots/idiot.py +++ /dev/null @@ -1,18 +0,0 @@ -""" bots/idiot.py - se ensimmäinen botti joka tekee kaiken väärin """ -from tui import Action -from .bot import Bot - -class IdiotBot(Bot): - """ IdiotBot - merkistsee kaikki turvallisiksi avata """ - # pylint: disable = too-few-public-methods - - def hint(self, matrix, cursor_x, cursor_y): - """ merkitsee jonkin ruudun """ - super().hint(matrix, cursor_x, cursor_y) - # pylint: disable = consider-using-enumerate - for ty in range(len(matrix[0])): - for tx in range(len(matrix)): - if matrix[tx][ty]==12: - return(Action.SAFE, tx, ty) - return (Action.NOOP, cursor_x, cursor_y) -
\ No newline at end of file diff --git a/bots/simple.py b/bots/simple.py new file mode 100644 index 0000000..cb3d25c --- /dev/null +++ b/bots/simple.py @@ -0,0 +1,29 @@ +""" 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_bomb_tiles(n) + if c == 0: + for safe in n: + self.safe_tiles.add(safe) + if c == len(n): + for bomb in n: + self.bomb_tiles.add(bomb) + 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/doc/viikkoraportti3.pdf b/doc/viikkoraportti3.pdf Binary files differnew file mode 100644 index 0000000..ea16a4d --- /dev/null +++ b/doc/viikkoraportti3.pdf diff --git a/game/game.py b/game/game.py index 76ee382..2a4efad 100644 --- a/game/game.py +++ b/game/game.py @@ -6,10 +6,7 @@ class Game: def __init__(self, board, ui): self.board = board self.ui = ui - self.x, self.y = self.ui.game_begin( - self.board.get_width(), - self.board.get_height() - ) + self.x, self.y = board.get_width()//2, board.get_height()//2 def __del__(self): diff --git a/tui/__init__.py b/tui/__init__.py index f39cd45..c1c5b91 100644 --- a/tui/__init__.py +++ b/tui/__init__.py @@ -1,4 +1,3 @@ """ tui - hoitaa käyttäjälle katseltavaa ja havaitsee syötteet """ from .tui import Tui -from .autotui import AutoTui from .static import Action diff --git a/tui/ansi_draw.py b/tui/ansi_draw.py new file mode 100644 index 0000000..dd304d0 --- /dev/null +++ b/tui/ansi_draw.py @@ -0,0 +1,61 @@ +""" 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 = 15): + print(end="\n"*height) + + 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 """ + return True + + 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 """ + return True + + def status_line(self, text): + """ draw_status_line - tulostaa pelitietorivin""" + return True diff --git a/tui/autotui.py b/tui/autotui.py deleted file mode 100644 index ae5d58c..0000000 --- a/tui/autotui.py +++ /dev/null @@ -1,27 +0,0 @@ -""" autotui - pelaa botin antamat vinkit jonka jälkeen käyttäjän """ -from .tui import Tui -from .static import Action -from .ansi import Ansi - -class AutoTui(Tui): - """ Tui - Luokka joka tekee botin vinkit ensin """ - def matrix_selector(self, matrix, x, y): - """ yritetään pyydellä botilta vinkkiä ensin """ - if self.bot is not None: - action, x, y = self.bot.hint(matrix, x, y) - if action != Action.NOOP: - self.draw_matrix(matrix, -1, -1) - if action==Action.SAFE: - action = Action.OPEN - return action, x, y - - return super().matrix_selector(matrix, x, y) - - def show_board_with_text(self, matrix, x, y, text): - """ näyttää laudan, tekstin alla (ei odota nappia) """ - self.draw_matrix(matrix, x, y) - print(text) - Ansi.cup(1) - - def game_end(self, matrix): - """ pelin lopetus """ diff --git a/tui/kbd.py b/tui/kbd.py new file mode 100644 index 0000000..b6d2363 --- /dev/null +++ b/tui/kbd.py @@ -0,0 +1,78 @@ +""" tui/kbd.py - näppäimistön käsittellijä """ +# pylint: disable = multiple-imports +import termios, fcntl, sys, os +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 + 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) + + def __del__(self): + # palautetaan terminaali takaisin alkupetäiseen uskoon + fd = sys.stdin.fileno() + termios.tcsetattr(fd, termios.TCSAFLUSH, self.oldterm) + fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags) + + 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.BOMB | 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) @@ -1,136 +1,89 @@ -""" tui/tui.py - teksikäyttöliittymä """ +""" tui/tui.py - runko käyttöliittymälle """ # pylint: disable = multiple-imports -import termios, fcntl, sys, os -from time import sleep -from .static import Action, ActionKeys, TileTypes -from .ansi import Ansi +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 """ - def __init__(self, bot = None): - # Vaatii hieman terminaaliasetusten muokkaamista jotta yksittäiset - # napin painallukset voidaan lukea - # https://stackoverflow.com/questions/983354/how-do-i-wait-for-a-pressed-key - fd = sys.stdin.fileno() - self.oldterm = termios.tcgetattr(fd) + # pylint: disable = unused-argument + def __init__(self, **opts): + self.bot = opts['bot'] if 'bot' in opts else None + self.autoplay = opts['autoplay'] if 'autoplay' in opts else False + self.interact = opts['interact'] if 'interact' in opts else True + self.suppress = opts['suppress'] if 'suppress' in opts else False + self.height = opts['height'] if 'height' in opts else 15 + + # jos ei oo bottia pitää olla interaktiivinen + if self.bot is None: + self.autoplay = False + self.interact = True + self.suppress = False + + # jos ei mitään näytetä ei voi olla interaktiivinen + self.interact = False if self.suppress else self.interact + + # automaattipeli pitää olla päällä jos ei interaktiivinen + self.autoplay = self.autoplay if self.interact else True + + if self.interact: + self.kbd = Kbd() + else: + self.kbd = NoKbd() + + if self.suppress: + self.draw = SuppressDraw() + else: + self.draw = AnsiDraw(height=self.height) - 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) - - self.bot = bot - - - def __del__(self): - # palautetaan terminaali takaisin alkupetäiseen uskoon - fd = sys.stdin.fileno() - termios.tcsetattr(fd, termios.TCSAFLUSH, self.oldterm) - fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags) - print() - - - def draw_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 draw_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.draw_tile(matrix[x][y], hilight) - print() + 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: + self.draw.matrix(matrix, -1, -1) + return Action.OPEN if action==Action.SAFE else action, x, y - 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 + # ilman näppiskäsittelijää voidaan lopettaa + if not self.interact: + return Action.QUIT, 0, 0 - def matrix_selector(self, matrix, x, y): - """ piirtää ruudukon ja antaa käyttäjän valita nuolinäppäimillä """ - self.draw_matrix(matrix, x, y) + w, h = len(matrix), len(matrix[0]) while True: - action = self.read_action() + 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.BOMB | Action.SAFE: if matrix[x][y] >= 10: 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 < len(matrix[0])-1 else y - case Action.RIGHT: - x = x+1 if x < len(matrix)-1 else x - case Action.TOP: - y = 0 - case Action.BOTTOM: - y = len(matrix[0])-1 - case Action.BEGIN: - x = 0 - case Action.END: - x = len(matrix)-1 case Action.HINT: if self.bot is not None: return self.bot.hint(matrix, x, y) - self.draw_matrix(matrix, x, y) - - - def show_board_with_text(self, matrix, x, y, text): - """ näyttää laudan, tekstin alla ja jää odottelemaan nappia """ - self.draw_matrix(matrix, x, y) - print(text) - Ansi.cup(1) - self.read_action() - - - def game_begin(self, width, height): - """ ruudun alustus ja lähtökoordinaatien määritys """ - print(end="\n"*(height+1)) - Ansi.cup(1) - return width//2, height//2 - def game_over(self, matrix, x, y): - """ näyttää pelin lopputilanteen ja odottaa nappia """ - self.show_board_with_text(matrix, x, y, - "KUOLEMA! ...näppäimellä eteenpäin...") - + """ tehtävät kun kuolee """ + self.draw.matrix(matrix, x, y) + self.draw.status_line( + "K " if self.suppress else "Peli ohitse! Kuolit!" + ) + self.kbd.read_action() def game_win(self, matrix, x, y): - """ näyttäää pelin lopputilanteen ja odottaa nappia """ - self.show_board_with_text(matrix, x, y, - "VOITTO! ...näppäimellä eteenpäin...") - + """ tehtävät kun voittaa """ + self.draw.matrix(matrix, x, y) + self.draw.status_line( + "V " if self.suppress else "Peli ohitse! Voitit!" + ) + self.kbd.read_action() def game_end(self, matrix): - """ pelin lopetus """ - self.show_board_with_text(matrix, -1, -1, - "PELI OHI! ...näppäimellä eteenpäin...") - print() + """ tehtävät ihan pelin lopuksi """ + if self.interact: + self.draw.matrix(matrix, -1, -1) + self.draw.status_line("Kiitos! ") |