aboutsummaryrefslogtreecommitdiff
path: root/ouija.py
diff options
context:
space:
mode:
Diffstat (limited to 'ouija.py')
-rw-r--r--ouija.py327
1 files changed, 327 insertions, 0 deletions
diff --git a/ouija.py b/ouija.py
new file mode 100644
index 0000000..4dc56b1
--- /dev/null
+++ b/ouija.py
@@ -0,0 +1,327 @@
+import curses
+from functools import reduce
+from enum import Enum
+import math
+
+class Ouija:
+
+ class Align(Enum):
+ LEFT = 0
+ RIGHT = 1
+ CENTER = 2
+
+ DEFAULT_VERT_EDGE = "|"
+ DEFAULT_HORIZ_EDGE = "~"
+ DEFAULT_CORNER = ":"
+ DEFAULT_HORIZ_PAD = 1
+ DEFAULT_VERT_PAD = 0
+ DEFAULT_HORIZ_MARGIN = 0
+ DEFAULT_VERT_MARGIN = 0
+ DEFAULT_UNIFORM_WIDTH = True
+ DEFAULT_ALIGN = Align.LEFT
+
+ class _OuijaColor:
+
+ DEFAULT = -1
+ BLACK = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 4
+ CYAN = 6
+ WHITE = 7
+ ORANGE = 9
+ YELLOW = 11
+
+ def __init__(self, fg=DEFAULT, bg=DEFAULT):
+ self.fg = fg
+ self.bg = bg
+
+ def color_pair(self):
+ # start with an offset of 1 (can't do negatives)
+ out = 1
+
+ # hacky bitwise operator for different fg/bg colors
+ white_bg = 16
+ white_fg = 32
+ black_fg = 64
+
+ if self.bg == self.DEFAULT:
+ return out + self.fg
+ elif self.bg == self.WHITE:
+ return out + white_bg + self.fg
+ elif self.fg == self.WHITE:
+ return out + white_fg + self.bg
+ else:
+ return out + black_fg + self.bg
+
+ COLORS = {
+ "DEFAULT" : _OuijaColor(),
+ "BLACK" : _OuijaColor(_OuijaColor.BLACK),
+ "RED" : _OuijaColor(_OuijaColor.RED),
+ "GREEN" : _OuijaColor(_OuijaColor.GREEN),
+ "BLUE" : _OuijaColor(_OuijaColor.BLUE),
+ "CYAN" : _OuijaColor(_OuijaColor.CYAN),
+ "WHITE" : _OuijaColor(_OuijaColor.WHITE),
+ "ORANGE" : _OuijaColor(_OuijaColor.ORANGE),
+ "YELLOW" : _OuijaColor(_OuijaColor.YELLOW),
+
+ "RED_ON_WHITE" : _OuijaColor(_OuijaColor.RED, _OuijaColor.WHITE),
+ "GREEN_ON_WHITE" : _OuijaColor(_OuijaColor.GREEN, _OuijaColor.WHITE),
+ "BLUE_ON_WHITE" : _OuijaColor(_OuijaColor.BLUE, _OuijaColor.WHITE),
+ "CYAN_ON_WHITE" : _OuijaColor(_OuijaColor.CYAN, _OuijaColor.WHITE),
+ "ORANGE_ON_WHITE" : _OuijaColor(_OuijaColor.ORANGE, _OuijaColor.WHITE),
+ "YELLOW_ON_WHITE" : _OuijaColor(_OuijaColor.YELLOW, _OuijaColor.WHITE),
+
+ "WHITE_ON_BLACK" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.BLACK),
+ "WHITE_ON_RED" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.RED),
+ "WHITE_ON_GREEN" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.GREEN),
+ "WHITE_ON_BLUE" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.BLUE),
+ "WHITE_ON_CYAN" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.CYAN),
+ "WHITE_ON_ORANGE" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.ORANGE),
+ "WHITE_ON_YELLOW" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.YELLOW),
+
+ "BLACK_ON_WHITE" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.WHITE),
+ "BLACK_ON_RED" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.RED),
+ "BLACK_ON_GREEN" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.GREEN),
+ "BLACK_ON_BLUE" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.BLUE),
+ "BLACK_ON_CYAN" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.CYAN),
+ "BLACK_ON_ORANGE" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.ORANGE),
+ "BLACK_ON_YELLOW" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.YELLOW),
+ }
+
+ def _setup_curses_colors():
+
+ # initialize colors for curses
+ curses.start_color()
+ curses.use_default_colors()
+
+ for c in Ouija.COLORS.keys():
+ color = Ouija.COLORS[c]
+ curses.init_pair(color.color_pair(), color.fg, color.bg)
+
+ def __init__(self, stdscr, y, x):
+ self.stdscr = stdscr
+ self.y = y
+ self.x = x
+
+ # setup default styles
+ self.style()
+
+ # setup the curses colors
+ Ouija._setup_curses_colors()
+
+ # helper method to access style variables
+ def style(self,
+ vert_edge=DEFAULT_VERT_EDGE,
+ horiz_edge=DEFAULT_HORIZ_EDGE,
+ corner=DEFAULT_CORNER,
+ horiz_pad=DEFAULT_HORIZ_PAD,
+ vert_pad=DEFAULT_VERT_PAD,
+ horiz_margin=DEFAULT_HORIZ_MARGIN,
+ vert_margin=DEFAULT_VERT_MARGIN,
+ uniform_width=DEFAULT_UNIFORM_WIDTH,
+ align=DEFAULT_ALIGN):
+ self.vert_edge = vert_edge
+ self.horiz_edge = horiz_edge
+ self.corner = corner
+ self.horiz_pad = horiz_pad
+ self.vert_pad = vert_pad
+ self.horiz_margin = horiz_margin
+ self.vert_margin = vert_margin
+ self.uniform_width = uniform_width
+ self.align = align
+
+ # setup the tiles
+ def setup_tiles(self, ts):
+ if type(ts) != list:
+ return False
+ self.tiles = {}
+ for t in ts:
+ if type(t) != Tile:
+ return False
+ self.tiles[t.in_val] = t
+ print(str(self.tiles))
+ return True
+
+ def calc_cell_width(self, cell_value):
+ edges = 2 if self.vert_edge is not None else 0
+ return len(cell_value) + (2 * self.horiz_pad) + edges
+
+ def calc_row_width(self, row):
+ width = 0
+ for cell in row:
+
+ # if there's a cooresponding display value, use its length
+ # if not, just take the width directly
+ if cell in self.tiles:
+ width += self.calc_cell_width(self.tiles[cell].out_val)
+ else:
+ width += self.calc_cell_width(cell)
+
+ # if there's no margin, we double counted some dividers
+ if self.horiz_margin == 0:
+ width -= len(row) - 1
+ elif self.horiz_margin > 1:
+ width += (self.horiz_margin - 1) * (len(row) - 1)
+
+ return width
+
+ # draw the board, optionally highlighting a cell
+ def draw_board(self, board, hy=-1, hx=-1):
+
+ # calculate the longest row
+ max_row_length = max([len(row) for row in board])
+
+ if self.uniform_width:
+ # get the widest possible cell
+ out_vals = [i.out_val for i in self.tiles.values()]
+ max_cell_width = max([len(x) for x in out_vals])
+
+ # the max row width is the longest row of the widest cells
+ max_row_width = self.calc_row_width(["." * max_cell_width] * max_row_length)
+ else:
+ max_row_width = max([self.calc_row_width(row) for row in board])
+
+ # if specified, make every key as wide as the widest key
+ if self.uniform_width:
+ out_vals = [i.out_val for i in self.tiles.values()]
+ max_width = max([len(x) for x in out_vals])
+
+ # keep track of y-coordinate, and board y-index
+ cur_y = self.y
+ iy = 0
+
+ # loop through all columns in board
+ for col in board:
+
+ if self.uniform_width:
+ cur_row_width = self.calc_row_width(["." * max_cell_width] * len(col))
+ else:
+ cur_row_width = self.calc_row_width(col)
+
+ # keep track of x-coordinate, and board x-index
+ cur_x = self.x
+ ix = 0
+
+ # loop through all cells in the column
+ for cell in col:
+
+ tile = self.tiles[cell]
+
+ if self.uniform_width:
+ cell_width = self.calc_cell_width("." * max_cell_width)
+ else:
+ cell_width = self.calc_cell_width(tile.out_val)
+
+ target_x = cur_x
+ if self.align == Ouija.Align.RIGHT:
+ target_x += max_row_width - cur_row_width
+ if self.align == Ouija.Align.CENTER:
+ target_x += math.floor(max_row_width / 2) - math.floor(cur_row_width / 2)
+
+
+ # create the format string with the appropriate width
+ target_width = max_width if self.uniform_width else len(tile.out_val)
+ fmt_string = "{:^" + str(target_width) + "}"
+
+ # add A_STANDOUT if the cell needs to be highlighted
+ target_style = tile.style
+ if hx == ix and hy == iy:
+ target_style = target_style | curses.A_STANDOUT
+
+ # draw the key
+ #self.draw_rect_key(cur_y, cur_x, fmt_string.format(tile.out_val), tile.color, target_style)
+ self.draw_rect_key(cur_y, target_x, fmt_string.format(tile.out_val), tile.color, target_style)
+
+ # update the x-coordinate, based on width, padding, and margin
+ # if there's a vertical edge, add an additional position
+ #cur_x += target_width + (2 * self.horiz_pad) + self.horiz_margin
+ cur_x += cell_width - 1 + self.horiz_margin
+
+ # update the board x-index
+ ix += 1
+
+ # update the y-coordinate, based on padding and margin
+ # if there's a horizontal edge, add an additional position
+ cur_y += 1 + (2 * self.vert_pad) + self.vert_margin
+ if self.horiz_edge is not None:
+ cur_y += 1
+
+ # update the board x-index
+ iy += 1
+
+ # draw a rectangular key, with the top corner at the given (y, x),
+ # the specified text, color, and style
+ def draw_rect_key(self, y, x, text, color, style):
+ # calculate the inner width, including padding
+ iw = len(text) + (2 * self.horiz_pad)
+
+ # outer width and horizontal offset are defaults
+ ow = iw
+ h_off = 0
+ v_off = 1
+
+ # if we have a vertical edge, adjust the outer width and horizontal offset
+ if self.vert_edge is not None:
+ ow += 2
+ h_off += 1
+
+ # create format strings for the inner and outer widths
+ owf = "{:^" + str(ow) + "}"
+ iwf = "{:^" + str(iw) + "}"
+
+ # calculate how tall each cell should be,
+ # and where in the cell the text should appear
+ max_y = y + 2 + (2 * self.vert_pad)
+ mid_y = math.ceil((max_y - y) / 2)
+
+ # draw the horizontal edges, if defined
+ if self.horiz_edge is not None:
+
+ # top edge begins at (y, x),
+ # is 'iw' long,
+ # centered within the 'ow' length
+ self.stdscr.addstr(y, x, owf.format(self.horiz_edge * iw))
+
+ # bottom edge takes into account vertical offset
+ # and vertical padding
+ self.stdscr.addstr(y + (2 * v_off) + (2 * self.vert_pad), x, owf.format(self.horiz_edge * iw))
+
+ # draw the vertical edges, if defined
+ if self.vert_edge is not None:
+
+ # loop through each row we need to draw
+ for iy in range(y + 1, max_y):
+ self.stdscr.addstr(iy, x, self.vert_edge)
+ self.stdscr.addstr(iy, x + ow - 1, self.vert_edge)
+
+ # draw the corners, if defined
+ if self.corner is not None:
+ self.stdscr.addstr(y, x, self.corner)
+ self.stdscr.addstr(y, x + ow - 1, self.corner)
+ self.stdscr.addstr(max_y, x, self.corner)
+ self.stdscr.addstr(max_y, x + ow - 1, self.corner)
+
+ # draw the inside
+ for iy in range(y + 1, max_y):
+
+ # if we're at the mid-point of the cell, draw the text
+ # otherwise, don't draw anything
+ target_text = text if iy == (y + mid_y) else ""
+ self.stdscr.addstr(iy, x + h_off, iwf.format(target_text), style | curses.color_pair(color.color_pair()))
+
+class Tile:
+
+ def __init__(self, in_val, out_val, color=Ouija.COLORS["DEFAULT"], style=0):
+ self.in_val = in_val
+ self.out_val = out_val
+ self.color = color
+ self.style = style
+
+ def __str__(self):
+ return "[{},{},{}]".format(self.in_val, self.out_val, str(self.color))
+
+# helper function, not sure where it goes yet
+def biggest_cell(board):
+ return max([max(list(map(lambda x: len(x), row))) for row in board])
+