-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsolve.py
377 lines (305 loc) · 13.8 KB
/
solve.py
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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# -*- coding: utf-8 -*-
from __future__ import print_function
import cStringIO
from collections import namedtuple
COMBOS_TRIED = 0 # the number of placements tried during the search
TermColors = dict(
BOLD='\033[1m',
INV='\033[7m', # inverted background and foreground
RESET='\033[0m',
)
TermColors = namedtuple('TermColors', TermColors.keys())(*TermColors.values())
class Card(object):
"""A card which can be placed onto the game panel.
It can be placed either with its front or back side up and with its main
edge facing one of the four possible directions (north, east, south, west)
resulting in 8 different possible placements.
"""
# F - front side up, B - back side up
# N, E, S, W - orientation of the card's reference (main) edge
Orientation = namedtuple(
'Orientation', ['FN', 'FE', 'FS', 'FW', 'BN', 'BE', 'BS', 'BW'])
Orientation = Orientation(*xrange(8))
Color = namedtuple('Color', ['UNKNOWN', 'BLUE', 'GREEN', 'RED', 'YELLOW'])
Color = Color(*xrange(-1, 4))
def __init__(self, card_id, edges, orientation=Orientation.FN):
"""Create a new card instance.
:param card_id: card's identifier (must be unique!)
:param edges: list of descriptions of card edges' colors with each
description being a two-tuple (color1, color2). The following
edge order is assumed: FN, FE, FS, FW, BN, BE, BS, BW
(clockwise starting with NORTH, FRONT side first)
:param orientation: card's orientation
"""
self.card_id = card_id
self.edges = edges
self.orientation = orientation
def __eq__(self, other):
return (other is not None) and self.card_id == other.card_id
def __ne__(self, other):
return (other is None) or self.card_id != other.card_id
def __getitem__(self, item_idx):
"""Return card's color at item_idx on the side currently facing up
(taking the current card orientation into account).
item_idx:
0 1
7 2
6 3
5 4
"""
# adjust index for card's rotation
idx = (item_idx - 2 * (self.orientation % 4)) % 8
if self.orientation < 4: # front side up
return (self.edges[idx // 2])[idx % 2]
else:
return (self.edges[idx // 2 + 4])[idx % 2]
def __setitem__(self, item_idx, item):
"""Prevent any subsequent modifications of the card's edge colors."""
raise TypeError("Modifying card colors not allowed.")
class GamePanel(object):
"""Representation of the game panel with 9 slots for cards.
A note on the card placement: if the card is placed with its back side up,
it is not "physically turned around". Instead the back side "bubbles up"
to the surface so that the front side is now behind (under) it.
This way the card's edge indexes are not mirrored, simplyfying some
calculations.
Indexes of panel's slots (where the cards can be placed):
0 1 2
3 4 5
6 7 8
"""
# a mapping of Card.Color to the colors in the output to the terminal
COLOR_STR = {
Card.Color.UNKNOWN: ' ',
Card.Color.BLUE: '\033[34mB\033[0m',
Card.Color.GREEN: '\033[32mG\033[0m',
Card.Color.RED: '\033[31mR\033[0m',
Card.Color.YELLOW: '\033[33mY\033[0m',
}
# indexes of slots adjacent to a particular slot
NeighborIdx = namedtuple('NeighborIdx', ['north', 'east', 'south', 'west'])
SLOT_NEIGHBORS = (
#(NORTH, EAST, SOUTH, WEST)
NeighborIdx(None, 1, 3, None),
NeighborIdx(None, 2, 4, 0),
NeighborIdx(None, None, 5, 1),
NeighborIdx(0, 4, 6, None),
NeighborIdx(1, 5, 7, 3),
NeighborIdx(2, None, 8, 4),
NeighborIdx(3, 7, None, None),
NeighborIdx(4, 8, None, 6),
NeighborIdx(5, None, None, 7),
)
def __init__(self):
self.slots = [None] * 9
# current empty slot where new card will be placed
# defaults to 0 (upper-left corner)
self.curr_slot = 0
def __str__(self):
Color = Card.Color
output = cStringIO.StringIO()
output.write(''.join(['+', '-----+' * 3, '\n']))
for row in range(3):
# each card is displayed in 5 text lines, 3 cards per each
# game panel row
cards_in_row = self.slots[row * 3: row * 3 + 3]
# 1st line
for card in cards_in_row:
# highlight card's edge if oriented to north
TermColors.INV
rev = TermColors.INV if card.orientation in (0, 4) else ''
color1, color2 = (card[0], card[1]) if card \
else (Color.UNKNOWN, Color.UNKNOWN)
output.write('| {rev}{0}{reset} {rev}{1}{reset} '.format(
self.COLOR_STR[color1], self.COLOR_STR[color2],
rev=rev, reset=TermColors.RESET)
)
output.write('|\n')
# 2nd line
for card in cards_in_row:
# highlight western edge if the card is oriented to the west
rev_w = TermColors.INV if card.orientation in (3, 7) else ''
# highlight eastern edge if the card oriented to the east
rev_e = TermColors.INV if card.orientation in (1, 5) else ''
color1, color2 = (card[7], card[2]) if card \
else (Color.UNKNOWN, Color.UNKNOWN)
output.write('|{rev_w}{}{reset} {rev_e}{}{reset}'.format(
self.COLOR_STR[color1], self.COLOR_STR[color2],
rev_w=rev_w, rev_e=rev_e, reset=TermColors.RESET)
)
output.write('|\n')
# 3rd line
for card in cards_in_row:
# highlight western edge if the card is oriented to the west
rev_w = TermColors.INV if card.orientation in (3, 7) else ''
# highlight eastern edge if the card oriented to the east
rev_e = TermColors.INV if card.orientation in (1, 5) else ''
color1, color2 = (card[6], card[3]) if card \
else (Color.UNKNOWN, Color.UNKNOWN)
output.write('|{rev_w}{}{reset} {rev_e}{}{reset}'.format(
self.COLOR_STR[color1], self.COLOR_STR[color2],
rev_w=rev_w, rev_e=rev_e, reset=TermColors.RESET)
)
output.write('|\n')
# 4th line
for card in cards_in_row:
# highlight card's edge if oriented to south
rev = TermColors.INV if card.orientation in (2, 6) else ''
color1, color2 = (card[5], card[4]) if card else \
(Color.UNKNOWN, Color.UNKNOWN)
output.write('| {rev}{}{reset} {rev}{}{reset} '.format(
self.COLOR_STR[color1], self.COLOR_STR[color2],
rev=rev, reset=TermColors.RESET)
)
output.write('|\n')
# 5th line
output.write(''.join(['+', '-----+' * 3, '\n']))
placement = []
for i, c in enumerate(self.slots):
if i % 3 == 0:
placement.append('\n')
if c is not None:
# facing = "down" if c.orientation // 4 else "up"
placement.append('{bold}{}{reset}({bold}{}{reset})'.format(
c.card_id, c.Orientation._fields[c.orientation],
bold=TermColors.BOLD, reset=TermColors.RESET)
)
output.write("CARD PLACEMENT: {0}".format(' '.join(placement)))
contents = output.getvalue()
output.close()
return contents
def can_place(self, card):
"""Can we place a card into the currently first empty slot?
We must check all cards in adjacent slots to verify that all edge
colors match.
"""
# northern neighbor
idx = GamePanel.SLOT_NEIGHBORS[self.curr_slot].north
neighbor_card = self.slots[idx] if (idx is not None) else None
if neighbor_card is not None:
if (card[0] != neighbor_card[5]) or (card[1] != neighbor_card[4]):
return False
# eastern neighbor
idx = GamePanel.SLOT_NEIGHBORS[self.curr_slot].east
neighbor_card = self.slots[idx] if (idx is not None) else None
if neighbor_card is not None:
if (card[2] != neighbor_card[7]) or (card[3] != neighbor_card[6]):
return False
# southern neighbor
idx = GamePanel.SLOT_NEIGHBORS[self.curr_slot].south
neighbor_card = self.slots[idx] if (idx is not None) else None
if neighbor_card is not None:
if (card[5] != neighbor_card[0]) or (card[4] != neighbor_card[1]):
return False
# western neighbor
idx = GamePanel.SLOT_NEIGHBORS[self.curr_slot].west
neighbor_card = self.slots[idx] if (idx is not None) else None
if neighbor_card is not None:
if (card[7] != neighbor_card[2]) or (card[6] != neighbor_card[3]):
return False
return True
def place(self, card):
self.slots[self.curr_slot] = card
self.curr_slot += 1
# XXX: automatically update list of cards available for placing?
def remove_last(self):
assert self.curr_slot > 0, "No cards to remove"
self.curr_slot -= 1
assert self.slots[self.curr_slot] is not None, \
"Can't remove card that isn't there"
self.slots[self.curr_slot] = None
# XXX: automatically update list of cards available for placing?
def generate_cards():
"""Generate and return a list of all cards in the game."""
Color = Card.Color # just to save some typing ...
card_edges = [
[(Color.GREEN, Color.BLUE), (Color.RED, Color.GREEN),
(Color.BLUE, Color.YELLOW), (Color.RED, Color.YELLOW),
(Color.YELLOW, Color.BLUE), (Color.RED, Color.YELLOW),
(Color.BLUE, Color.GREEN), (Color.RED, Color.GREEN)
],
[(Color.GREEN, Color.YELLOW), (Color.BLUE, Color.RED),
(Color.GREEN, Color.BLUE), (Color.RED, Color.YELLOW),
(Color.YELLOW, Color.GREEN), (Color.RED, Color.BLUE),
(Color.YELLOW, Color.BLUE), (Color.GREEN, Color.RED)
],
[(Color.GREEN, Color.RED), (Color.YELLOW, Color.BLUE),
(Color.GREEN, Color.YELLOW), (Color.BLUE, Color.RED),
(Color.YELLOW, Color.RED), (Color.BLUE, Color.GREEN),
(Color.YELLOW, Color.GREEN), (Color.RED, Color.BLUE)
],
[(Color.BLUE, Color.YELLOW), (Color.RED, Color.GREEN),
(Color.BLUE, Color.GREEN), (Color.YELLOW, Color.RED),
(Color.GREEN, Color.BLUE), (Color.YELLOW, Color.RED),
(Color.GREEN, Color.YELLOW),
(Color.RED, Color.BLUE)
],
[(Color.YELLOW, Color.RED), (Color.GREEN, Color.BLUE),
(Color.YELLOW, Color.BLUE), (Color.RED, Color.GREEN),
(Color.BLUE, Color.RED), (Color.YELLOW, Color.GREEN),
(Color.BLUE, Color.YELLOW), (Color.GREEN, Color.RED)
],
[(Color.BLUE, Color.RED), (Color.GREEN, Color.YELLOW),
(Color.BLUE, Color.YELLOW), (Color.RED, Color.GREEN),
(Color.RED, Color.BLUE), (Color.YELLOW, Color.GREEN),
(Color.RED, Color.YELLOW), (Color.GREEN, Color.BLUE)
],
[(Color.YELLOW, Color.GREEN), (Color.BLUE, Color.RED),
(Color.YELLOW, Color.RED), (Color.GREEN, Color.BLUE),
(Color.GREEN, Color.YELLOW), (Color.RED, Color.BLUE),
(Color.GREEN, Color.RED), (Color.BLUE, Color.YELLOW)
],
[(Color.RED, Color.YELLOW), (Color.BLUE, Color.YELLOW),
(Color.GREEN, Color.RED), (Color.BLUE, Color.GREEN),
(Color.RED, Color.GREEN), (Color.YELLOW, Color.GREEN),
(Color.BLUE, Color.RED), (Color.YELLOW, Color.BLUE)
],
[(Color.GREEN, Color.RED), (Color.YELLOW, Color.RED),
(Color.BLUE, Color.GREEN), (Color.YELLOW, Color.BLUE),
(Color.GREEN, Color.YELLOW), (Color.RED, Color.YELLOW),
(Color.BLUE, Color.GREEN), (Color.RED, Color.BLUE)
],
]
cards = []
for index, edges in enumerate(card_edges):
cards.append(Card(index+1, edges))
return cards
def solve_problem(panel, avail_cards):
global COMBOS_TRIED
if not avail_cards:
yield panel # all cards have been placed, we have a solution
else:
for card in avail_cards:
for orientation in Card.Orientation:
COMBOS_TRIED += 1
# copy needed? just remove it form the list/set!
# XXX: profile!
new_card = Card(card.card_id, card.edges, orientation)
if not panel.can_place(new_card):
continue
else:
panel.place(new_card)
# XXX: not efficient this copying back and forth?
new_cards_avail = \
[x for x in avail_cards if x != new_card]
for solved_panel in solve_problem(panel, new_cards_avail):
yield solved_panel
# after examining all sub-placings, remove last placed card
# and try with another card/orientation in the next
# iteration
panel.remove_last()
def main():
global COMBOS_TRIED
n_solutions = 0
cards = generate_cards()
panel = GamePanel()
for solved_panel in solve_problem(panel, cards):
n_solutions += 1
print(solved_panel, '\n')
# break # uncomment if need the first solution found
print("*** SOLUTIONS FOUND: ",
TermColors.BOLD, n_solutions, TermColors.RESET, sep='')
print("*** COMBINATIONS CHECKED: ",
TermColors.BOLD, COMBOS_TRIED, TermColors.RESET, sep='')
if __name__ == "__main__":
main()