1# -*- coding: utf-8 -*-
2# vim:fenc=utf-8
3#
4# Copyright © 2019 Shlomi Fish <shlomif@cpan.org>
5#
6# Distributed under terms of the Expat license.
7
8"""
9
10"""
11
12from pysol_cards.cards import Card, createCards
13from pysol_cards.random import shuffle
14
15from six import print_
16
17
18def empty_card():
19    ret = Card(0, 0, 0)
20    ret.empty = True
21    return ret
22
23
24class Columns(object):
25    def __init__(self, num):
26        self.cols = [[] for _ in range(num)]
27
28    def add(self, idx, card):
29        self.cols[idx].append(card)
30
31    def rev(self):
32        self.cols.reverse()
33
34
35class Board(object):
36    def __init__(self, num_columns, with_freecells=False,
37                 with_talon=False, with_foundations=False):
38        self.with_freecells = with_freecells
39        self.with_talon = with_talon
40        self.with_foundations = with_foundations
41        self.raw_foundations_cb = None
42        self.raw_foundations_line = None
43        self.columns = Columns(num_columns)
44        if self.with_freecells:
45            self.freecells = []
46        if self.with_talon:
47            self.talon = []
48        if self.with_foundations:
49            self.foundations = [empty_card() for s in range(4)]
50        self._lines = []
51
52    def add_line(self, string):
53        self._lines.append(string)
54
55    def reverse_cols(self):
56        self.columns.rev()
57
58    def add(self, idx, card):
59        self.columns.add(idx, card)
60
61    def add_freecell(self, card):
62        if not self.with_freecells:
63            raise AttributeError("Layout does not have freecells!")
64        self.freecells.append(card)
65
66    def add_talon(self, card):
67        if not self.with_talon:
68            raise AttributeError("Layout does not have a talon!")
69        self.talon.append(card)
70
71    def put_into_founds(self, card):
72        if not self.with_foundations:
73            raise AttributeError("Layout does not have foundations!")
74        res = self.foundations[card.suit].rank + 1 == card.rank
75        if res:
76            self.foundations[card.suit] = card
77        return res
78
79    def print_foundations(self, renderer):
80        cells = []
81        for f in [2, 0, 3, 1]:
82            if not self.foundations[f].empty:
83                cells.append(renderer.found_s(self.foundations[f]))
84
85        if len(cells):
86            self.add_line("Foundations:" + ("".join([" " + s for s in cells])))
87
88    def gen_lines(self, renderer):
89        self._lines = []
90        if self.with_talon:
91            self.add_line("Talon: " + renderer.l_concat(self.talon))
92        if self.with_foundations:
93            self.print_foundations(renderer)
94        if self.raw_foundations_cb:
95            self.add_line(self.raw_foundations_cb(renderer))
96        elif self.raw_foundations_line:
97            self.add_line(self.raw_foundations_line)
98        if self.with_freecells:
99            self.add_line("Freecells: " + renderer.l_concat(self.freecells))
100
101        self._lines += [renderer.l_concat(c) for c in self.columns.cols]
102
103    def calc_string(self, renderer):
104        self.gen_lines(renderer)
105        return "".join(l + "\n" for l in self._lines)
106
107
108class Game(object):
109    REVERSE_MAP = \
110        {
111            "freecell":
112            ["forecell", "bakers_game",
113             "ko_bakers_game", "kings_only_bakers_game",
114             "relaxed_freecell", "eight_off"],
115            "der_katz":
116            ["der_katzenschwantz", "die_schlange"],
117            "seahaven":
118            ["seahaven_towers", "relaxed_seahaven",
119             "relaxed_seahaven_towers"],
120            "bakers_dozen": [],
121            "gypsy": [],
122            "klondike":
123            ["klondike_by_threes",
124             "casino_klondike", "small_harp", "thumb_and_pouch",
125             "vegas_klondike", "whitehead"],
126            "simple_simon": [],
127            "yukon": [],
128            "beleaguered_castle":
129            ["streets_and_alleys", "citadel"],
130            "fan": [],
131            "black_hole": [],
132            "all_in_a_row": [],
133            "golf": [],
134        }
135
136    GAMES_MAP = {}
137    for k, v in REVERSE_MAP.items():
138        for name in [k] + v:
139            GAMES_MAP[name] = k
140
141    def __init__(self, game_id, game_num, which_deals, max_rank=13):
142        self.game_id = game_id
143        self.game_num = game_num
144        self.which_deals = which_deals
145        self.max_rank = max_rank
146        self.game_class = self.GAMES_MAP[self.game_id]
147        if not self.game_class:
148            raise ValueError("Unknown game type " + self.game_id + "\n")
149
150    def is_two_decks(self):
151        return self.game_id in ("der_katz", "der_katzenschwantz",
152                                "die_schlange", "gypsy")
153
154    def get_num_decks(self):
155        return 2 if self.is_two_decks() else 1
156
157    def calc_deal_string(self, game_num, renderer):
158        self.game_num = game_num
159        self.deal()
160        getattr(self, self.game_class)()
161        return self.board.calc_string(renderer)
162
163    def calc_layout_string(self, renderer):
164        self.deal()
165        getattr(self, self.game_class)()
166        return self.board.calc_string(renderer)
167
168    def print_layout(self, renderer):
169        print_(self.calc_layout_string(renderer), sep='', end='')
170
171    def new_cards(self, cards):
172        self.cards = cards
173        self.card_idx = 0
174
175    def deal(self):
176        cards = shuffle(createCards(self.get_num_decks(),
177                                    self.max_rank),
178                        self.game_num, self.which_deals)
179        cards.reverse()
180        self.new_cards(cards)
181
182    def __iter__(self):
183        return self
184
185    def no_more_cards(self):
186        return self.card_idx >= len(self.cards)
187
188    def __next__(self):
189        if self.no_more_cards():
190            raise StopIteration
191        c = self.cards[self.card_idx]
192        self.card_idx += 1
193        return c
194
195    def next(self):
196        return self.__next__()
197
198    def add(self, idx, card):
199        self.board.add(idx, card)
200
201    def add_freecell(self, card):
202        self.board.add_freecell(card)
203
204    def cyclical_deal(self, num_cards, num_cols, flipped=False):
205        for i in range(num_cards):
206            self.add(i % num_cols, next(self).flip(flipped=flipped))
207
208    def add_all_to_talon(self):
209        for c in self:
210            self.board.add_talon(c)
211
212    def add_empty_fc(self):
213        self.add_freecell(empty_card())
214
215    def _shuffleHookMoveSorter(self, cards, cb, ncards):
216        extracted, i, new = [], len(cards), []
217        for c in cards:
218            select, ord_ = cb(c)
219            if select:
220                extracted.append((ord_, i, c))
221                if len(extracted) >= ncards:
222                    new += cards[(len(cards) - i + 1):]
223                    break
224            else:
225                new.append(c)
226            i -= 1
227        return new, [x[2] for x in reversed(sorted(extracted))]
228
229    def _shuffleHookMoveToBottom(self, inp, cb, ncards=999999):
230        cards, scards = self._shuffleHookMoveSorter(inp, cb, ncards)
231        return scards + cards
232
233    def _shuffleHookMoveToTop(self, inp, cb, ncards=999999):
234        cards, scards = self._shuffleHookMoveSorter(inp, cb, ncards)
235        return cards + scards
236
237    def all_in_a_row(game):
238        game.board = Board(13)
239        game.cards = game._shuffleHookMoveToTop(
240            game.cards,
241            lambda c: (c.id == 13, c.suit),
242            1)
243        game.cyclical_deal(52, 13)
244        game.board.raw_foundations_line = 'Foundations: -'
245
246    def bakers_dozen(game):
247        n = 13
248        cards = list(reversed(game.cards))
249        for i in [i for i, c in enumerate(cards) if c.is_king()]:
250            j = i % n
251            while j < i:
252                if not cards[j].is_king():
253                    cards[i], cards[j] = cards[j], cards[i]
254                    break
255                j += n
256        game.new_cards(cards)
257        game.board = Board(13)
258        game.cyclical_deal(52, 13)
259
260    def beleaguered_castle(game):
261        game.board = Board(8, with_foundations=True)
262        if game.game_id in ('beleaguered_castle', 'citadel'):
263            new = []
264            for c in game:
265                if c.is_ace():
266                    game.board.put_into_founds(c)
267                else:
268                    new.append(c)
269            game.new_cards(new)
270        for _ in range(6):
271            for s in range(8):
272                c = next(game)
273                cond1 = game.game_id == 'citadel'
274                if not (cond1 and game.board.put_into_founds(c)):
275                    game.add(s, c)
276            if game.no_more_cards():
277                break
278        if game.game_id == 'streets_and_alleys':
279            game.cyclical_deal(4, 4)
280
281    def black_hole(game):
282        game.board = Board(17)
283        game.cards = game._shuffleHookMoveToBottom(
284            game.cards,
285            lambda c: (c.id == 13, c.suit),
286            1)
287        next(game)
288        game.cyclical_deal(52 - 1, 17)
289        game.board.raw_foundations_line = 'Foundations: AS'
290
291    def der_katz(game):
292        is_ds = game.game_id == 'die_schlange'
293        if is_ds:
294            print_('Foundations: H-A S-A D-A C-A H-A S-A D-A C-A')
295        game.board = Board(9)
296        i = 0
297        for c in game:
298            if c.is_king():
299                i += 1
300            if not (is_ds and c.is_ace()):
301                game.add(i, c)
302
303    def fan(game):
304        game.board = Board(18)
305        game.cyclical_deal(52 - 1, 17)
306        game.add(17, next(game))
307
308    def freecell(game):
309        is_fc = (game.game_id in ("forecell", "eight_off"))
310        game.board = Board(8, with_freecells=is_fc)
311        max_rank = (game.max_rank - 1 if is_fc else game.max_rank)
312        game.cyclical_deal(4 * max_rank, 8)
313
314        if is_fc:
315            for c in game:
316                game.add_freecell(c)
317                if game.game_id == "eight_off":
318                    game.add_empty_fc()
319
320    def golf(game):
321        num_cols = 7
322        game.board = Board(num_cols, with_talon=True)
323        game.cyclical_deal(num_cols * 5, num_cols)
324        game.add_all_to_talon()
325        card = game.board.talon.pop(0)
326        game.board.raw_foundations_cb = lambda renderer: 'Foundations: ' + \
327            renderer.l_concat([card])
328
329    def gypsy(game):
330        num_cols = 8
331        game.board = Board(num_cols, with_talon=True)
332        game.cyclical_deal(num_cols * 2, num_cols, flipped=True)
333        game.cyclical_deal(num_cols, num_cols)
334        game.add_all_to_talon()
335
336    def klondike(game):
337        num_cols = 7
338        game.board = Board(num_cols, with_talon=True)
339        for r in range(num_cols - 1, 0, -1):
340            game.cyclical_deal(r, r, flipped=True)
341        game.cyclical_deal(num_cols, num_cols)
342        game.add_all_to_talon()
343        if not (game.game_id == 'small_harp'):
344            game.board.reverse_cols()
345
346    def seahaven(game):
347        game.board = Board(10, with_freecells=True)
348        game.cyclical_deal(50, 10)
349        game.add_empty_fc()
350        for c in game:
351            game.add_freecell(c)
352
353    def simple_simon(game):
354        game.board = Board(10)
355        for num_cards in range(9, 2, -1):
356            game.cyclical_deal(num_cards, num_cards)
357        game.cyclical_deal(10, 10)
358
359    def yukon(game):
360        num_cols = 7
361        game.board = Board(num_cols)
362        for i in range(1, num_cols):
363            for j in range(i, num_cols):
364                game.add(j, next(game).flip())
365        for i in range(4):
366            for j in range(1, num_cols):
367                game.add(j, next(game))
368        game.cyclical_deal(num_cols, num_cols)
369