summaryrefslogtreecommitdiff
path: root/src/miinaharava/tui
diff options
context:
space:
mode:
Diffstat (limited to 'src/miinaharava/tui')
-rw-r--r--src/miinaharava/tui/__init__.py3
-rw-r--r--src/miinaharava/tui/ansi.py46
-rw-r--r--src/miinaharava/tui/ansi_draw.py58
-rw-r--r--src/miinaharava/tui/kbd.py86
-rw-r--r--src/miinaharava/tui/static.py77
-rw-r--r--src/miinaharava/tui/tui.py109
6 files changed, 379 insertions, 0 deletions
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}"
+ )