-
Notifications
You must be signed in to change notification settings - Fork 0
/
pytang.py
330 lines (243 loc) · 9.58 KB
/
pytang.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
"""Tangram game main module.
Run this module to play the game.
NB! Pygame package is needed!
Tangram puzzle game was invented in the ancient China.
Game task is to dock together polygonal shapes
to form some funny picture (a tree, a fox, a dog etc.).
The game has minimalistic user interface. Push mouse button down
near the shape center to start shape drag-and-drop operation,
or in shape area near the shape vertex to start shape rotate operation.
Run this module such as:
python pytang.py option
where option (may be omitted) is one of the following:
-h - print this help message and exit;
-d - draw shapes in a draft mode;
-p - print shapes location after game over before exit.
TODO:
1. Shapes s.b. dockable, i.e. have a 'sticky edges';
2. Library of standard images and means to display
tham on a background are needed;
3. One of the shapes (a parallelogram shape) is
not isomorphical, so 'shape mirror' operation is
needed in UI;
4. Shapes in non-draft mode m.b. more pretty ;-)
"""
__all__ = []
import sys
import math
from math import pi as PI
import getopt
import pygame
from pygame import display, event, mouse, image, draw
from pygame import QUIT, MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION
pygame.init()
import lines
from lines import distance, inclination, vectorAB
import shapes
from shapes import Triangle, Parallelogram
#---- Global constants
# UI mode
DRAFT_MODE = False
PRINT_OUT = False
# Game display title
TITLE = "pyTang"
# Game field background color
BACKGROUND_COLOR = (0xFF, 0xFF, 0xFF)
# Shape edge color
SHAPE_COLOR = (0x00, 0x00, 0x00)
def _draw_shape_filled(surface, shape):
"""Draw the shape on the given surface.
Arguments:
surface - drawing surface (an instance of pygame.Surface class);
shape - shape to draw (an instance of one of the shape classes
defined in shapes module);
Result:
bounding box, defined during the drawing;
"""
return draw.polygon(surface, SHAPE_COLOR, shape.get_vertices())
def _draw_shape_draft(surface, shape):
"""Draw the shape on the given surface.
Arguments:
surface - drawing surface (an instance of pygame.Surface class);
shape - shape to draw (an instance of one of the shape classes
defined in shapes module);
Result:
bounding box, defined during the drawing;
"""
# Get some of the shape attributes
ref_point = shape.get_ref_point()
r_inner = shape.get_r_inner()
# Draw the shape edges
shape_bounds = draw.aalines(
surface, SHAPE_COLOR, True, shape.get_vertices()
)
# Draw the shape inner circle
draw.circle(
surface, SHAPE_COLOR,
# NB! draw.circle() expects integer arguments
map(int, ref_point), int(r_inner),
1 # circle edge width
)
# Draw the cross in the inner circle center
for (a, b) in (
((-1, 0), (+1, 0)),
((0, +1), (0, -1))
):
draw.aaline(
surface, SHAPE_COLOR,
*[
[
ref_point[coord] + r_inner / 3 * p[coord]
for coord in (0, 1)
]
for p in (a, b)
]
)
return shape_bounds
#----------------------------------------------------------------------
# Application main routine
#----------------------------------------------------------------------
def main():
#---- Prepare
# Game field size
SCREEN_W, SCREEN_H = (640, 420)
# Shape drag machine states
(ST_MOVE, ST_ROTATE, ST_NONE) = range(3)
state = ST_NONE
# Setup game window
display.set_caption(TITLE)
display.set_icon(image.load('pytang.bmp'))
# Create and setup game field
screen = display.set_mode((SCREEN_W, SCREEN_H))
screen.fill(BACKGROUND_COLOR)
# Create a game field copy for internal purposes
background = screen.copy()
# Game objects - different shapes
shapes = [
Triangle((0, 0), (60, 60), (0, 120)),
Triangle((0, 0), (60, 0), (30, 30)),
Triangle((60, 0), (120, 0), (120, 60)),
Triangle((0, 120), (60, 60), (120, 120)),
Triangle((60, 60), (90, 30), (90, 90)),
Parallelogram((60, 0), (90, 30), (60, 60)),
Parallelogram((90, 30), (120, 60), (120, 120))
]
# Game object bounding rectangles as a dictionary,
# where key is the game object instance, and
# the appropriate value is it's bounding rectangle
frames = {}
# Each shape is located in a separate cell of the game field
# at first time. The game field is divided by 3 rows and
# 3 columns, ergo, 3 x 3 = 9 cells.
# One cell size
CELL_SIZE = (SCREEN_W / 3, SCREEN_H / 3)
# Shapes initial locations as (column, row) tuples.
locations = (
(0, 0), (1, 0), (2, 1),
(2, 2), (0, 1), (0, 2),
(2, 0)
)
if DRAFT_MODE:
draw_shape = _draw_shape_draft
else:
draw_shape = _draw_shape_filled
# Move shapes to the initial locations
for i in range(len(shapes)):
shapes[i].move_to(
[
(locations[i][coord] + 0.5) * CELL_SIZE[coord]
for coord in (0, 1)
]
)
frames[shapes[i]] = draw_shape(screen, shapes[i])
#---- Event loop
do_exit = False
# Shape object which is moving or rotating now
active_shape = None
# Mouse previous position, is valid when some shape
# is rotating or moving
prev_pos = None
while not do_exit:
display.flip()
for ev in event.get():
# Game is over - user close main window
if ev.type == QUIT :
do_exit = True
break
# Mouse button is push down:
# user wants to move or rotate one of the shapes
elif (ev.type == MOUSEBUTTONDOWN) and (state == ST_NONE):
touchpoint = mouse.get_pos()
# Search for the first shape including
# the mouse button down point.
for shape in shapes:
if shape.include(touchpoint):
active_shape = shape
prev_pos = touchpoint
# If the touchpoint is laying inside the
# inner circle, the shape s.b. moved;
# the shape s.b. rotated otherwise
if (
distance(touchpoint, shape.get_ref_point())
<=
shape.get_r_inner()
):
state = ST_MOVE
else:
state = ST_ROTATE
# Take the active shape from the shapes
# common list,
# draw other shapes on the background surface.
shapes.remove(active_shape)
background.fill(BACKGROUND_COLOR)
for shape in shapes:
draw_shape(background, shape)
break
# Mouse button is up: shape moving/rotating is done
elif (ev.type == MOUSEBUTTONUP) and (state != ST_NONE) :
shapes.append(active_shape)
active_shape = None
state = ST_NONE
# Mouse is moving. If the mouse button is down
# (i.e. if we are in ST_MOVE or ST_ROTATE state),
# the active shape s.b. moved or rotated.
elif (ev.type == MOUSEMOTION) and (state != ST_NONE) :
screen.blit(
background,
frames[active_shape].topleft, frames[active_shape]
)
curr_pos = mouse.get_pos()
if state == ST_MOVE :
active_shape.move_by(vectorAB(prev_pos, curr_pos))
else:
active_shape.rotate(
inclination(
active_shape.get_ref_point(), curr_pos
)
-
inclination(
active_shape.get_ref_point(), prev_pos
)
)
prev_pos = curr_pos
frames[active_shape] = draw_shape(screen, active_shape)
# Game is over, print shapes location if needed
if PRINT_OUT:
for i in range(len(shapes)):
print "Shape %u vertices:" % (i,)
for vertex in shapes[i].get_vertices():
print "\t (%.2f, %.2f)" % vertex
#----------------------------------------------------------------------
# Application entry point
#----------------------------------------------------------------------
if __name__ == '__main__':
(opts, args) = getopt.getopt(sys.argv[1 : ], 'dhp')
for option, value in opts:
if option == '-h':
print __doc__
exit()
if option == '-d':
DRAFT_MODE = True
if option == '-p':
PRINT_OUT = True
main()