refactor(ai)

This commit is contained in:
Kristofers Solo 2024-01-07 04:29:01 +02:00
parent c8859bc571
commit 77cd153b10
9 changed files with 82 additions and 41 deletions

8
config
View File

@ -1,7 +1,7 @@
[NEAT] [NEAT]
fitness_criterion = max fitness_criterion = max
fitness_threshold = 0 fitness_threshold = 500
pop_size = 100 pop_size = 50
reset_on_extinction = False reset_on_extinction = False
[DefaultGenome] [DefaultGenome]
@ -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 = 3 num_hidden = 1
num_inputs = 216 num_inputs = 200
num_outputs = 6 num_outputs = 6
# node response options # node response options

View File

@ -61,14 +61,13 @@ def main(args: argparse.ArgumentParser) -> None:
elif args.verbose: elif args.verbose:
CONFIG.log_level = "info" CONFIG.log_level = "info"
# import ai import ai
import game import game
if args.train is not None: if args.train is not None:
# ai.log.debug("Training the AI") ai.log.debug("Training the AI")
# # ai.train(*args.train) ai.train(*args.train)
# game.Menu(GameMode.AI_TRAINING).run() # game.Menu(GameMode.AI_TRAINING).run()
pass
else: else:
game.log.debug("Running the game") game.log.debug("Running the game")
game.Main(GameMode.PLAYER).run() game.Main(GameMode.PLAYER).run()

View File

@ -6,55 +6,59 @@ import numpy as np
import pygame import pygame
from game import Main from game import Main
from game.sprites import Block from game.sprites import Block
from utils import CONFIG from utils import CONFIG, GameMode
from .fitness import calculate_fitness # from .fitness import calculate_fitness
from .log import log from .log import log
from .moves import calculate_fitness
def eval_genome(genome: neat.DefaultGenome, config: neat.Config) -> float: def eval_genome(genome: neat.DefaultGenome, config: neat.Config) -> float:
app = Main() app = Main(GameMode.AI_TRAINING).play()
app.mute()
game = app.game game = app.game
tetris = game.tetris
net = neat.nn.FeedForwardNetwork.create(genome, config) net = neat.nn.FeedForwardNetwork.create(genome, config)
genome.fitness = 0 genome.fitness = 0
while not game.game_over: while not tetris.game_over:
current_figure: list[int] = [ # current_figure: list[int] = [
component # component
for block in game.tetromino.blocks # for block in tetris.tetromino.blocks
for component in (int(block.pos.x), int(block.pos.y)) # for component in (int(block.pos.x), int(block.pos.y))
] # ]
next_figure: list[int] = [ # next_figure: list[int] = [
vec # vec
for vec in app.next_figures[0].value.shape # for vec in game.next_figure.value.shape
for vec in (int(vec.x), int(vec.y)) # for vec in (int(vec.x), int(vec.y))
] # ]
field = np.where(game.field != None, 1, 0) field = np.where(tetris.field != None, 1, 0)
output = net.activate((*next_figure, *current_figure, *field.flatten())) for block in tetris.tetromino.blocks:
field[int(block.pos.y), int(block.pos.x)] = 2
output = net.activate(field.flatten())
decision = output.index(max(output)) decision = output.index(max(output))
decisions = { decisions = {
0: game.move_left, 0: tetris.move_left,
1: game.move_right, 1: tetris.move_right,
2: game.move_down, 2: tetris.move_down,
3: game.rotate, 3: tetris.rotate,
4: game.rotate_reverse, 4: tetris.rotate_reverse,
5: game.drop, 5: tetris.drop,
} }
decisions[decision]() decisions[decision]()
app.run_game_loop() app.run_game_loop()
genome.fitness = calculate_fitness(field) genome.fitness = calculate_fitness(field)
score, lines, level = app.game.score, app.game.lines, app.game.level score, lines, level = tetris.score, tetris.lines, tetris.level
log.debug(f"{genome.fitness=:<+6.6}\t{score=:<6} {lines=:<6} {level=:<6}") log.debug(f"{genome.fitness=:<+6.6}\t{score=:<6} {lines=:<6} {level=:<6}")
game.restart() tetris.restart()
return genome.fitness return genome.fitness

