adjust the weights

This commit is contained in:
Kristofers Solo 2024-01-03 16:35:00 +02:00
parent b10c90cbdf
commit cb54859b6c
8 changed files with 102 additions and 48 deletions

Binary file not shown.

View File

@ -1,14 +1,14 @@
[NEAT] [NEAT]
fitness_criterion = mean fitness_criterion = max
fitness_threshold = 400 fitness_threshold = 32768
pop_size = 1000 pop_size = 5000
reset_on_extinction = False reset_on_extinction = False
[DefaultGenome] [DefaultGenome]
# node activation options # node activation options
activation_default = relu activation_default = sigmoid
activation_mutate_rate = 1.0 activation_mutate_rate = 0.0
activation_options = relu activation_options = sigmoid
# node aggregation options # node aggregation options
aggregation_default = sum aggregation_default = sum
@ -44,8 +44,8 @@ node_add_prob = 0.2
node_delete_prob = 0.2 node_delete_prob = 0.2
# network parameters # network parameters
num_hidden = 2 num_hidden = 4
num_inputs = 17 num_inputs = 16
num_outputs = 4 num_outputs = 4
# node response options # node response options

View File

@ -1,9 +1,12 @@
import random
import time import time
import neat import neat
from loguru import logger from loguru import logger
from py2048 import Menu from py2048 import Menu
from .fitness import calculate_fitness
def eval_genomes(genomes, config: neat.Config): def eval_genomes(genomes, config: neat.Config):
app = Menu() app = Menu()
@ -12,22 +15,17 @@ def eval_genomes(genomes, config: neat.Config):
for genome_id, genome in genomes: for genome_id, genome in genomes:
genome.fitness = 0 genome.fitness = 0
net = neat.nn.FeedForwardNetwork.create(genome, config) net = neat.nn.FeedForwardNetwork.create(genome, config)
start_time = time.perf_counter()
start_time = time.perf_counter()
while True: while True:
output = net.activate( output = net.activate((*app.game.board.matrix(),))
(
*app.game.board.matrix(),
app.game.board.score,
)
)
decision = output.index(max(output)) decision = output.index(max(output))
decisions = { decisions = {
0: app.game.move_up, 0: app.game.move_left,
1: app.game.move_down, 1: app.game.move_down,
2: app.game.move_left, 2: app.game.move_up,
3: app.game.move_right, 3: app.game.move_right,
} }
@ -35,23 +33,15 @@ def eval_genomes(genomes, config: neat.Config):
app._hande_events() app._hande_events()
app.game.draw(app._surface) app.game.draw(app._surface)
max_val = app.game.board.max_val()
time_passed = time.perf_counter() - start_time time_passed = time.perf_counter() - start_time
score = app.game.board.score
if max_val >= 32: if app.game.board.is_game_over():
calculate_fitness(genome, max_val) max_tile, score = calculate_fitness(genome, app)
logger.info(f"{max_val=}\t{score=:_}\t{genome_id=}")
logger.info(f"{max_tile=}\t{score=:_}\t{genome_id=}")
app.game.restart() app.game.restart()
break break
elif app.game.board.is_game_over() or ( elif app.game.board._is_full() and time_passed >= 0.1:
app.game.board._is_full() and time_passed >= 0.1 decisions[random.choice((0, 1, 2, 3))]()
): max_tile, score = calculate_fitness(genome, app)
calculate_fitness(genome, -max_val)
logger.info(f"{max_val=}\t{score=:_}\t{genome_id=}")
app.game.restart()
break
def calculate_fitness(genome: neat.DefaultGenome, score: int):
genome.fitness += score

64
src/ai/fitness.py Normal file
View File

