-
Notifications
You must be signed in to change notification settings - Fork 1
/
tile.py
339 lines (262 loc) · 12.7 KB
/
tile.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
import pygame
import texture_manager
from position import Position
from draw import Draw
import copy
class Tile( object ):
@staticmethod
def create( tile_type, player ):
return tile_type_object[ tile_type ]( player )
def __init__( self, tile_type, player ):
self.__position = Position()
self.__render_position = [ 0, 0 ]
self.image = texture_manager.load( tile_image_path[ tile_type ] )
self.player = player
self.type = tile_type
self.beetle_ontop = None
# Set the tile type colour
self.image.fill( tile_type_colours[ tile_type ], special_flags = pygame.BLEND_ADD )
# Set the player colour, i can't think of a better way to do this atm...
for y in range( self.image.get_height() ):
for x in range( self.image.get_width() ):
colour = self.image.get_at( ( x, y ) )
if colour[ 0 ] == 255 and colour[ 1 ] == 255 and colour[ 2 ] == 255:
self.image.set_at( ( x, y ), player.get_colour() )
def get_position( self ):
return self.__position
def set_position( self, position, board ):
self.__position.set( position )
self.__render_position = Draw.get_render_position( self.__position )
def render( self, surface ):
Draw.image( surface, self.image, self.__position )
def adjacent_position_movement_is_wide_enough( self, board, begin, end ):
# distance between begin and end should be 1
# firstly find adjacent positions that are shared by begin and end
begin_adjacent_positions = list( begin.get_adjacent_positions() )
end_adjacent_positions = list( end.get_adjacent_positions() )
shared_adjacent_positions = [ x for x in begin_adjacent_positions if x in end_adjacent_positions ]
assert len( shared_adjacent_positions ) == 2
# if both shared positions are occupied then we cannot move between them
# i.e. if either position is free, then we can pass
if len( board.get_tiles_with_position( shared_adjacent_positions[0] ) ) == 0 or \
len( board.get_tiles_with_position( shared_adjacent_positions[1] ) ) == 0:
return True
return False
def position_is_reachable( self, board, target_position, free_positions ):
# we need to walk from get_position() to target_position
# whilst checking that the pieces either side of each position on the route...
#...are large enough for us to fit through
# it's a stupid recurrsive search but does the job (i think)
def can_create_path_between( begin, end, visited_nodes ):
adjacent_positions_to_begin = begin.get_adjacent_positions()
adjacent_free_positions_to_begin = [ x for x in adjacent_positions_to_begin if x in free_positions ]
# calculate if we can move to adjacent position
valid_positions = [ x for x in adjacent_free_positions_to_begin \
if self.adjacent_position_movement_is_wide_enough( board, begin, x ) \
and x not in visited_nodes ]
# Find positions that have an adjacent tile that is not the current tile
valid_positions_with_adjacent_tile = []
for position in valid_positions:
adjacent_tiles = [ x for x in board.get_adjacent_tiles( position ) if x != self ]
if len( adjacent_tiles ) > 0:
valid_positions_with_adjacent_tile.append( position )
visited_nodes.extend( valid_positions_with_adjacent_tile )
if end in valid_positions_with_adjacent_tile:
return True
for position in valid_positions_with_adjacent_tile:
if can_create_path_between( position, end, visited_nodes ):
return True
return False
return can_create_path_between( self.get_position(), target_position, [] )
def has_occupied_shared_tile( self, board, a, b, apart_from = [] ):
a_adjacent_positions = list( a.get_adjacent_positions() )
b_adjacent_positions = list( b.get_adjacent_positions() )
shared_adjacent_positions = [ x for x in a_adjacent_positions if x in b_adjacent_positions ]
assert len( shared_adjacent_positions ) == 2
shared_adjacent_tiles = [
len( [ x for x in board.get_tiles_with_position( shared_adjacent_positions[0] ) if x not in apart_from ] ),
len( [ x for x in board.get_tiles_with_position( shared_adjacent_positions[1] ) if x not in apart_from ] )
]
return shared_adjacent_tiles.count( 0 ) == 1
class Ant( Tile ):
def __init__( self, player ):
super( Ant, self ).__init__( TileType.ant, player )
def get_valid_positions( self, board ):
# can be placed anywhere that's free and touching another piece
# ant needs to be able to 'walk' into the space...
#...sometimes a gap might be too small for it to enter into (lol)
free_positions = board.get_unoccupied_positions()
reachable_positions = [ x for x in free_positions if self.position_is_reachable( board, x, free_positions ) ]
return reachable_positions
class Bee( Tile ):
def __init__( self, player ):
super( Bee, self ).__init__( TileType.bee, player )
def get_valid_positions( self, board ):
# valid positions for bee are:
# * 1 tile away
# * not already occupied
# * touching another tile (if we 'remove' the current bee tile)
# * 'reachable' - i.e. wide enough to be walked into
# there also needs to be a tile that is adjacently shared by both the start and end position...
#...this is so we dont 'jump' over a cave-like gap
adjacent_positions = self.get_position().get_adjacent_positions()
free_adjacent_positions = [ x for x in adjacent_positions if len( board.get_tiles_with_position( x ) ) == 0 ]
reachable_free_adjacent_positions = \
[ x for x in free_adjacent_positions if self.adjacent_position_movement_is_wide_enough( board, self.get_position(), x ) ]
reachable_free_adjacent_positions = [ x for x in reachable_free_adjacent_positions \
if self.has_occupied_shared_tile( board, self.get_position(), x, [ self ] ) ]
def movement_creates_an_island( end ):
tiles = list( board.tiles )
start_position = copy.deepcopy( self.get_position() )
self.set_position( end, board )
connected_hive = board.hive_is_connected( tiles )
self.set_position( start_position, board )
return not connected_hive
return [ x for x in reachable_free_adjacent_positions if not movement_creates_an_island( x ) ]
class Beetle( Tile ):
def __init__( self, player ):
self.tile_underneith = None
super( Beetle, self ).__init__( TileType.beetle, player )
def get_valid_positions( self, board ):
# valid positions for beetle are:
# * 1 tile away
# * can be already occupied (we just jump on top)
# * touching another tile (if we 'remove' the current bee tile)
# * 'reachable' - i.e. wide enough to be walked into
# there also needs to be a tile that is adjacently shared by both the start and end position...
#...this is so we dont 'jump' over a cave-like gap
adjacent_positions = self.get_position().get_adjacent_positions()
# For tiles that are already taken, we can move there no problem...
# For target positions that have no tile attached, we need to check that moving here doesn't create an island
def movement_creates_an_island( end ):
tiles = list( board.tiles )
start_position = copy.deepcopy( self.get_position() )
self.set_position( end, board )
connected_hive = board.hive_is_connected( tiles )
self.set_position( start_position, board )
return not connected_hive
valid_positions = [ x for x in adjacent_positions \
if not movement_creates_an_island( x ) and \
self.has_occupied_shared_tile( board, self.get_position(), x ) ]
return valid_positions
def set_position( self, position, board ):
# Remove the beetle reference from the tile that we're moving from (if there is one)
if self.tile_underneith is not None:
self.tile_underneith.beetle_ontop = None
# Find the tiles we're ontop of, then find the highest one
tiles_ontop_of = board.get_tiles_with_position( position )
self.tile_underneith = next( ( x for x in tiles_ontop_of if x.beetle_ontop == None and x is not self ), None )
# If we're not ontop of a tile, update the reference to this beetle
if self.tile_underneith is not None:
self.tile_underneith.beetle_ontop = self
super( Beetle, self ).set_position( position, board )
self.create_scaled_image()
def create_scaled_image( self ):
scale_factor = 1
tile_underneith = self.tile_underneith
while tile_underneith is not None:
scale_factor *= 0.75
if tile_underneith.type == TileType.beetle:
assert tile_underneith != tile_underneith.tile_underneith
tile_underneith = tile_underneith.tile_underneith
else:
tile_underneith = None
self.__scaled_image = pygame.transform.scale( self.image,
[ int( self.image.get_width() * scale_factor ), int( self.image.get_height() * scale_factor ) ] )
self.__render_position = Draw.get_render_position( self.get_position() )
self.__render_position[ 0 ] += ( self.image.get_width() - self.__scaled_image.get_width() ) / 2
self.__render_position[ 1 ] += ( self.image.get_height() - self.__scaled_image.get_height() ) / 2
def render( self, surface ):
Draw.image_explicit( surface, self.__scaled_image, *self.__render_position )
class GrassHopper( Tile ):
def __init__( self, player ):
super( GrassHopper, self ).__init__( TileType.grass_hopper, player )
def get_valid_positions( self, board ):
# Grass hopper moves along edges until it reaches the first blank square
# If there is a blank square right next to it, then it can't jump in that direction
def find_first_blank_square( position, callback ):
if len( board.get_tiles_with_position( position ) ) == 0:
return position
return find_first_blank_square( callback( position ), callback )
jumpable_positions = [
find_first_blank_square( self.get_position(), lambda position : position.east() ),
find_first_blank_square( self.get_position(), lambda position : position.west() ),
find_first_blank_square( self.get_position(), lambda position : position.north_west() ),
find_first_blank_square( self.get_position(), lambda position : position.north_east() ),
find_first_blank_square( self.get_position(), lambda position : position.south_west() ),
find_first_blank_square( self.get_position(), lambda position : position.south_east() ),
]
# If one of the jumpable positions is directly adjacent then ignore it
non_adjacent_jumpable_positions = [ x for x in jumpable_positions \
if x not in self.get_position().get_adjacent_positions() ]
return non_adjacent_jumpable_positions
class Spider( Tile ):
def __init__( self, player ):
super( Spider, self ).__init__( TileType.spider, player )
def get_valid_positions( self, board ):
# Spider movement is three tiles away from self
# Tiles must be free
# Can 'jump' gaps (might be tricky to create a test case for this)
# Must not double back on itself (i think)
free_positions = board.get_unoccupied_positions()
reachable_positions = [ x for x in free_positions if self.position_is_reachable( board, x, free_positions ) ]
# Algorithm adapted from here: http://stackoverflow.com/a/58446
def routes_between( begin, end ):
routes = []
def find_routes_between( begin, end, visited ):
if len( visited ) == 0:
visited.append( begin )
adjacent_positions = visited[ len( visited ) - 1 ].get_adjacent_positions()
free_adjacent_positions = [ x for x in adjacent_positions \
if len( board.get_tiles_with_position( x ) ) == 0 and
board.touching_any( x, [ self ] ) and
self.has_occupied_shared_tile( board, visited[ len( visited ) - 1 ], x, [ self ] ) ]
unvisted_free_adjacent_positions = [ x for x in free_adjacent_positions if x not in visited ]
for position in unvisted_free_adjacent_positions:
if position == end:
visited.append( position )
routes.append( visited[1:] )
visited.pop()
for position in unvisted_free_adjacent_positions:
if position == end:
continue
visited.append( position )
if len( visited ) < 4:
find_routes_between( begin, end, visited )
visited.pop()
find_routes_between( begin, end, [] )
return routes
positions_with_routes_of_length_3 = []
for reachable_position in reachable_positions:
routes = list( routes_between( self.get_position(), reachable_position ) )
route_lengths = [ len( route ) for route in routes ]
if 3 in route_lengths:
positions_with_routes_of_length_3.append( reachable_position )
return positions_with_routes_of_length_3
class TileType:
ant = 0
bee = 1
beetle = 2
grass_hopper = 3
spider = 4
tile_image_path = {
TileType.ant : "images/ant.png",
TileType.bee : "images/bee.png",
TileType.beetle : "images/beetle.png",
TileType.grass_hopper : "images/grass_hopper.png",
TileType.spider : "images/spider.png",
}
tile_type_object = {
TileType.ant : Ant,
TileType.bee : Bee,
TileType.beetle : Beetle,
TileType.grass_hopper : GrassHopper,
TileType.spider : Spider,
}
tile_type_colours = {
TileType.ant : ( 0, 148, 255 ),
TileType.bee : ( 255, 185, 0 ),
TileType.beetle : ( 224, 0, 255 ),
TileType.grass_hopper : ( 40, 220, 40 ),
TileType.spider : ( 144, 0, 7 ),
}