From 77cd153b10b5eea645266e59307a3eb2590b7d9c Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Sun, 7 Jan 2024 04:29:01 +0200 Subject: [PATCH] refactor(ai) --- config | 8 +++--- main.py | 7 +++-- src/ai/evaluations.py | 54 ++++++++++++++++++++------------------ src/ai/moves/__init__.py | 3 +++ src/ai/moves/calculate.py | 35 ++++++++++++++++++++++++ src/game/screens/game.py | 2 +- src/game/screens/main.py | 4 +-- src/game/screens/tetris.py | 8 +++--- src/utils/config.py | 2 +- 9 files changed, 82 insertions(+), 41 deletions(-) create mode 100644 src/ai/moves/__init__.py create mode 100644 src/ai/moves/calculate.py diff --git a/config b/config index 7bee74d..b314c26 100644 --- a/config +++ b/config @@ -1,7 +1,7 @@ [NEAT] fitness_criterion = max -fitness_threshold = 0 -pop_size = 100 +fitness_threshold = 500 +pop_size = 50 reset_on_extinction = False [DefaultGenome] @@ -44,8 +44,8 @@ node_add_prob = 0.2 node_delete_prob = 0.2 # network parameters -num_hidden = 3 -num_inputs = 216 +num_hidden = 1 +num_inputs = 200 num_outputs = 6 # node response options diff --git a/main.py b/main.py index 7103c7b..adf8564 100755 --- a/main.py +++ b/main.py @@ -61,14 +61,13 @@ def main(args: argparse.ArgumentParser) -> None: elif args.verbose: CONFIG.log_level = "info" - # import ai + import ai import game if args.train is not None: - # ai.log.debug("Training the AI") - # # ai.train(*args.train) + ai.log.debug("Training the AI") + ai.train(*args.train) # game.Menu(GameMode.AI_TRAINING).run() - pass else: game.log.debug("Running the game") game.Main(GameMode.PLAYER).run() diff --git a/src/ai/evaluations.py b/src/ai/evaluations.py index e8ed37d..36362a3 100644 --- a/src/ai/evaluations.py +++ b/src/ai/evaluations.py @@ -6,55 +6,59 @@ import numpy as np import pygame from game import Main 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 .moves import calculate_fitness def eval_genome(genome: neat.DefaultGenome, config: neat.Config) -> float: - app = Main() + app = Main(GameMode.AI_TRAINING).play() - app.mute() game = app.game + tetris = game.tetris net = neat.nn.FeedForwardNetwork.create(genome, config) genome.fitness = 0 - while not game.game_over: - current_figure: list[int] = [ - component - for block in game.tetromino.blocks - for component in (int(block.pos.x), int(block.pos.y)) - ] + while not tetris.game_over: + # current_figure: list[int] = [ + # component + # for block in tetris.tetromino.blocks + # for component in (int(block.pos.x), int(block.pos.y)) + # ] - next_figure: list[int] = [ - vec - for vec in app.next_figures[0].value.shape - for vec in (int(vec.x), int(vec.y)) - ] + # next_figure: list[int] = [ + # vec + # for vec in game.next_figure.value.shape + # 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)) decisions = { - 0: game.move_left, - 1: game.move_right, - 2: game.move_down, - 3: game.rotate, - 4: game.rotate_reverse, - 5: game.drop, + 0: tetris.move_left, + 1: tetris.move_right, + 2: tetris.move_down, + 3: tetris.rotate, + 4: tetris.rotate_reverse, + 5: tetris.drop, } decisions[decision]() app.run_game_loop() 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}") - game.restart() + tetris.restart() return genome.fitness diff --git a/src/ai/moves/__init__.py b/src/ai/moves/__init__.py new file mode 100644 index 0000000..1275e53 --- /dev/null +++ b/src/ai/moves/__init__.py @@ -0,0 +1,3 @@ +from .calculate import calculate_fitness + +__all__ = ["calculate_fitness"] diff --git a/src/ai/moves/calculate.py b/src/ai/moves/calculate.py new file mode 100644 index 0000000..7118446 --- /dev/null +++ b/src/ai/moves/calculate.py @@ -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 diff --git a/src/game/screens/game.py b/src/game/screens/game.py index f015946..9f57e41 100644 --- a/src/game/screens/game.py +++ b/src/game/screens/game.py @@ -23,7 +23,7 @@ class Game(BaseScreen): game: Game object. score: Score object. preview: Preview object. - next_figures: List of upcoming figures. + next_figure: List of upcoming figures. music: Music that plays in the background. """ diff --git a/src/game/screens/main.py b/src/game/screens/main.py index af7f09e..0b5f234 100644 --- a/src/game/screens/main.py +++ b/src/game/screens/main.py @@ -13,7 +13,7 @@ from .game import Game class Main(BaseScreen, SceenElement, TextScreen): def __init__(self, mode: GameMode) -> None: - log.info("Initializing the game") + # log.info("Initializing the game") self._initialize_pygame() self._initialize_surface() self._initialize_rect() @@ -61,7 +61,7 @@ class Main(BaseScreen, SceenElement, TextScreen): def exit(self) -> None: """Exit the game.""" - log.info("Exiting the game") + # log.info("Exiting the game") pygame.quit() sys.exit() diff --git a/src/game/screens/tetris.py b/src/game/screens/tetris.py index 94f7f57..cb43f91 100644 --- a/src/game/screens/tetris.py +++ b/src/game/screens/tetris.py @@ -150,8 +150,8 @@ class Tetris(BaseScreen): self._check_finished_rows() self.game_over: bool = self._check_game_over() - if self.game_over: - self.restart() + # if self.game_over: + # self.restart() self.tetromino = Tetromino( self.sprites, @@ -171,13 +171,13 @@ class Tetris(BaseScreen): """ for block in self.tetromino.blocks: if block.pos.y <= 0: - log.info("Game over!") + # log.info("Game over!") return True return False def restart(self) -> None: """Restart the game.""" - log.info("Restarting the game") + # log.info("Restarting the game") self._reset_game_state() self._initialize_field_and_tetromino() self.game_over = False diff --git a/src/utils/config.py b/src/utils/config.py index 48e99f6..6158f69 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -21,7 +21,7 @@ class Game: size: Size = Size(columns * cell.width, rows * cell.width) pos: Vec2 = Vec2(padding, padding) offset: Vec2 = Vec2(columns // 2, -1) - initial_speed: float | int = 300 + initial_speed: float | int = 100 movment_delay: int = 150 rotation_delay: int = 200 drop_delay: int = 200