3
src/ai/moves/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .calculate import calculate_fitness
__all__ = ["calculate_fitness"]

35
src/ai/moves/calculate.py Normal file
View File

@ -0,0 +1,35 @@
from typing import Optional
import numpy as np
from ai.log import log
from .bumpiness import bumpiness
from .height import aggregate_height
from .holes import holes
from .lines import complete_lines
def calculate_fitness(field: np.ndarray) -> float:
"""
Calculate the fitness value for the given field.
Args:
field: The game field.
Returns:
The fitness value.
"""
height_w = aggregate_height(field)
holes_w = holes(field)
bumpiness_w = bumpiness(field)
lines_w = complete_lines(field)
fitness = (
-0.510066 * height_w
+ 0.760666 * lines_w
- 0.35663 * holes_w
- 0.184483 * bumpiness_w
)
return fitness

View File

@ -23,7 +23,7 @@ class Game(BaseScreen):
game: Game object. game: Game object.
score: Score object. score: Score object.
preview: Preview object. preview: Preview object.
next_figures: List of upcoming figures. next_figure: List of upcoming figures.
music: Music that plays in the background. music: Music that plays in the background.
""" """

View File

@ -13,7 +13,7 @@ from .game import Game
class Main(BaseScreen, SceenElement, TextScreen): class Main(BaseScreen, SceenElement, TextScreen):
def __init__(self, mode: GameMode) -> None: def __init__(self, mode: GameMode) -> None:
log.info("Initializing the game") # log.info("Initializing the game")
self._initialize_pygame() self._initialize_pygame()
self._initialize_surface() self._initialize_surface()
self._initialize_rect() self._initialize_rect()
@ -61,7 +61,7 @@ class Main(BaseScreen, SceenElement, TextScreen):
def exit(self) -> None: def exit(self) -> None:
"""Exit the game.""" """Exit the game."""
log.info("Exiting the game") # log.info("Exiting the game")
pygame.quit() pygame.quit()
sys.exit() sys.exit()

View File

@ -150,8 +150,8 @@ class Tetris(BaseScreen):
self._check_finished_rows() self._check_finished_rows()
self.game_over: bool = self._check_game_over() self.game_over: bool = self._check_game_over()
if self.game_over: # if self.game_over:
self.restart() # self.restart()
self.tetromino = Tetromino( self.tetromino = Tetromino(
self.sprites, self.sprites,
@ -171,13 +171,13 @@ class Tetris(BaseScreen):
""" """
for block in self.tetromino.blocks: for block in self.tetromino.blocks:
if block.pos.y <= 0: if block.pos.y <= 0:
log.info("Game over!") # log.info("Game over!")
return True return True
return False return False
def restart(self) -> None: def restart(self) -> None:
"""Restart the game.""" """Restart the game."""
log.info("Restarting the game") # log.info("Restarting the game")
self._reset_game_state() self._reset_game_state()
self._initialize_field_and_tetromino() self._initialize_field_and_tetromino()
self.game_over = False self.game_over = False

View File

@ -21,7 +21,7 @@ class Game:
size: Size = Size(columns * cell.width, rows * cell.width) size: Size = Size(columns * cell.width, rows * cell.width)
pos: Vec2 = Vec2(padding, padding) pos: Vec2 = Vec2(padding, padding)
offset: Vec2 = Vec2(columns // 2, -1) offset: Vec2 = Vec2(columns // 2, -1)
initial_speed: float | int = 300 initial_speed: float | int = 100
movment_delay: int = 150 movment_delay: int = 150
rotation_delay: int = 200 rotation_delay: int = 200
drop_delay: int = 200 drop_delay: int = 200