@ -0,0 +1,64 @@
import neat
from py2048 import Menu
from py2048.utils import Position
def calculate_fitness(genome: neat.DefaultGenome, app: Menu) -> tuple[int, int]:
board = app.game.board
score = board.score
max_tile = board.max_val()
empty_cells = 16 - len(board.sprites())
smoothness = calc_smoothness(app)
monotonicity = calc_monotonicity(app)
genome.fitness = score + max_tile**3 + smoothness + monotonicity
return max_tile, score
def calc_smoothness(app: Menu) -> int:
smoothness = 0
for row in range(4):
for col in range(4):
current_value = app.game.board.get_tile_value(Position(row, col))
if current_value:
right_value = app.game.board.get_tile_value(Position(row, col + 1))
if right_value:
smoothness -= abs(current_value - right_value)
left_value = app.game.board.get_tile_value(Position(row, col - 1))
if left_value:
smoothness -= abs(current_value - left_value)
for col in range(4):
for row in range(4):
current_value = app.game.board.get_tile_value(Position(row, col))
if current_value:
up_value = app.game.board.get_tile_value(Position(row - 1, col))
if up_value:
smoothness -= abs(current_value - up_value)
down_value = app.game.board.get_tile_value(Position(row + 1, col))
if down_value:
smoothness -= abs(current_value - down_value)
return smoothness
def calc_monotonicity(app: Menu):
monotonicity = 0
for row in range(4):
row_values = [
app.game.board.get_tile_value(Position(row, col)) for col in range(4)
]
monotonicity += sum(sorted(row_values))
for col in range(4):
col_values = [
app.game.board.get_tile_value(Position(row, col)) for row in range(4)
]
monotonicity += sum(sorted(col_values))
return monotonicity

View File

@ -11,12 +11,10 @@ def train(generations: int) -> None:
"""Train the AI for a given number of generations.""" """Train the AI for a given number of generations."""
config = get_config() config = get_config()
population = neat.Population(config) population = neat.Population(config)
population.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter() population.add_reporter(neat.Checkpointer(None))
population.add_reporter(stats)
population.add_reporter(neat.Checkpointer(1))
winner = population.run(eval_genomes, generations) winner = population.run(eval_genomes, generations)
logger.info(winner) logger.info(winner)
save_genome(winner, BASE_PATH / "best_genome") save_genome(winner)

View File

@ -129,6 +129,13 @@ class Board(pygame.sprite.Group):
return tile return tile
return None return None
def get_tile_value(self, position: Position) -> int:
"""Return the value of the tile at the specified position."""
tile = self.get_tile(position)
if tile:
return tile.value
return 0
def matrix(self) -> list[int]: def matrix(self) -> list[int]:
"""Return a 1d matrix of values of the tiles.""" """Return a 1d matrix of values of the tiles."""
matrix: list[int] = [] matrix: list[int] = []

View File

@ -46,9 +46,9 @@ class Game:
"""Moved the board in the given direction and updates the score.""" """Moved the board in the given direction and updates the score."""
self.board.move(direction) self.board.move(direction)
self.update_score(self.board.score) self.update_score(self.board.score)
if self.board.is_game_over(): # if self.board.is_game_over():
logger.info("Game over!") # logger.info(f"Game over! Score was {self.board.score}.")
self.restart() # self.restart()
def move_up(self) -> None: def move_up(self) -> None:
self.move(Direction.UP) self.move(Direction.UP)

View File

@ -91,7 +91,7 @@ class Menu:
elif event.type == pygame.KEYDOWN: elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_q: if event.key == pygame.K_q:
self.exit() self.exit()
if self._game_active: if self._game_active or self._ai_active:
self.game.handle_events(event) self.game.handle_events(event)
def play(self) -> None: def play(self) -> None:
@ -114,12 +114,7 @@ class Menu:
3: self.game.move_right, 3: self.game.move_right,
} }
output = self.network.activate( output = self.network.activate((*self.game.board.matrix(),))
(
*self.game.board.matrix(),
self.game.board.score,
)
)
decision = output.index(max(output)) decision = output.index(max(output))
decisions[decision]() decisions[decision]()