-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathfeorh.py
351 lines (316 loc) · 14.2 KB
/
feorh.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
"""
Name: feorh.py
Author: Oliver Giles & Max Potter
Date: July 2017
Description:
- Contains class definitions for game
"""
import pygame
import sys
from pygame.locals import *
from copy import deepcopy
import constants as const
import mapfuncs as mf
import creatures as c
import minds as m
import genetic_algorithm as ga
class Feorh():
"""
Feorh - Class describing a simple hunting game, played by AI.
"""
epoch = 1
width = const.WIDTH
height = const.HEIGHT
tileSize = const.TILESIZE
def __init__(self):
self.start_diagnostics()
#Initialise display
self.display, self.displayRect = start_display('Feorh')
#Create & show map
self.tilemap = mf.create_map(self.width, self.height, const.MINSEEDS,
const.MAXSEEDS)
self.tilemapMaster = deepcopy(self.tilemap) #edits to sub arrays in tilemap
#won't edit tilemapMaster
self.make_background()
#Spawn creatures
for i in range(const.TIGERPOP):
self.populate('tiger', const.TIGERPOP)
for i in range(const.DEERPOP):
self.populate('deer', const.DEERPOP)
#Set up housekeeping lists and counters
self.oldDeerPoints = [] #used for tracking past locations of deer
self.oldTigerPoints = []
self.pause = False
self.epochCounter = 0
self.timeCounter = 0
self.killTotal = 0
self.fps = const.START_FPS
self.clock = pygame.time.Clock()
return
def update(self):
"""
Limit fps, handle user input and, if not paused, run game logic.
"""
self.clock.tick(self.fps)
self.handle_user_input()
if not self.pause:
self.run_game_logic()
else:
self.run_pause_logic()
return
def run_game_logic(self):
"""
The structure of the main game loop.
Everything here is ran once per update when not paused.
"""
#************ UPDATE CREATURES
#Detect collisions between tigers and deer
self.kill_detection()
#Gather all living sprites into one list
self.cList = c.tigerList.sprites() + c.deerList.sprites()
#Update tilemap with position of all creatures
self.update_tilemap()
#Update creatures: update & supply vision, get and enact actions
for creature in self.cList:
i, j = mf.find_tile(creature, self.tileSize, self.height, self.width)
creature.get_directional_vision(i, j, self.tilemap, self.height, self.width,
self.tilemapMaster[i][j])
#Kill any creature that walks off the map.
if not self.displayRect.colliderect(creature.rect):
creature.die(deathByWall = True)
c.deerList.update()
c.tigerList.update()
#Check if there are enough tigers and deer - if not, create children
self.populate('tiger', const.TIGERPOP, len(c.tigerList),
len(ga.tGenepool), len(ga.tPregnancies))
self.populate('deer', const.DEERPOP, len(c.deerList),
len(ga.dGenepool), len(ga.dPregnancies))
#************ END OF STEP: visualisation and diagnostics
update_display(self.display, self.cList, [c.deerList, c.tigerList],
self.bgSurface)
self.diagnostics()
return
def run_pause_logic(self):
pygame.event.pump()
return
def running(self):
"""
Describes the conditions under which the game will keep running.
Returns the truth value of these conditions.
"""
return True
def quit_game():
if const.RUN_DIAGNOSTICS:
for f in self.openFiles:
f.close()
pygame.quit()
sys.exit()
return
def start_diagnostics(self):
"""
Open output files. Toggleable with RUN_DIAGNOSTICS flag in constants.
"""
if const.RUN_DIAGNOSTICS:
self.f = open('bestTigers.txt', 'w')
self.f2 = open('fitnessAndDeath.txt', 'w')
self.f.write("epoch,fitness,DNA,ID\n")
self.f2.write("time,epoch,live average fitness,"
"average fitness of breeders,%wall deaths,killTotal\n")
self.openFiles = [self.f,self.f2]
return
def make_background(self):
"""
Generate a pygame rect to represent the map, draw it to a surface.
Blit that surface to the display.
"""
self.bgSurface = pygame.Surface(self.display.get_size())
for row in range(self.height):
for column in range(self.width):
self.bgRect = pygame.draw.rect(self.bgSurface,
const.colours[self.tilemap[row][column]],
(column*self.tileSize,
row*self.tileSize,
self.tileSize,
self.tileSize))
self.bgSurface = self.bgSurface.convert()
self.display.blit(self.bgSurface,(0,0)) # blit the map to the screen
return
def populate(self, ctype, popSize, cListSize = 0, poolSize = 0,
pregListSize = 0):
"""
Check if there are enough tigers and deers. If not, create children.
"""
#Do we need to spawn a creature?
if cListSize < popSize:
if poolSize < const.GENE_POOL_SIZE:
#If genepool is small, spawn creature with randomised DNA
c.spawn_creature(ctype, mapHeight=self.height, mapWidth=self.width,
tileSize=self.tileSize)
else:
#If not, make sure DNA is ready (a 'pregnancy')
if pregListSize == 0:
#Make DNA through breeding creatures in genepool
ga.breed(ctype)
DNA = ga.get_DNA(ctype)
c.spawn_creature(ctype, mapHeight=self.height, mapWidth=self.width,
tileSize=self.tileSize, DNA=DNA)
return
def handle_user_input(self):
for event in pygame.event.get():
if event.type == QUIT:
self.quit_game()
elif event.type == pygame.MOUSEBUTTONDOWN:
#print creature's vision on mouse click
for creature in self.cList:
if creature.rect.collidepoint(event.pos):
creature.print_vision()
elif event.type == pygame.KEYDOWN:
#Increase/decrease speed of newly spawning deer on =/-
if event.key == K_EQUALS:
c.deerSpeed += 1
print "Deer speed increased to %d" % c.deerSpeed
elif event.key == K_MINUS:
c.deerSpeed = c.deerSpeed - 1 if c.deerSpeed > 0 else 0
print "Deer speed decreased to %d" % c.deerSpeed
#Raise/lower fps by 10 (never goes below 1) on ]/[
elif event.key == K_LEFTBRACKET:
self.fps = self.fps - 10 if self.fps >= 0 else 1
self.fps = self.fps if self.fps >= 1 else 1
print " * FPS = %d" % self.fps
elif event.key == K_RIGHTBRACKET:
self.fps += 10
print " * FPS = %d" % self.fps
#Pause game on space
elif event.key == K_SPACE:
if not self.pause:
print " * PAUSED * "
self.pause = True
else:
print " * RESUMED * "
self.pause = False
elif event.key == K_SLASH:
c.spawn_creature('tiger', mapHeight=self.height, mapWidth=self.width,
tileSize=self.tileSize, DNA=const.beastDNA, beastMode=True)
return
def kill_detection(self):
"""
Detect collisions between each tiger and all the deer on the map.
If there is a collision, kill the deer and feed the tiger. Record kill.
"""
for tiger in c.tigerList:
collision_list = pygame.sprite.spritecollide(tiger, c.deerList, True)
for deer in collision_list:
deer.deathByTiger = True;
# print "%s was eaten by %s!" % (col.name.rstrip(), tiger.name.rstrip())
tiger.eat(const.TIGER_EAT_ENERGY)
self.killTotal += 1
return
def update_tilemap(self):
"""
Store the position of all creatures on the tilemap. tilemapMaster
records the original tile in order to revert once a creature moves.
"""
tigerPoints = []
deerPoints = []
for creature in self.cList:
#what tile is the creature on? If it stands on a tile not previously
#visted, update creature.tiles.
i, j = mf.find_tile(creature, self.tileSize, self.height, self.width)
unique = True
for t in creature.tiles:
if i == t[0] and j == t[1]:
unique = False
break
if unique:
creature.tiles.append((i,j))
#Update tilemap to reflect what creatures are on each tiles
# * tigers trump deer and are invisible in forests.
isTiger = creature.ctype == "tiger"
isNotInWood = self.tilemapMaster[i][j] != const.WOOD
isDeer = creature.ctype == "deer"
tigerNotOnTile = self.tilemap[i][j] != const.TIGERCOLOUR
if isTiger and isNotInWood:
tigerPoints.append([i,j])
self.tilemap[i][j] = const.TIGERCOLOUR
elif isDeer and tigerNotOnTile:
self.tilemap[i][j] = const.DEERCOLOUR
deerPoints.append([i,j])
if self.tilemapMaster[i][j] == const.GRASS:
#don't forget to feed the deer!
creature.eat(const.DEER_EAT_ENERGY)
#Gather list of empty tiles by comparing old and new lists, then replace
#the tiles with their original types.
emptyTiles = [point for point in self.oldTigerPoints \
if point not in tigerPoints]
emptyTiles.extend([point for point in self.oldDeerPoints \
if point not in deerPoints])
for tile in emptyTiles:
self.tilemap[tile[0]][tile[1]] = self.tilemapMaster[tile[0]][tile[1]]
self.oldDeerPoints = deerPoints
self.oldTigerPoints = tigerPoints
return
def diagnostics(self):
"""
Collect and dump information about average fitness, rate of wall deaths
and total kills.
"""
self.timeCounter += 1
self.epochCounter += 1
if const.RUN_DIAGNOSTICS:
if self.epochCounter % 100 == 0:
#Calculate rolling average fitness values from last 50 deaths
fitnessesLength = len(c.fitnesses)
fitnessesExist = fitnessesLength > 0
liveFitness = 100 * sum(c.fitnesses) / fitnessesLength if fitnessesExist else 0.0
if fitnessesLength > 50:
del c.fitnesses[:fitnessesLength - 50]
#Record fitness of breeding pool
avBreedingFitness = 0
for t in ga.tGenepool:
avBreedingFitness += t[0]
breedersExist = len(ga.tGenepool) > 0
avBreedingFitness = avBreedingFitness/len(ga.tGenepool) if breedersExist else 0.0
#Calculate rolling average wall death rate as above
wallDeathLength = len(c.wallDeaths)
wallDeathsExist = wallDeathLength > 0
wallDeathRate = 100 * sum(c.wallDeaths) / wallDeathLength if wallDeathsExist else 0.0
if wallDeathLength > 50:
del c.wallDeaths[:wallDeathLength - 50]
self.f2.write("%d,%d,%f,%f,%f,%d\n" % (self.timeCounter,
self.epoch,
liveFitness,
avBreedingFitness,
wallDeathRate,
self.killTotal))
if self.epochCounter >= 2000: #End of epoch diagnostics
#Dump current best tiger list
newTigerCount = 0
for t in c.newBreeders:
print "New tiger added to bestTigers.txt! Fitness = %d." % t[1]
self.f.write("%d,%d,%d,%s\n" % (self.epoch, t[0], t[1], t[2]))
newTigerCount += 1
ga.epochTigers = newTigerCount
c.newBreeders = []
self.epochCounter = 0
self.epoch += 1
c.epoch = self.epoch
print ' * Epoch:', self.epoch
return
def start_display(title, w = const.WIDTH, h = const.HEIGHT,
tileSize = const.TILESIZE):
"""
Initialise pygame, open and title a window.
"""
pygame.init()
display = pygame.display.set_mode((w * tileSize, h * tileSize))
drect = display.get_rect()
pygame.display.set_caption(title)
return display, drect
def update_display(display, spriteList, groupsList, background):
display.blit(background,(0,0))
for sprite in spriteList:
display.blit(background, sprite.rect, sprite.rect)
for group in groupsList:
group.draw(display)
pygame.display.update()
return