forked from yangshun/2048-python
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpuzzle.py
257 lines (216 loc) · 8.37 KB
/
puzzle.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
try:
import tkinter as tk
from itertools import zip_longest
except ImportError:
import Tkinter as tk
from itertools import izip_longest as zip_longest
from random import choice
CELL_SIZE = 100 # minimum, in pixels
GRID_LEN = 4
GRID_PADDING = 10
BACKGROUND_COLOR_GAME = "#92877d"
FONT = ("Verdana", 40, "bold")
FONT_MED = ("Verdana", 25, "bold")
FONT_SMALL = ("Verdana", 18, "bold")
COLORS = {
# (foreground color, background color)
2: ("#776e65", "#eee4da"),
4: ("#776e65", "#ede0c8"),
8: ("#f9f6f2", "#f2b179"),
16: ("#f9f6f2", "#f59563"),
32: ("#f9f6f2", "#f67c5f"),
64: ("#f9f6f2", "#f65e3b"),
128: ("#f9f6f2", "#edcf72"),
256: ("#f9f6f2", "#edcc61"),
512: ("#f9f6f2", "#edc850"),
1024: ("#f9f6f2", "#edc53f"),
2048: ("#f9f6f2", "#edc22e"),
None: ("#9e948a", "#9e948a"),
}
WINNER = 2048
ANIMATE = True
class Cell(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, bg=BACKGROUND_COLOR_GAME, **kwargs)
self.lbl = tk.Label(self, justify=tk.CENTER, font=FONT)
self.lbl.pack(fill=tk.BOTH, padx=GRID_PADDING, pady=GRID_PADDING, expand=True)
self.value = None
self.change()
def change(self, value=None):
self.value = value
text = str(value) if value is not None else ''
if value is not None:
while value > WINNER:
value //= WINNER
fg, bg = COLORS[value]
if ANIMATE and value is not None and self.lbl['bg'] != bg:
self.config(bg=bg)
self.after(120, self.shrink)
if len(text) <= 2:
font = FONT
elif len(text) == 3:
font = FONT_MED
else:
font = FONT_SMALL
self.lbl.config(text=text, font=font, bg=bg, fg=fg)
def shrink(self):
self.config(bg=BACKGROUND_COLOR_GAME)
class Message(tk.Frame):
def __init__(self, master=None, text=None, **kwargs):
tk.Frame.__init__(self, master, bd=4, relief=tk.RIDGE, **kwargs)
self.lbl = tk.Label(self, text=text, font=FONT)
self.lbl.pack(padx=GRID_PADDING, pady=GRID_PADDING)
btn = tk.Button(self, font=FONT, text='Continue', command=lambda: self.cont(False))
btn.pack(padx=GRID_PADDING, fill=tk.X, expand=True)
btn.focus()
btn = tk.Button(self, font=FONT, text='Restart', command=lambda: self.cont(True))
btn.pack(padx=GRID_PADDING,fill=tk.X, expand=True)
self.place(relx=.5, rely=.5, anchor=tk.CENTER)
def cont(self, restart):
self.master.mode = 'overtime'
if restart:
self.master.restart()
self.destroy()
class GameGrid(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, bg=BACKGROUND_COLOR_GAME, **kwargs)
master.cells = [Cell(self) for _ in range(GRID_LEN**2)]
for idx, cell in enumerate(master.cells):
row, col = divmod(idx, GRID_LEN)
cell.grid(row=row, column=col, sticky='nsew')
for i in range(GRID_LEN):
self.rowconfigure(i, minsize=CELL_SIZE, weight=1)
self.columnconfigure(i, minsize=CELL_SIZE, weight=1)
def compute(row):
'''computes the row towards the left'''
row = filter(None, row)
state = None
output = []
score = 0
for element in row:
if element == state:
output.append(element * 2)
score += element * 2
state = None
elif state is None:
state = element
else:
output.append(state)
state = element
if state is not None:
output.append(state)
return output, score
class ScoreBox(tk.Frame):
def __init__(self, master=None, name=None, **kwargs):
tk.Frame.__init__(self, master, bg=BACKGROUND_COLOR_GAME, **kwargs)
lbl = tk.Label(self, text=name, font=FONT_SMALL, bg=BACKGROUND_COLOR_GAME, fg='white')
lbl.pack()
var = tk.IntVar()
var.trace('w', self.draw)
self.lbl = tk.Label(self, font=FONT_MED, bg=BACKGROUND_COLOR_GAME, fg='white')
self.lbl.pack(fill=tk.Y, expand=True)
self.set, self.get = var.set, var.get
var.set(0)
def draw(self, *args):
self.lbl.config(text='{:,}'.format(self.get()))
class Status(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, height=CELL_SIZE, **kwargs)
self.pack_propagate(False)
btn = tk.Button(self, text='New\nGame', font=FONT_SMALL, command=master.restart)
btn.pack(side=tk.LEFT)
self.high_score = ScoreBox(self, "BEST")
self.high_score.pack(side=tk.RIGHT, padx=GRID_PADDING, pady=GRID_PADDING)
self.curr_score = ScoreBox(self, "SCORE")
self.curr_score.pack(side=tk.RIGHT, padx=GRID_PADDING, pady=GRID_PADDING)
try:
with open('high_score') as f:
self.high_score.set(int(f.read()))
except IOError:
pass
def inc_score(self, score):
self.curr_score.set(self.curr_score.get() + score)
if self.curr_score.get() > self.high_score.get():
self.high_score.set(self.curr_score.get())
with open('high_score', 'w') as f:
f.write(str(self.curr_score.get()))
class Game(tk.Tk):
def __init__(self, **kwargs):
tk.Tk.__init__(self, **kwargs)
self.title('2048')
self.bind('<Key>', self.keypress)
self.status = Status(self)
self.status.pack(fill=tk.X)
self.grid = GameGrid(self)
self.grid.pack(fill=tk.BOTH, expand=True)
self.restart()
self.after(500, lambda: self.minsize(self.winfo_width(), self.winfo_height()))
def restart(self):
self.mode = 'normal' # can be 'normal', 'lost', 'won', or 'overtime'
self.status.curr_score.set(0)
for cell in self.cells:
cell.change()
self.drop_a_two()
self.drop_a_two()
def keypress(self, event=None):
if self.mode in ('lost', 'won'):
return
if event.keysym == 'Left':
self.shift(self.get_rows())
elif event.keysym == 'Up':
self.shift(list(self.get_cols()))
elif event.keysym == 'Right':
self.shift(self.get_rows(), True)
elif event.keysym == 'Down':
self.shift(list(self.get_cols()), True)
def shift(self, rows, reverse=False):
new_rows = []
changed = False
score = 0
for row in rows:
values = [cell.value for cell in row]
new_values, s = compute(reversed(values) if reverse else values)
score += s
if reverse and len(new_values) > 0 and values[-len(new_values):] != new_values[::-1]:
changed = True
if not reverse and values[:len(new_values)] != new_values:
changed = True
new_rows.append(new_values)
if not changed:
return # move is illegal as there is nothing to do
for new_values, row in zip(new_rows, rows):
for cell, new_value in zip_longest(reversed(row) if reverse else row, new_values):
cell.change(new_value)
self.status.inc_score(score)
# check for winners
if self.mode == 'normal' and WINNER in [cell.value for cell in self.cells]:
self.mode = 'won'
self.message = Message(self, 'You Won!!')
self.drop_a_two()
# check for losers
if not self.moves_available():
self.mode = 'lost'
self.message = Message(self, 'You Lost!!')
def drop_a_two(self):
'''randomly pick an empty cell and make it a 2'''
empty_cells = [cell for cell in self.cells if cell.value is None]
choice(empty_cells).change(2)
def moves_available(self):
'''check if there are any moves left'''
for row in self.get_rows():
values = [cell.value for cell in row]
if compute(values)[0] != values:
return True
for row in self.get_cols():
values = [cell.value for cell in row]
if compute(values)[0] != values:
return True
return False
def get_rows(self):
return [self.cells[i:i+GRID_LEN] for i in range(0, GRID_LEN**2, GRID_LEN)]
def get_cols(self):
return zip(*self.get_rows())
r = Game()
r.mainloop()