這一節的標題是
9.6 Evolving Forces: Smart Rockets
因為方格子標題字數限制,所以沒完整顯現
因超過方格子內容長度限制,故分成上、下兩部分
Making Improvements
到目前為止,我們已經讓火箭具有演化的能力,不管目標物的位置怎麼改變,都能夠自動調整飛行路線,朝目標物飛過去。這樣子的火箭雖然厲害,不過還是可以讓它再厲害一些。本節一開始的時候提到,在Thorp的範例中,火箭和目標物之間有許多障礙物,而火箭藉由演化的方式,可以自己找出避開障礙物的路線。接下來,就來升級我們的火箭,讓我們的火箭也擁有避開障礙物的能力。
在升級火箭之前,得先製作障礙物。為了方便處理,我們用矩形來當作障礙物。所以,描述障礙物的類別可以這樣設計:class Obstacle:
def __init__(self, x, y, w, h):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
self.position = pygame.Vector2(x, y)
self.width = w
self.height = h
self.rect = pygame.Rect(x, y, w, h)
def show(self):
pygame.draw.rect(self.screen, (100, 100, 100), self.rect)
火箭在飛行途中有可能會擊中障礙物,所以在Obstacle類別裡,我們加入一個contains()方法,可以用來檢查某個點是否落在障礙物裡頭。當想要確認火箭是不是擊中障礙物時,就用這個方法來檢查火箭的位置是不是落在障礙物裡頭;如果是的話,就代表火箭擊中了障礙物。
def contains(self, spot):
return self.rect.collidepoint(tuple(spot))
設計好描述障礙物的Obstacle類別之後,就可以來升級Rocket類別了。
由於障礙物可能不只一個,所以對於火箭而言,必須逐一檢查各個障礙物,才能知道是不是有擊中障礙物的情況發生。如果火箭擊中了任何一個障礙物,就把hit_obstacle這個屬性設定為True。
def check_obstacles(self, obstacles):
for obstacle in obstacles:
if obstacle.contains(self.position):
self.hit_obstacle = True
當火箭擊中障礙物時,就停止更新其位置,讓它留在原地,所以把run()方法改成
def run(self, obstacles):
if not self.hit_obstacle:
self.apply_force(self.dna.genes[self.gene_counter])
self.gene_counter += 1
self.update()
self.check_obstacles(obstacles)
self.show()
火箭擊中障礙物,代表適應度極差,現有適應度的值失準;所以碰到這種情況時,應該要大幅調降算出來的適應度值,這樣才能反映出真實的狀況。說得直白一點,就是表現不好就罰的意思啦。
# 擊中障礙物要罰
if self.hit_obstacle:
self.fitness *= 0.1
在Example 9.2中,當火箭結束飛行時才會去計算適應度。這會有個問題:如果火箭在飛行途中一度非常接近目標物,但因飛行時間還沒結束,所以會繼續飛而飛過頭。因為適應度是根據火箭和目標物間的距離來計算的,所以如果火箭飛過頭,即便它曾經很接近過目標物,那算出來的適應度也不會因為這樣就變得比較好。換句話說,這樣子的做法,實際上可能會變成是在懲罰比較快飛近目標物的火箭,而讓飛得慢、穩紮穩打的火箭比較容易勝出。
如果想讓比較快飛近目標物的火箭比較容易勝出,那要怎麼做呢?這可以從時間和空間兩方面來考慮;時間指的是火箭飛了多少時間才擊中目標物,而空間則是指火箭在飛行過程中,最接近目標物的距離有多大。很顯然的,時間越短、距離越近,則火箭的適應度應該要越大,這樣才能達到我們的目的。因此,火箭的適應度計算方式,可以設定成
fitness = 1 / (擊中目標物所花的時間×離目標物最近的距離)
有了計算火箭適應度的公式之後,接下來就是要把公式中的「擊中目標物所花的時間」、「離目標物最近的距離」這兩個值給找出來。
要想知道火箭擊中目標物所花的時間,首要之務,就是要能夠判斷火箭是不是擊中了目標物。要知道火箭是否擊中目標物,方法很簡單,就把目標物當作是一種障礙物就可以了。這也就是說,如果設定目標物是Obstacle類別的實例,那就可以用contains()方法來判斷火箭是否擊中目標物了。我們在Rocket類別中,新增一個check_target()方法,用來判斷並記錄火箭是否擊中目標物:
def check_target(self, target):
if target.contains(self.position):
self.hit_target = True
在每幀畫面中,都會執行check_target()方法。因此,用個計數器來計數,執行了check_target()方法多少次之後,火箭才擊中目標,這樣就可以知道火箭擊中目標物所花的時間了;這可以在check_target()方法中加入下列程式來辦到:
if not self.hit_target:
self.finish_counter += 1
找出火箭擊中目標物所花的時間之後,再來就是要找出火箭離目標物最近的距離。同樣的,在check_target()方法中加入一段程式就可以了:
distance = self.position.distance_to(target.position)
if distance < self.record_distance:
self.record_distance = distance
所以,完整的check_target()方法會長這樣:
def check_target(self, target):
distance = self.position.distance_to(target.position)
if distance < self.record_distance:
self.record_distance = distance
if target.contains(self.position):
self.hit_target = True
if not self.hit_target:
self.finish_counter += 1
至於計算適應度的calculate_fitness()方法,則設計成這樣:
def calculate_fitness(self):
self.fitness = 1/(self.finish_counter*self.record_distance)
self.fitness **= 4
# 擊中障礙物要罰
if self.hit_obstacle:
self.fitness *= 0.1
# 擊中目標物要賞
if self.hit_target:
self.fitness *= 2
在這裡,我們改用4次式來計算適應度,試看看效果如何。另外,既然擊中障礙物要罰,那擊中目標物當然要賞囉!所以,如果火箭擊中了目標物,那就將算出的適應度值翻倍,讓它在GA的挑選步驟中,更容易雀屏中選。
依照上述升級Rocket類別的說明,升級後的Rocket類別程式碼如下:
class Rocket:
def __init__(self, x, y, dna, size=24, mass=1):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
# 讓傳遞進來的數值來決定火箭的質量
self.mass = mass
# 火箭的大小,長 x 寬 = size x size/2
self.size = size
# 火箭的初始位置、初始速度、初始加速度
self.position = pygame.Vector2(x, y)
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# 設定火箭所在surface的格式為per-pixel alpha,並在上面畫出火箭
self.surface = pygame.Surface((self.size, self.size/2), pygame.SRCALPHA)
rocket_color = (100, 100, 100, 200)
body = [(self.size//5, 0), (self.size//5, self.size//2), (self.size, self.size//4)]
pygame.draw.polygon(self.surface, rocket_color, body)
# 推進器圖案座標為(0, self.size//4-self.size//8), (0, self.size//4+self.size//8),
# (self.size//5+self.size//4, self.size//4)
thruster = [(0, self.size//8), (0, 3*self.size//8), (9*self.size//20, self.size//4)]
pygame.draw.polygon(self.surface, rocket_color, thruster)
self.dna = dna
self.fitness = 0
self.gene_counter = 0
self.hit_obstacle = False
self.hit_target = False
self.record_distance = float('inf')
self.finish_counter = 0
def apply_force(self, force):
self.acceleration += force/self.mass
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.acceleration *= 0
def show(self):
# 旋轉surface,讓火箭面朝前進方向
heading = math.atan2(self.velocity.y, self.velocity.x)
rotated_surface = pygame.transform.rotate(self.surface, -math.degrees(heading))
rect_new = rotated_surface.get_rect(center=self.position)
# 把火箭所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, rect_new)
def calculate_fitness(self):
self.fitness = 1/(self.finish_counter*self.record_distance)
self.fitness **= 4
# 擊中障礙物要罰
if self.hit_obstacle:
self.fitness *= 0.1
# 擊中目標物要賞
if self.hit_target:
self.fitness *= 2
def check_obstacles(self, obstacles):
for obstacle in obstacles:
if obstacle.contains(self.position):
self.hit_obstacle = True
def check_target(self, target):
distance = self.position.distance_to(target.position)
if distance < self.record_distance:
self.record_distance = distance
if target.contains(self.position):
self.hit_target = True
if not self.hit_target:
self.finish_counter += 1
def run(self, obstacles):
if (not self.hit_obstacle) and (not self.hit_target) :
self.apply_force(self.dna.genes[self.gene_counter])
self.gene_counter += 1
self.update()
self.check_obstacles(obstacles)
self.show()
Population類別也須新增、修改部分內容;完整的程式碼如下:
class Population:
def __init__(self, population_size, mutation_rate, life_span):
self.population_size = population_size
self.mutation_rate = mutation_rate
self.dna_length = life_span
# 發射場位置
screen = pygame.display.get_surface()
self.width, self.height = screen.get_size()
self.launch_site = pygame.Vector2(self.width//2, self.height+20)
self.population = [Rocket(self.launch_site.x, self.launch_site.y, DNA(self.dna_length)) for _ in range(population_size)]
self.generations = 0
def calculate_fitness(self):
for rocket in self.population:
rocket.calculate_fitness()
def evolve(self):
weights = [rocket.fitness for rocket in self.population]
next_generation = []
for i in range(self.population_size):
[parentA, parentB] = random.choices(self.population, weights, k=2)
child = parentA.dna.crossover(parentB.dna)
child.mutate(self.mutation_rate)
next_generation.append(Rocket(self.launch_site.x, self.launch_site.y, child))
self.population = next_generation
self.generations += 1
def get_generations(self):
return self.generations
def live(self, target, obstacles):
for rocket in self.population:
rocket.check_target(target)
rocket.run(obstacles)
def target_reached(self):
for rocket in self.population:
if rocket.hit_target:
return True
return False
DNA類別完全不需更動,不再列出。至於新增的Obstacle類別,完整的程式碼如下:
class Obstacle:
def __init__(self, x, y, w, h):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
self.position = pygame.Vector2(x, y)
self.width = w
self.height = h
self.rect = pygame.Rect(x, y, w, h)
def contains(self, spot):
return self.rect.collidepoint(tuple(spot))
def show(self):
pygame.draw.rect(self.screen, (100, 100, 100), self.rect)
接下來,就來讓升級版的火箭飛一飛,看看效果如何。
Example 9.3: Smarter Rockets
下面是三張模擬過程的截圖。從截圖可以看出來,火箭從一開始的四處亂竄,經過不斷地演化,自己進化成能夠避開障礙物而擊中目標。這就是演化式運算的威力!



主程式如下:
# python version 3.10.9
import math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 9.2: Smart Rockets")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 製造火箭族群
population_size = 150
mutation_rate = 0.01
life_span = 250
population = Population(population_size, mutation_rate, life_span)
font14 = pygame.font.SysFont('courier', 14)
target = Obstacle(WIDTH//2-12, 24, 24, 24)
obstacles = [Obstacle(WIDTH//2-75, HEIGHT//2, 150, 10)]
life_counter = 0
record_time = life_span
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 火箭存活life_span幀畫面之後才會進行演化
if life_counter < life_span:
population.live(target, obstacles)
if population.target_reached() and life_counter < record_time:
record_time = life_counter
life_counter += 1
else:
life_counter = 0
population.calculate_fitness()
population.evolve()
string = f'{"Generation #:"}{population.get_generations():>3}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 20))
string = f'{"Cycles left:"}{life_span-life_counter:>4}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 40))
string = f'{"Record cycles:"}{record_time:>4}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 60))
# 按滑鼠左鍵可更動目標物位置至滑鼠指標處
if pygame.mouse.get_pressed()[0]:
x, y = pygame.mouse.get_pos()
target = Obstacle(x, y, 24, 24)
target.show()
for obstacle in obstacles:
obstacle.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 9.9
在障礙物分佈比較複雜的環境中,不需調整GA的設定,火箭依然可以演化出能夠飛抵目標物的路徑;只不過所需的世代數會增加就是了。例如,如果目標物和障礙物的配置為
target = Obstacle(WIDTH//2-12, 24, 24, 24)
obstacle1 = Obstacle(WIDTH//2-85, HEIGHT//2, 100, 10)
obstacle2 = Obstacle(WIDTH//2+45, HEIGHT//2, 50, 10)
obstacle3 = Obstacle(WIDTH//2-15, HEIGHT//2-50, 75, 10)
obstacles = [obstacle1, obstacle2, obstacle3]
在完全沒有調整GA設定以及火箭性能的情形下,演化了超過250代,才有火箭擊中目標;跟Example 9.3比起來,多了非常多。



Exercise 9.10
基因型,也就是DNA類別的genes,前5個元素為推進器,剩下的部分則為推進器作動序列。交配時,推進器部分和推進器動作序列部分視為兩組不同之基因,需分開處理。
燃料消耗計算方式:每1單位推力強度持續作用1幀畫面,會消耗1單位燃料。
燃料耗盡之火箭停止飛行,並在箭體中心標註紅色圓點;計算適應度時略施薄懲,將適應度降低50%。
一開始有許多火箭耗盡燃料,但隨著演化的進行,這種情況越來越少。



改變目標物位置後,火箭很快就演化出新的飛行路線。

程式部分,未更動之類別、方法略過不列出。
class Population:
def __init__(self, population_size, mutation_rate, life_span, fuel_amount):
self.population_size = population_size
self.mutation_rate = mutation_rate
# dna長度為5個推進器加上推進器作動序列
self.dna_length = 5 + life_span
# 發射場位置
screen = pygame.display.get_surface()
self.width, self.height = screen.get_size()
self.launch_site = pygame.Vector2(self.width//2, self.height+20)
self.fuel_amount = fuel_amount
self.population = [Rocket(self.launch_site.x, self.launch_site.y, DNA(self.dna_length), fuel_amount) for _ in range(population_size)]
self.generations = 0
def calculate_fitness(self):
略
def evolve(self):
weights = [rocket.fitness for rocket in self.population]
next_generation = []
for i in range(self.population_size):
[parentA, parentB] = random.choices(self.population, weights, k=2)
child = parentA.dna.crossover(parentB.dna)
child.mutate(self.mutation_rate)
rocket = Rocket(self.launch_site.x, self.launch_site.y, child, self.fuel_amount)
next_generation.append(rocket)
self.population = next_generation
self.generations += 1
def get_generations(self):
略
def live(self, target, obstacles):
略
def target_reached(self):
略
class DNA:
def __init__(self, length):
self.length = length
self.max_force = 0.1
self.genes = []
# 前5個為推進器
for _ in range(5):
r = random.uniform(0, self.max_force)
theta = random.uniform(0, 360)
# 推進器推力大小介於0~最大出力,方向為任意方向
thruster = pygame.Vector2.from_polar((r, theta))
self.genes.append(thruster)
# 推進器作動序列
self.genes += random.choices([0, 1, 2, 3, 4], k=self.length-5)
def crossover(self, partner):
child = DNA(self.length)
# 推進器部分
crossover_point = random.randint(0, 4)
# 在crossover_point之前的基因來自此DNA,之後的基因則來自partner這個DNA
for i in range(5):
if i < crossover_point:
child.genes[i] = self.genes[i]
else:
child.genes[i] = partner.genes[i]
# 推進器作動序列部分
crossover_point = random.randint(5, self.length-1)
# 在crossover_point之前的基因來自此DNA,之後的基因則來自partner這個DNA
for i in range(5, self.length):
if i < crossover_point:
child.genes[i] = self.genes[i]
else:
child.genes[i] = partner.genes[i]
return child
def mutate(self, mutation_rate):
for i in range(self.length):
if random.random() < mutation_rate:
if i <= 4:
# 推進器產生突變
r = random.uniform(0, self.max_force)
theta = random.uniform(0, 360)
# 推進器推力大小介於0~最大出力,方向為任意方向
thruster = pygame.Vector2.from_polar((r, theta))
self.genes[i] = thruster
else:
# 推進器作動序列產生突變
self.genes[i] = random.randint(0, 4)
class Rocket:
def __init__(self, x, y, dna, fuel_amount, size=24, mass=1):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
# 讓傳遞進來的數值來決定火箭的質量
self.mass = mass
# 火箭的大小,長 x 寬 = size x size/2
self.size = size
# 火箭的初始位置、初始速度、初始加速度
self.position = pygame.Vector2(x, y)
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# 設定火箭所在surface的格式為per-pixel alpha,並在上面畫出火箭
self.surface = pygame.Surface((self.size, self.size/2), pygame.SRCALPHA)
rocket_color = (100, 100, 100, 200)
body = [(self.size//5, 0), (self.size//5, self.size//2), (self.size, self.size//4)]
pygame.draw.polygon(self.surface, rocket_color, body)
# 推進器圖案座標為(0, self.size//4-self.size//8), (0, self.size//4+self.size//8),
# (self.size//5+self.size//4, self.size//4)
thruster = [(0, self.size//8), (0, 3*self.size//8), (9*self.size//20, self.size//4)]
pygame.draw.polygon(self.surface, rocket_color, thruster)
self.dna = dna
self.fitness = 0
self.gene_counter = 5
self.hit_obstacle = False
self.hit_target = False
self.record_distance = float('inf')
self.finish_counter = 0
self.fuel_exhausted = False
self.fuel_guage = fuel_amount
def apply_force(self, force):
略
def update(self):
略
def show(self):
# 旋轉surface,讓火箭面朝前進方向
heading = math.atan2(self.velocity.y, self.velocity.x)
rotated_surface = pygame.transform.rotate(self.surface, -math.degrees(heading))
rect_new = rotated_surface.get_rect(center=self.position)
# 把火箭所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, rect_new)
# 燃料耗盡,在箭體中心標註紅色圓點
if self.fuel_exhausted:
pygame.draw.circle(self.screen, (255, 0, 0), self.position, self.size/8)
def calculate_fitness(self):
self.fitness = 1/(self.finish_counter*self.record_distance)
self.fitness **= 4
# 擊中障礙物要罰
if self.hit_obstacle:
self.fitness *= 0.1
# 擊中目標物要賞
if self.hit_target:
self.fitness *= 2
# 燃料耗盡,略施薄懲
if self.fuel_exhausted:
self.fitness *= 0.5
def check_obstacles(self, obstacles):
略
def check_target(self, target):
略
def run(self, obstacles):
thruster_id = self.dna.genes[self.gene_counter]
thrust = self.dna.genes[thruster_id]
self.check_fuel(thrust)
if (not self.hit_obstacle) and (not self.hit_target) and (not self.fuel_exhausted):
self.apply_force(thrust)
self.gene_counter += 1
self.update()
self.check_obstacles(obstacles)
self.show()
def check_fuel(self, thrust):
self.fuel_guage -= thrust.length()
if self.fuel_guage <= 0:
self.fuel_exhausted = True
class Obstacle:
略
主程式
# python version 3.10.9
import math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 9.10")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 700, 500
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 製造火箭族群
population_size = 150
mutation_rate = 0.01
life_span = 250
fuel_amount = 10
population = Population(population_size, mutation_rate, life_span, fuel_amount)
font14 = pygame.font.SysFont('courier', 14)
target = Obstacle(WIDTH//2-12, 24+260, 24, 24)
obstacles = [Obstacle(WIDTH//2-75, 24+260+96, 150, 10)]
life_counter = 0
record_time = life_span
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 歷經life_span幀畫面之後進行演化
if life_counter < life_span:
population.live(target, obstacles)
if population.target_reached() and life_counter < record_time:
record_time = life_counter
life_counter += 1
else:
life_counter = 0
population.calculate_fitness()
population.evolve()
string = f'{"Generation #:"}{population.get_generations():>3}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 20))
string = f'{"Cycles left:"}{life_span-life_counter:>4}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 40))
string = f'{"Record cycles:"}{record_time:>4}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 60))
# 按滑鼠左鍵可更動目標物位置至滑鼠指標處
if pygame.mouse.get_pressed()[0]:
x, y = pygame.mouse.get_pos()
target = Obstacle(x, y, 24, 24)
target.show()
for obstacle in obstacles:
obstacle.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 9.11


參考4.4節的Exercise 4.4,在推進器尾端加入噴煙效果。
修改Rocket類別。在__init__()方法中,新增
# 飛行路徑
self.course = [self.position.copy()]
# 推進器煙霧產生器
self.emitter = Emitter(0, 0, 0.3)
在update()方法加入記錄飛行路徑功能
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.acceleration *= 0
# 記錄飛行路徑
self.course.append(self.position.copy())
在show()方法加入推進器噴煙效果
def show(self):
# 旋轉surface,讓火箭面朝前進方向
heading = math.atan2(self.velocity.y, self.velocity.x)
rotated_surface = pygame.transform.rotate(self.surface, -math.degrees(heading))
rect_new = rotated_surface.get_rect(center=self.position)
# 把火箭所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, rect_new)
# 顯示推進器煙霧
heading_vec = self.velocity.normalize()
self.emitter.origin = self.position - (self.size/2)*heading_vec
self.emitter.add_particle()
self.emitter.particles[-1].velocity = -0.5*heading_vec
self.emitter.particles[-1].lifespan = 50
self.emitter.run()
主程式部分,在演化至下一代之前,先檢查是否有火箭擊中目標。若有火箭擊中目標,則進行最短路徑修正工作。
# python version 3.10.9
import math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 9.11")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 製造火箭族群
population_size = 150
mutation_rate = 0.01
life_span = 250
population = Population(population_size, mutation_rate, life_span)
font14 = pygame.font.SysFont('courier', 14)
target = Obstacle(WIDTH//2-12, 24, 24, 24)
obstacles = [Obstacle(WIDTH//2-75, HEIGHT//2, 150, 10)]
life_counter = 0
record_time = life_span
gravity = pygame.Vector2(0, 0)
shortest_path = []
shortest_distance = float('inf')
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 火箭存活life_span幀畫面之後才會進行演化
if life_counter < life_span:
population.live(target, obstacles)
if population.target_reached() and life_counter < record_time:
record_time = life_counter
life_counter += 1
else:
# 找出最短路徑
for rocket in population.population:
if rocket.hit_target:
distance = 0
for i in range(1, len(rocket.course)-1):
distance += rocket.course[i+1].distance_to(rocket.course[i])
if distance < shortest_distance:
shortest_distance = distance
shortest_path = rocket.course.copy()
# 演化至下一代
life_counter = 0
population.calculate_fitness()
population.evolve()
# 畫最短路徑
if shortest_path:
pygame.draw.lines(screen, (255, 0, 0), False, shortest_path)
string = f'{"Generation #:"}{population.get_generations():>3}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 20))
string = f'{"Cycles left:"}{life_span-life_counter:>4}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 40))
string = f'{"Record cycles:"}{record_time:>4}'
text = font14.render(string, True, (0, 0, 0))
screen.blit(text, (5, 60))
# 按滑鼠左鍵可更動目標物位置至滑鼠指標處
if pygame.mouse.get_pressed()[0]:
x, y = pygame.mouse.get_pos()
target = Obstacle(x, y, 24, 24)
target.show()
for obstacle in obstacles:
obstacle.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 9.12
修改DNA類別。
class DNA:
def __init__(self, rows, cols, resolution):
self.rows = rows
self.cols = cols
self.resolution = resolution
self.length = self.rows*self.cols
# 基因型是流場向量
self.genes = [None]*self.length
for i in range(self.length):
theta = random.uniform(0, 360)
self.genes[i] = pygame.Vector2.from_polar((1, theta))
def crossover(self, partner):
child = DNA(self.rows, self.cols, self.resolution)
crossover_point = random.randint(0, self.length-1)
# 在crossover_point之前的基因來自此DNA,之後的基因則來自partner這個DNA
for i in range(self.length):
if i < crossover_point:
child.genes[i] = self.genes[i]
else:
child.genes[i] = partner.genes[i]
return child
def mutate(self, mutation_rate):
for i in range(self.length):
if random.random() < mutation_rate:
theta = random.uniform(0, 360)
self.genes[i] = pygame.Vector2.from_polar((1, theta))
Rocket類別新增look_field_up()方法:
def look_field_up(self):
x, y = self.position
column = int(x//self.dna.resolution)
if column < 0:
column = 0
elif column > self.dna.cols-1:
column = self.dna.cols-1
row = int(y//self.dna.resolution)
if row < 0:
row = 0
elif row > self.dna.rows-1:
row = self.dna.rows-1
return self.dna.genes[row*self.dna.cols+column]
並修改run()方法:
def run(self, obstacles):
if (not self.hit_obstacle) and (not self.hit_target):
force = 0.1*self.look_field_up()
self.apply_force(force)
self.update()
self.check_obstacles(obstacles)
self.show()
Population類別的__init__()方法也須修改;完整程式碼如下:
def __init__(self, population_size, mutation_rate, resolution):
self.population_size = population_size
self.mutation_rate = mutation_rate
# 發射場位置
screen = pygame.display.get_surface()
self.width, self.height = screen.get_size()
self.launch_site = pygame.Vector2(self.width//2, self.height+20)
# 流場大小
self.resolution = resolution
self.cols = self.width//self.resolution
self.rows = self.height//self.resolution
# 建立族群
self.population = [None]*self.population_size
for i in range(self.population_size):
dna = DNA(self.rows, self.cols, self.resolution)
self.population[i] = Rocket(self.launch_site.x, self.launch_site.y, dna)
self.generations = 0




















