aboutsummaryrefslogtreecommitdiff
path: root/ouija.py
blob: 4dc56b126524949fb667c946ad853d0d65d28a51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
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])