From cb54859b6cf54767534f27790f042533ad4f9274 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 3 Jan 2024 16:35:00 +0200 Subject: [PATCH] adjust the weights --- best_genome | Bin 2923 -> 6299 bytes config.txt | 16 ++++----- src/ai/evaluation.py | 40 +++++++++------------- src/ai/fitness.py | 64 ++++++++++++++++++++++++++++++++++++ src/ai/training.py | 8 ++--- src/py2048/objects/board.py | 7 ++++ src/py2048/screens/game.py | 6 ++-- src/py2048/screens/menu.py | 9 ++--- 8 files changed, 102 insertions(+), 48 deletions(-) create mode 100644 src/ai/fitness.py diff --git a/best_genome b/best_genome index 2b7a09a2ef7598fc359267a111753d1ffc6a572c..c4ce4d5b9cefc9b07b935dda4281bd969c695c66 100644 GIT binary patch literal 6299 zcma)=c~nzZ9>>FuAPu(F(NQ}U2A7V4t)6jIG_+6|K~O6WZd3xqK!AWOwzySP79mBJ z;8tZ6D=y=Rh`0*~2z9MRM1oQ%D60ZV0C9u9`|c0Uz4zn~_#@}s@ZtOYypZ?a@7-;N z>B*x7{Exz1E)hrAN+t4;V2M&;;Urlnjtq)$#_KfYlxXD^A@Xqa8<&3W$F0*aR;e%>NRosr6(b?YaS-PsCCY(7ezcKGFv#tGQ%k1PMAg@R7_iBF zFF`S0|3Fl4oWOmQ!+nBcylfHm+dSifT2URQ-s$lAoTD7{s+#88ym~`WjMq$#5>=JO zI1S%tN>Gf~EJSrZOEOt>KZ&3iuQ`b7DYu>{bP;3ftq!kwh-z6dCOUfS1%hI{9QekS z%NLcUPbDbE%aNlBy7oWJ^SE}Gpct z9Zyh! z`*+s~it!3XRN3k2Q+uN735xLw=O_ok+<*1Dd;LsMjMoO_)v|PSP0Z_UnCj8t6@?np zP4@*id4d;DFA$n*AIw#DxWA%ocaOL$arl>UQG>WtMkp^D%Y*U zD+XnCpJ}=A^DGOT#Z!z|Jfa#i<>QpMjuRB)m58VgbBj9lDK~;*ypj-AUpU(;!zTb! zT{^r{5cQ~3Kc=PW7(p>!N{$j;{4~9Hw{;dlFS-m?qCy)7LERn@{Wg)7i zD|UCd^#V+3baP>k0lM7>BT7@rg7OHhp0Rn+)wnnHbiEnH3b zM#d{2W#!H-SM5G%hqF3$c-=r*MH@XI%}hN{P>fdz%F;w#`j2q4H9;|6Wr%8gSboZ5 zs=8U_(P3VDE1D&jL%uI@G-MD!`- zAwtw|`s%W0<9`e)GN6!3gtT{CpUx2W;}kXPqCzezP5kytLVP=S;}F{ADN3pA7X|y4 z;d2xrv`Z~Qnr^5!WW}r+ETUcNQBlL49Y&rhlQ5!TU7mADp^;~5j$K>YAVRw|qm)MP zcEQg2Uk4G|r41p?uKrct3AYCk+NB*KZT*dNs-6a7M9aFU5pumb@9sAy`1a_=A+$>u zhlpN#e-s;U>phr4ySze3eNKv7>(ZV*%lov&~g z?jj({M<&n{2vAt(ml=)?M{pkwqoia8z!hFxxHow0bRv&3lNn=9)RlkMX+zOw%%P#3 zmXi&Gyx;1AQ=fP2AUN7@IOJ8OZwnNvh{2uyGRcI%Y4-l%?9=B-AyZrLY?WYKi_WK05 zDx23YFGMfI92(*ozX{OWlC)Ko-~*ov&(VH=2d-0b|A}QV41hdG`%N0~J2o$??-zK) z;5pjwGvF%c2dymK0yhdY%rkz|p!dnfP|sl>!-F8t(SFl`OFO*FxPLngHatiB&4AvT z+$WW#nfM%a;W^sxpODvfd0G7aMfd{df<|n{Z#LxhDvHCd+1cXHndfLfd&qlx>#gUR zl4^pZ{Y1cJ+cX)qH-r)#?e`V*Y8}$83AmoVeN*#)gzE;MNBeCBzn8Hay)4SW4?P4h ze$l`cel;;oe@z$efM@)E1n$~7!J1w>*p}is+AkS;?_XJRcbSS9^_lxy8gOSUzjaTo^~1-(=h1$9 zptoDwe&6r+PcVl@bjEK#Z8 zl88LoFB3SexUVPM_b1GuQJwKS23&u#ld%2J27;sgPJmyhOSHFBh!w%ne*c5K_Uy6i zJ)&UCk?*Db&OqL!)R@h==U?JH^pL>#tx;0_;?WHP$qx~)p za36o>n)w3GH@=tly8^v^KiwFmUxgoaA{9pR5I=59m)q=h75EQR2w`W}oqLzr({&Xn z41EH`;oOeyrh>SR>VVY>(=bVRD7RNEQ96q{VawS}%1;qi4@*92#*X_M*6tw$qG5Efem18SG=V8W!bN^p)%%N(A>TL}j2gK9u5$H;R`}vW66* zWi2nceMli1CWKq#kVlo#w*LAPh7_V@Lbx>!(NqtU7B(9WB_vW|vMwM( RE(s4;x=b7)Fpc!F{Sz8pfrS77 literal 2923 zcmZwJdr(wm7{_s7L6FsSawM67E*cBS$V8xorfh)_t|DS)juX=&hdqn3%j~j>6cL)l zo@54^P^rAUrh0qcXm3N)04f6|IO&}RU z4zP6Z{er~6w^agqCO!}1I7dKcZ)1C2mXScX$qA0@f1`C(P-Yf^aMLTW)Dp72Ccn^< zK)7i+ER8FdE<3%+ETE^-CKp)hxwLZImE7Y5!cA_lGj$9> zfpC*2hi+QrpSbo&wOK$^3N@58&1;KIsz9%agg|~pFq)j1!>O4PD_?*8_Al$T>LpQD3OA=ojvnLR4+6qep zu@cRhQBVublJ{(1f%}4XBgJJaMgE`~&j{H$_2{x%Z29uPpK3oJY7x z14|uO%YH1aKTjNon_>WsD5@$HCKmyXOPgZhxPh|t>J?{B69_lO0cx$jct>Zohd{W= z2*-5=y)+rLw}wEtX+NO5Va>HUZsLn>Oxko1j%!INS!VAb6P9>{n?8l5&el*bcV`oU za8oj%p=)O9w%1hx;if}yT-#x{vh_{w0y0aRjsU99-kMFlca}i7DGiQ0=2W!d%?*tN z!c9j3b*6tk-RHv+2seEL=-bd!Upe}T&u&!ObR1A|`HM=|huOjsk8l&qA^(S?KOYPC z5Z^F(gqtz}72Q=NBvfjNCESz+s3L#$>#`%_d*+C=DF>Q<^4T?fp*28Q;t_889?yY12&sp&egGacj7|{4Fg-t}$a^bikY14H;Ra*)YavSvo!c8}z zsitB4Gso#21j0?Fu+))mx$eQ832d^X__=2}=Xgrdn9~bxjHLj!Qm) za8n(ibM+J5#Y;932six+$2Ek9_HSH}LLl7K2&nmK(;vCnAMnV;*y!|8+|`e}6RIre z`OG5m1{lgc`oFuM%7U?u)T)gv_u@Wv(e|U7XvWS!8RNJsCB@wa2gQr2y_&mb?pMQA zlVxm;T1~888^tnmwMJv0G~(kjqbZg(MXD_6mf33}_j!A8c}{s-(w6Z~x?^sT?xefu zZn}rlR)n_VwEyV##chvwc0XUs2rUC$mzQDZntA=X+QisKYvXm4(a5Tlrby5K0FPr3 AJ^%m! diff --git a/config.txt b/config.txt index a1fc871..0295912 100644 --- a/config.txt +++ b/config.txt @@ -1,14 +1,14 @@ [NEAT] -fitness_criterion = mean -fitness_threshold = 400 -pop_size = 1000 +fitness_criterion = max +fitness_threshold = 32768 +pop_size = 5000 reset_on_extinction = False [DefaultGenome] # node activation options -activation_default = relu -activation_mutate_rate = 1.0 -activation_options = relu +activation_default = sigmoid +activation_mutate_rate = 0.0 +activation_options = sigmoid # node aggregation options aggregation_default = sum @@ -44,8 +44,8 @@ node_add_prob = 0.2 node_delete_prob = 0.2 # network parameters -num_hidden = 2 -num_inputs = 17 +num_hidden = 4 +num_inputs = 16 num_outputs = 4 # node response options diff --git a/src/ai/evaluation.py b/src/ai/evaluation.py index e128ad7..858ac22 100644 --- a/src/ai/evaluation.py +++ b/src/ai/evaluation.py @@ -1,9 +1,12 @@ +import random import time import neat from loguru import logger from py2048 import Menu +from .fitness import calculate_fitness + def eval_genomes(genomes, config: neat.Config): app = Menu() @@ -12,22 +15,17 @@ def eval_genomes(genomes, config: neat.Config): for genome_id, genome in genomes: genome.fitness = 0 net = neat.nn.FeedForwardNetwork.create(genome, config) - start_time = time.perf_counter() + start_time = time.perf_counter() while True: - output = net.activate( - ( - *app.game.board.matrix(), - app.game.board.score, - ) - ) + output = net.activate((*app.game.board.matrix(),)) decision = output.index(max(output)) decisions = { - 0: app.game.move_up, + 0: app.game.move_left, 1: app.game.move_down, - 2: app.game.move_left, + 2: app.game.move_up, 3: app.game.move_right, } @@ -35,23 +33,15 @@ def eval_genomes(genomes, config: neat.Config): app._hande_events() app.game.draw(app._surface) - max_val = app.game.board.max_val() time_passed = time.perf_counter() - start_time - score = app.game.board.score - if max_val >= 32: - calculate_fitness(genome, max_val) - logger.info(f"{max_val=}\t{score=:_}\t{genome_id=}") + + if app.game.board.is_game_over(): + max_tile, score = calculate_fitness(genome, app) + + logger.info(f"{max_tile=}\t{score=:_}\t{genome_id=}") app.game.restart() break - elif app.game.board.is_game_over() or ( - app.game.board._is_full() and time_passed >= 0.1 - ): - 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 + elif app.game.board._is_full() and time_passed >= 0.1: + decisions[random.choice((0, 1, 2, 3))]() + max_tile, score = calculate_fitness(genome, app) diff --git a/src/ai/fitness.py b/src/ai/fitness.py new file mode 100644 index 0000000..e6bd0ba --- /dev/null +++ b/src/ai/fitness.py @@ -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 diff --git a/src/ai/training.py b/src/ai/training.py index 47ee69b..83384ad 100644 --- a/src/ai/training.py +++ b/src/ai/training.py @@ -11,12 +11,10 @@ def train(generations: int) -> None: """Train the AI for a given number of generations.""" config = get_config() population = neat.Population(config) - population.add_reporter(neat.StdOutReporter(True)) - stats = neat.StatisticsReporter() - population.add_reporter(stats) - population.add_reporter(neat.Checkpointer(1)) + + population.add_reporter(neat.Checkpointer(None)) winner = population.run(eval_genomes, generations) logger.info(winner) - save_genome(winner, BASE_PATH / "best_genome") + save_genome(winner) diff --git a/src/py2048/objects/board.py b/src/py2048/objects/board.py index e136dd3..92fd2e0 100644 --- a/src/py2048/objects/board.py +++ b/src/py2048/objects/board.py @@ -129,6 +129,13 @@ class Board(pygame.sprite.Group): return tile 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]: """Return a 1d matrix of values of the tiles.""" matrix: list[int] = [] diff --git a/src/py2048/screens/game.py b/src/py2048/screens/game.py index 54d0e9b..7ec346e 100644 --- a/src/py2048/screens/game.py +++ b/src/py2048/screens/game.py @@ -46,9 +46,9 @@ class Game: """Moved the board in the given direction and updates the score.""" self.board.move(direction) self.update_score(self.board.score) - if self.board.is_game_over(): - logger.info("Game over!") - self.restart() + # if self.board.is_game_over(): + # logger.info(f"Game over! Score was {self.board.score}.") + # self.restart() def move_up(self) -> None: self.move(Direction.UP) diff --git a/src/py2048/screens/menu.py b/src/py2048/screens/menu.py index 5fd664c..0801590 100644 --- a/src/py2048/screens/menu.py +++ b/src/py2048/screens/menu.py @@ -91,7 +91,7 @@ class Menu: elif event.type == pygame.KEYDOWN: if event.key == pygame.K_q: self.exit() - if self._game_active: + if self._game_active or self._ai_active: self.game.handle_events(event) def play(self) -> None: @@ -114,12 +114,7 @@ class Menu: 3: self.game.move_right, } - output = self.network.activate( - ( - *self.game.board.matrix(), - self.game.board.score, - ) - ) + output = self.network.activate((*self.game.board.matrix(),)) decision = output.index(max(output)) decisions[decision]()