Added app code

main
František Špaček 3 months ago
parent 8cc44f0044
commit 6a14ccab48

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,122 @@
"""
Neural network module
"""
import copy
import random
import numpy as np
from scipy.stats import truncnorm
from config import min_mutation_rate, mutation_chance
# Generate random numbers
def truncated_normal(mean=0, sd=1, low=0, upp=10):
"""
returns a truncated normal
"""
return truncnorm((low - mean) / sd, (upp - mean) / sd, loc=mean, scale=sd)
# changes value by random value in mutation range
# modulo to keep value in range
def mutate_by(value, mutation_rate):
"""
mutates a variable using a scale and a normal distribution
"""
if random.uniform(0, 1) < mutation_chance:
return value + np.random.normal(0, mutation_rate)
return value
# Neural network class
class Nnetwork:
"""
Neural network class
contains all the layers and weights and clone and evaluate function
"""
def __init__(self,
numbers_of_nodes,
mutation_rate):
self.no_of_in_nodes = numbers_of_nodes[0]
self.no_of_out_nodes = numbers_of_nodes[1]
self.no_of_hidden_nodes = numbers_of_nodes[2]
self.no_of_hidden_layers = numbers_of_nodes[3]
self.mutation_rate = mutation_rate
self.create_weight_matrices()
def create_weight_matrices(self):
"""
A method to initialize the weight matrices of the neural network
"""
# Input
rad = 1 / np.sqrt(self.no_of_in_nodes)
x = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_in_hidden = x.rvs(
(self.no_of_hidden_nodes, self.no_of_in_nodes))
# Hidden layers
rad = 1 / np.sqrt(self.no_of_hidden_nodes)
self.hidden_layers = []
for _ in range(self.no_of_hidden_layers):
x = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
weights_hidden_hidden = x.rvs(
(self.no_of_hidden_nodes, self.no_of_hidden_nodes))
self.hidden_layers.append(weights_hidden_hidden)
# Output
x = truncated_normal(mean=0, sd=1, low=-rad, upp=rad)
self.weights_hidden_out = x.rvs(
(self.no_of_out_nodes, self.no_of_hidden_nodes))
def clone(self):
"""
returns a mutated copy of the neural network
"""
network_copy = copy.deepcopy(self)
# input weights
for i, weight in enumerate(network_copy.weights_in_hidden):
network_copy.weights_in_hidden[i] = mutate_by(weight, self.mutation_rate)
# hiddens weights
for i, layer in enumerate(network_copy.hidden_layers):
for j, weight in enumerate(layer):
network_copy.hidden_layers[i][j] = mutate_by(weight, self.mutation_rate)
# output weights
for i, weight in enumerate(network_copy.weights_hidden_out):
network_copy.weights_hidden_out[i] = mutate_by(weight, self.mutation_rate)
# change mutation rate
network_copy.mutation_rate = abs(mutate_by(self.mutation_rate, self.mutation_rate))
network_copy.mutation_rate = max(network_copy.mutation_rate, min_mutation_rate)
return network_copy
def evaluate(self, input_vector):
"""
running the network with an input vector 'input_vector'.
'input_vector' can be tuple, list or ndarray
"""
# Turn the input vector into a column vector:
input_vector = np.array(input_vector, ndmin=2).T
input_hidden = self.weights_in_hidden @ input_vector
for _, layer in enumerate(self.hidden_layers):
input_hidden = layer @ input_hidden
output_vector = self.weights_hidden_out @ input_hidden
return output_vector
def run(self, input_data):
"""
Returns the resulting action
decided by biggest value
"""
output_vector = self.evaluate(input_data)
maximum = np.amax(output_vector)
for i, value in enumerate(output_vector):
if value == maximum:
return i
return 0

@ -0,0 +1,219 @@
"""
The main app module
contains the genetic algorithm
"""
import threading
import numpy as np
import pygame
import ai
import tetris
import gui
from button import Button
from config import s_width, s_height, start_btn_image, start_btn_position, start_btn_scale, load_btn_image, load_btn_position, load_btn_scale, run_ten_btn_image, run_ten_btn_position, run_ten_btn_scale, \
number_of_networks, no_of_out_nodes, no_of_in_nodes, mutation_rate, num_of_parts, graph_position, graph_size, \
save_file, col
import saves
from menu import show_menu
# false positive
# pylint: disable=no-member
def simulation_thread(neural_networks, variables, setting, dimensions):
"""
simulates a single game with parallel to others
"""
(index, show_gui, progress) = variables
neural_networks[index]["score"] = (0, 0)
for _ in range (3):
setting[5] += 1
subsurface = window.subsurface(0, 0, s_width / 2, s_height)
(score, block_count) = tetris.game(subsurface, neural_networks[index]["network"].run, setting, dimensions, show_gui) # play one game
#neural_networks[index]["score"] += (neural_networks[index]["score"][0] + score, neural_networks[index]["score"][1] + block_count)
neural_networks[index]["score"] = (neural_networks[index]["score"][0] + float(score), neural_networks[index]["score"][1] + float(block_count))
progress[0] += 1/3
gui.draw_simulation_progress(
subsurface, progress[0], dimensions[0], dimensions[1], dimensions[2])
def calculate_score(score):
"""
Calculates the final score
"""
return score[0] + score[1]
#return score[0]
# plays a game for each neural network and saves it's score
def simulate_generation(neural_networks, settings, block_size, play_width, play_height):
"""
runs many games in parallel and simulates a whole generation
"""
# simulation progress counter
progress = [-1]
# run parallel simulations
simulation_threads = []
# first network will show gui
simulation_threads.append(threading.Thread(target=simulation_thread, args=(
neural_networks, (0, True, progress), settings, (block_size, play_width, play_height))))
simulation_threads[0].start()
pool_size = int(number_of_networks / num_of_parts) * (num_of_parts - 1)
for i in range(pool_size):
simulation_threads.append(threading.Thread(target=simulation_thread, args=(
# list of neural networks
neural_networks,
# index, gui, progress bar
(number_of_networks - pool_size + i, False, progress),
# game settings
settings, (block_size, play_width, play_height))))
simulation_threads[i + 1].start()
simulation_threads[0].join()
# wait for all threads to finish
for i in range(pool_size):
# first network will show gui
simulation_threads[i + 1].join()
neural_networks.sort(key=lambda item: calculate_score(item["score"]))
return neural_networks[::-1] # return sorted from largest to smallest
def generate_networks(in_count, settings):
"""
generates brand new network and clones it to acheve desired amount
"""
# generate networks
neural_network = ai.Nnetwork((in_count, no_of_out_nodes, settings[3], settings[4]),
mutation_rate=mutation_rate)
networks = [{
"score": (0, 0),
"network": neural_network.clone()
} for i in range(number_of_networks)]
print("networks are ready")
return networks
def evolve(networks, logs, blob, dimensions, buttons):
"""
runs a generation and then sorts and evolves the neural networks
"""
networks = simulate_generation(
networks, blob[1], dimensions[0], dimensions[1], dimensions[2])
# create offsprings to replenish population - roulette selection
roulette_wheel_scores = [calculate_score(
network["score"]) for network in networks]
roulette_wheel_networks = [network["network"] for network in networks]
print([round(score, 2) for score in roulette_wheel_scores])
score_sum = sum(roulette_wheel_scores)
print(score_sum)
average = score_sum / number_of_networks
print("average score: " + str(average))
print("generation: " + str(blob[0]))
logs[0].append(max(roulette_wheel_scores))
logs[1].append(average)
# kill the lower scoring half
networks = networks[0:int(number_of_networks / num_of_parts)]
for _ in range(int(number_of_networks / num_of_parts)):
# find random parent
score_target = np.random.randint(0, score_sum)
for j, score in enumerate([x**4 for x in roulette_wheel_scores]):
score_target -= score
# generate offsprings
if score_target <= 0:
for _ in range(num_of_parts - 1):
networks.append({
"score": (0, 0),
"network": roulette_wheel_networks[j].clone()
})
break
buttons[0].draw(window)
buttons[1].draw(window)
buttons[2].draw(window)
pygame.display.update()
gui.draw_graph(window, graph_position, graph_size, logs, (blob[0], dimensions[0]))
# save the best ai
saves.save_network(networks[0], save_file)
return networks
def main():
"""
main function
"""
settings = show_menu(window)
# (row, col, number_of_blocks, no_of_hidden_layers, no_of_hidden_nodes, rng_seed) = settings
block_size = int((600) / settings[0]) # size of block
# play window width; 300/10 = 30 width per block
# play window height; 600/20 = 20 height per block
gui.draw_graph(window, graph_position, graph_size, [], (0, block_size))
# create button instances
# button to run one evolution cycle
start_img = pygame.image.load(start_btn_image).convert_alpha()
start_button = Button(start_btn_position, start_img, start_btn_scale)
start_button.draw(window)
# button to load a neural network from save file
load_img = pygame.image.load(load_btn_image).convert_alpha()
load_button = Button(load_btn_position, load_img, load_btn_scale)
load_button.draw(window)
# button to run ten generations
run_ten_img = pygame.image.load(run_ten_btn_image).convert_alpha()
run_ten_button = Button(
run_ten_btn_position, run_ten_img, run_ten_btn_scale)
run_ten_button.draw(window)
pygame.display.update()
max_score_log = []
average_score_log = []
networks = generate_networks((no_of_in_nodes - col) + settings[1], settings)
generation = 0
run = True
while run:
# run once with gui
if start_button.click():
networks = evolve(networks, [max_score_log, average_score_log],
(generation, settings), (block_size, settings[1] * block_size , settings[0] * block_size), (start_button, load_button, run_ten_button))
generation += 1
# run ten times
if run_ten_button.click():
for _ in range(10):
networks = evolve(networks, [max_score_log, average_score_log],
(generation, settings), (block_size, settings[1] * block_size , settings[0] * block_size), (start_button, load_button, run_ten_button))
generation += 1
if load_button.click():
networks = [saves.load_network(save_file)] * number_of_networks
print("network loaded")
generation = 0
# event handler
for event in pygame.event.get():
# quit game
if event.type == pygame.QUIT:
run = False
pygame.quit()
if __name__ == '__main__':
window = pygame.display.set_mode((s_width, s_height))
main()

@ -0,0 +1,44 @@
"""
Button module
"""
import pygame
# button class
class Button():
"""
Button class
"""
def __init__(self, position, image, scale):
width = image.get_width()
height = image.get_height()
self.image = pygame.transform.scale(
image, (int(width * scale), int(height * scale)))
self.rect = self.image.get_rect()
self.rect.topleft = position
self.clicked = False
def click(self):
"""
returns true if the button was clicked
"""
action = False
# get mouse position
pos = pygame.mouse.get_pos()
# check mouseover and clicked conditions
if self.rect.collidepoint(pos):
if pygame.mouse.get_pressed()[0] == 1 and not self.clicked:
self.clicked = True
action = True
if pygame.mouse.get_pressed()[0] == 0:
self.clicked = False
return action
def draw(self, window):
"""
draw button on screen
"""
window.blit(self.image, (self.rect.x, self.rect.y))

@ -0,0 +1,149 @@
"""
Stores all important configuration data
"""
col = 8 # number of columns
row = 15 # number of rows
s_width = 1000 # window width
s_height = 750 # window height
block_size = int((600) / row) # size of block
play_width = col * block_size # play window width; 300/10 = 30 width per block
play_height = row * block_size # play window height; 600/20 = 20 height per block
top_left_x = 0 # play area to left x
top_left_y = s_height - play_height - 50 # play area to left y
fall_speed = 50 # determines the speed at which blocks fall
# how many inputs will the game wait for when running without graphical mode
turn_count = 1
max_block_count = 13 # maximum amount of blocks allowed to fall to prevent infinite games
no_of_in_nodes = col + 8 # number of input nodes - don't change
no_of_out_nodes = 5 # number of output nodes - don't change
no_of_hidden_nodes = 15 # number of hidden nodes in each hidden layer
no_of_hidden_layers = 2 # number of hidden layers
mutation_rate = 0.4 # starting mutation rate
mutation_chance = 0.1 # chance that the mutation will happen
min_mutation_rate = 0.1 # munimum mutation rate
number_of_networks = 10 # how many neural networks will be created, has to be even
num_of_parts = 3 # into how many parts will be the old and new networks divided
# Shapes
S = [['....',
'....',
'..00',
'.00.'],
['....',
'..0.',
'..00',
'...0']]
Z = [['....',
'....',
'.00.',
'..00'],
['....',
'..0.',
'.00.',
'.0..']]
I = [['..0.',
'..0.',
'..0.',
'..0.'],
['....',
'0000',
'....',
'....']]
O = [['....',
'....',
'.00.',
'.00.']]
J = [['....',
'.0..',
'.000',
'....'],
['....',
'..00',
'..0.',
'..0.'],
['....',
'....',
'.000',
'...0'],
['....',
'..0.',
'..0.',
'.00.']]
L = [['....',
'...0',
'.000',
'....'],
['....',
'..0.',
'..0.',
'..00'],
['....',
'....',
'.000',
'.0..'],
['....',
'.00.',
'..0.',
'..0.']]
T = [['....',
'..0.',
'.000',
'....'],
['....',
'..0.',
'..00',
'..0.'],
['....',
'....',
'.000',
'..0.'],
['....',
'..0.',
'.00.',
'..0.']]
num_of_blocks = 4
shape_list = [I, Z, L, O, J, S, T]
shape_colors = [(0, 0, 255), (255, 0, 0), (0, 255, 255),
(255, 255, 0), (255, 165, 0), (0, 255, 0), (128, 0, 128)]
border_color = (255, 255, 255) # border color
text_color = (255, 255, 255) # text color
background_color = (0, 0, 0) # background color
graph_size = (s_width / 2, 300) # size of the graph
graph_position = (s_width / 2, top_left_y + play_height -
graph_size[1]) # position of the graph
graph_colors = [(255, 165, 0), (0, 0, 255)]
save_file = "app/saves/ai_save.txt" # path to the ai save file
caption = 'Tetris machine learning'
start_btn_image = 'app/gui/start_btn.png' # path to the button image
# start button position
start_btn_position = (graph_position[0] * 1.05, graph_position[1] - 70)
start_btn_scale = 0.5 # start button scale
load_btn_image = 'app/gui/load_btn.png' # path to the button image
# load button position
load_btn_position = (graph_position[0] * 1.70, graph_position[1] - 70)
load_btn_scale = start_btn_scale # load button scale
run_ten_btn_image = 'app/gui/run_ten_btn.png' # path to the button image
# run ten times button position
run_ten_btn_position = (graph_position[0] * 1.38, graph_position[1] - 70)
run_ten_btn_scale = start_btn_scale # run ten button scale
continue_btn_image = 'app/gui/continue_btn.png' # path to the button image
continue_btn_position = ((s_width) / 2 - 65, 570) # continue button position
continue_btn_scale = start_btn_scale # continue button scale

@ -0,0 +1,159 @@
"""
Handles the application gui and draws the game
"""
import pygame
from config import s_width, number_of_networks, num_of_parts, border_color, text_color, background_color, graph_colors, top_left_x, top_left_y, caption
pygame.font.init()
def draw_text_middle(text, size, color, surface, data):
"""
Draws text in the middle of the window
"""
(top, play_width) = data
font = pygame.font.SysFont('Arial', size, bold=False, italic=True)
label = font.render(text, 1, color)
surface.blit(label, ((s_width - play_width) // 2 + play_width/2 - (label.get_width()/2),
top_left_y + top - (label.get_height()/2)))
def draw_graph(surface, graph_position, graph_size, data, args):
"""
Draws a performance history graph
"""
(generation, block_size) = args
rectangle = (graph_position[0], graph_position[1],
graph_size[0], graph_size[1])
surface.subsurface((graph_position[0], graph_position[1],
graph_size[0], graph_size[1] + 50)).fill(background_color)
# empty graph
if len(data) == 0:
# draw border
pygame.draw.rect(surface, border_color, rectangle, 4)
return
maximum = max(data[0])
# draw each dataset as separate line
for i, line in enumerate(data):
line = [0] + line
line = [(graph_position[0] + i * (graph_size[0] / (len(line) - 1)), graph_position[1] +
graph_size[1] - (graph_size[1] * point / maximum) * 0.95) for i, point in enumerate(line)]
pygame.draw.lines(surface, graph_colors[i], False, line, 2)
# draw border
pygame.draw.rect(surface, border_color, rectangle, 4)
# print data
font = pygame.font.SysFont('Arial', int(block_size / 2))
text = "gen: " + str(generation) + " max: " + \
str(round(data[0][-1], 2)) + " avg: " + str(round(data[1][-1], 2))
label = font.render(text, 1, text_color)
surface.blit(label, ((graph_position[0] + graph_size[0] / 2) - (
label.get_width() / 2), (graph_position[1] + graph_size[1]) + block_size / 2))
pygame.display.update(
rectangle[0], rectangle[1], rectangle[2], rectangle[3] + 40)
def draw_grid(surface, grid, block_size, play_width, play_height):
"""
Draws the lines of the grid for the game
"""
r = g = b = 0
grid_color = (r, g, b)
for i in range(len(grid)):
# draw grey horizontal lines
pygame.draw.line(surface, grid_color, (top_left_x, top_left_y + i * block_size),
(top_left_x + play_width, top_left_y + i * block_size))
for j in range(len(grid[0])):
# draw grey vertical lines
pygame.draw.line(surface, grid_color, (top_left_x + j * block_size, top_left_y),
(top_left_x + j * block_size, top_left_y + play_height))
def draw_simulation_progress(surface, progress, block_size, play_width, play_height):
"""
Prints the simulation progress
"""
font = pygame.font.SysFont('Arial', block_size)
text = str(int(progress / number_of_networks * 100 / (num_of_parts - 1) * num_of_parts)) + " %"
label = font.render(text, 1, text_color)
rectangle = (top_left_x + play_width / 2) - (label.get_width() / 2), top_left_y + \
play_height + 10, top_left_x + play_width, label.get_width() * 2
pygame.draw.rect(surface, background_color, rectangle)
surface.blit(label, ((top_left_x + play_width / 2) -
(label.get_width() / 2), top_left_y + play_height + 10))
pygame.display.update(rectangle)
def draw_next_shape(piece, surface, block_size, play_width):
"""
Draws the upcoming piece
"""
font = pygame.font.SysFont('Arial', int(block_size / 2))
label = font.render('Next shape:', 1, text_color)
start_x = top_left_x + play_width + 10
start_y = top_left_y + block_size
shape_format = piece.shape[piece.rotation % len(piece.shape)]
for i, line in enumerate(shape_format):
row_list = list(line)
for j, column in enumerate(row_list):
if column == '0':
pygame.draw.rect(surface, piece.color, (start_x + j*block_size,
start_y + i*block_size, block_size, block_size), 0)
surface.blit(label, (start_x, start_y - 30))
def draw_window(surface, grid, block_size, dimensions, score=0):
"""
Draws the content of the window
"""
(play_width, play_height) = dimensions
surface.fill(background_color) # fill the surface with background color
pygame.font.init() # initialise font
font = pygame.font.SysFont('Calibri', block_size * 2, bold=True)
# initialise 'Tetris' text with white
label = font.render('TETRIS', 1, text_color)
# put on the center of the window
surface.blit(label, ((top_left_x + play_width / 2) -
(label.get_width() / 2), 30))
# current score
font = pygame.font.SysFont('Arial', int(block_size / 2))
label = font.render('Score: ' + str(score), 1, text_color)
start_x = top_left_x + play_width + 10
start_y = top_left_y + block_size * 5
surface.blit(label, (start_x, start_y))
# draw content of the grid
for i, row in enumerate(grid):
for j, cell in enumerate(row):
# pygame.draw.rect()
# draw a rectangle shape
# rect(Surface, color, Rect, width=0) -> Rect
pygame.draw.rect(surface, cell, (top_left_x + j * block_size,
top_left_y + i * block_size, block_size, block_size), 0)
# draw vertical and horizontal grid lines
draw_grid(surface, grid, block_size, play_width, play_height)
# draw rectangular border around play area
pygame.draw.rect(surface, border_color, (top_left_x,
top_left_y, play_width, play_height), 4)
# initial config
pygame.display.set_caption(caption)

@ -0,0 +1,72 @@
"""
Module which add an interactive textbox into pygame
"""
# false positive
# pylint: disable=no-member
import pygame as pg
COLOR_INACTIVE = pg.Color('lightskyblue3')
COLOR_ACTIVE = pg.Color('dodgerblue2')
FONT = pg.font.Font(None, 32)
class InputBox:
"""
Box for data input
"""
def __init__(self, rect, text=''):
self.rect = pg.Rect(rect)
self.color = COLOR_INACTIVE
self.text = text
self.txt_surface = FONT.render(text, True, self.color)
self.active = False
def handle_event(self, event):
"""
handles the button click event
"""
if event.type == pg.MOUSEBUTTONDOWN:
# If the user clicked on the input_box rect.
if self.rect.collidepoint(event.pos):
# Toggle the active variable.
self.active = not self.active
else:
self.active = False
# Change the current color of the input box.
self.color = COLOR_ACTIVE if self.active else COLOR_INACTIVE
if event.type == pg.KEYDOWN:
if self.active:
if event.key == pg.K_RETURN:
print(self.text)
self.text = ''
elif event.key == pg.K_BACKSPACE:
self.text = self.text[:-1]
else:
if str(event.unicode).isnumeric():
self.text += event.unicode
# Re-render the text.
self.txt_surface = FONT.render(self.text, True, self.color)
def update(self):
"""
Resize the box if the text is too long.
"""
width = max(200, self.txt_surface.get_width()+10)
self.rect.w = width
def draw(self, screen):
"""
Draws the button on the screen
"""
screen.blit(self.txt_surface, (self.rect.x+5, self.rect.y+5))
pg.draw.rect(screen, self.color, self.rect, 2)
def value(self):
"""
Returns a numerical value of the box
"""
if self.text == '':
return 0
return int(self.text)

@ -0,0 +1,70 @@
"""
Starting menu
"""
import pygame
from inputbox import InputBox
import gui
from button import Button
from config import background_color, text_color, s_width, row, col, no_of_hidden_layers, no_of_hidden_nodes, continue_btn_image, continue_btn_position, continue_btn_scale, play_width, num_of_blocks
# false positive
# pylint: disable=no-member
def show_menu(window):
"""
displays the starting menu and returns settings
"""
left_pos = (s_width - 200) / 2
box_rows = InputBox((left_pos, 150, 140, 32), str(row))
box_columns = InputBox((left_pos, 220, 140, 32), str(col))
box_number_of_blocks = InputBox((left_pos, 290, 140, 32), str(num_of_blocks))
box_layers = InputBox((left_pos, 360, 140, 32), str(no_of_hidden_layers))
box_neurons = InputBox((left_pos, 430, 140, 32), str(no_of_hidden_nodes))
box_rng_seed = InputBox((left_pos, 500, 140, 32), str(no_of_hidden_nodes))
input_boxes = [box_rows, box_columns, box_number_of_blocks,
box_layers, box_neurons, box_rng_seed]
# continue button
continue_img = pygame.image.load(continue_btn_image).convert_alpha()
continue_button = Button(
continue_btn_position, continue_img, continue_btn_scale)
continue_button.draw(window)
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
done = True
for box in input_boxes:
box.handle_event(event)
for box in input_boxes:
box.update()
window.fill(background_color)
for box in input_boxes:
box.draw(window)
if continue_button.click():
done = True
gui.draw_text_middle("SETTINGS", 30, text_color, window, (0, play_width))
gui.draw_text_middle("rows", 20, text_color, window, (40, play_width))
gui.draw_text_middle("columns", 20, text_color,
window, (110, play_width))
gui.draw_text_middle("number of blocks", 20,
text_color, window, (180, play_width))
gui.draw_text_middle("layers", 20, text_color, window, (250, play_width))
gui.draw_text_middle("neurons", 20, text_color,
window, (320, play_width))
gui.draw_text_middle("rng seed", 20, text_color,
window, (390, play_width))
continue_button.draw(window)
pygame.display.update()
window.fill(background_color)
pygame.display.update()
return [box.value() for box in input_boxes]

@ -0,0 +1,21 @@
"""
Handles the saving and loading of objects
"""
import pickle
def save_network(obj, filename):
"""
Store the network in a save file
"""
with open(filename, 'wb') as outp: # Overwrites any existing file.
pickle.dump(obj, outp, pickle.HIGHEST_PROTOCOL)
def load_network(filename):
"""
Retrieve the network from a save file
"""
with open(filename, 'rb') as inp: # Overwrites any existing file.
network = pickle.load(inp)
return network

@ -0,0 +1,57 @@
import pytest
from ai import mutate_by, Nnetwork
from math import isclose
import numpy as np
# testing mutation
@pytest.mark.parametrize(
'value, mutation_rate, expected, tol',
[
(1, 0, 1000, 0),
(1, 0.1, 1000, 10),
(-10, 1, -10000, 100)
])
def test_normals(value, mutation_rate, expected, tol):
sum = 0
for _ in range(1000):
sum += mutate_by(value, mutation_rate)
assert isclose(sum, expected, abs_tol=tol)
@pytest.mark.parametrize(
'value, mutation_rate, expected, tol',
[
(1, 0, 999.999, 0),
(1, 0.1, 1000, 0.01),
(-10, 50, -10000, 1)
])
def test_normals2(value, mutation_rate, expected, tol):
sum = 0
for _ in range(1000):
sum += mutate_by(value, mutation_rate)
assert not isclose(sum, expected, abs_tol=tol)
# test cloning (should make exact clone when zero mutations)
def test_cloning():
network = Nnetwork([1, 1, 1, 1], 0)
clone = network.clone()
assert network.hidden_layers == clone.hidden_layers \
and network.weights_hidden_out == clone.weights_hidden_out \
and network.weights_in_hidden == clone.weights_in_hidden
# should make a diferent clone
def test_cloning2():
network = Nnetwork([100, 100, 100, 100], 10)
clone = network.clone()
assert not (np.array(network.hidden_layers) == np.array(clone.hidden_layers)).all()
# test inner logic
def test_evaluate():
network = Nnetwork([1, 1, 1, 1], 0)
assert isclose(network.evaluate([1])[0], 0, rel_tol=1)
# should only output left, right, down or rotate
def test_run():
network = Nnetwork([4, 4, 100, 1], 1)
assert network.run([1, 6, -2, 0.1]) in [0, 1, 2, 3]

@ -0,0 +1,95 @@
import pytest
from tetris import Piece, game, get_holes, create_grid, valid_space, handle_input, get_highest_list
import pygame
from math import isclose
# testing tetrominos
@pytest.mark.parametrize(
'x, y, shape, shapes, expected',
[
(0, 0, ["0"], [["0"]], [(-2,-4)]),
(0, 0, [".0"], [[".0"]], [(-2,-3)]),
(0, 0, [".0"], [[".0"]], [(-2,-3)]),
(1, 2, [[".","0"]], [[[".","0"]]], [(-1,-1)]),
(0, 0, [['....', '.0..', '.000', '....']],
[[['....', '.0..', '.000', '....']],["0"]],
[(-1, -3), (-1, -2), (0, -2), (1, -2)])
])
def test_cell_positions(x, y, shape, shapes, expected):
piece = Piece(x, y, shape, shapes)
assert piece.get_cell_positions() == expected
# testing grid
@pytest.mark.parametrize(
'locked_pos, row, col, expected',
{
((0, 0, 0), 1, 1, (0, 0, 0))
})
def test_create_grid(locked_pos, row, col, expected):
assert create_grid(locked_pos, row, col) == [[expected]]
@pytest.fixture
def test_grid():
return [[(0,0,0), (0,0,0)], [(0,0,0), (255,255,255)]]
@pytest.fixture
def col():
return 2
@pytest.fixture
def row():
return 2
# testing positions
@pytest.fixture
def test_piece():
return Piece(0, 0, [[".0",".0"]], [[[".0",".0"]]])
def test_valid_space(test_piece, test_grid):
assert valid_space(test_piece, test_grid, 2, 2) == True
def test_invalid_space(test_piece, test_grid):
test_piece.x += 10
test_piece.y += 10
assert valid_space(test_piece, test_grid, 2, 2) == False
# testing game input
@pytest.mark.parametrize(
'action_value, expected_x, expected_y',
{
(1, -1, -1),
(2, 1, 1),
(3, 0, 0),
(4, 0, 0),
})
def test_handle_input(action_value, test_piece, test_grid, row, col, expected_x, expected_y):
pygame.display.set_mode((100, 100))
handle_input(action_value, test_piece, test_grid, row, col)
assert test_piece.x == expected_x and test_piece.x == expected_y
# testing eval functions
def test_highest(col, test_grid):
assert get_highest_list(col, test_grid) == [0, 1]
def test_holes(col, test_grid):
highest = get_highest_list(col, test_grid)
assert get_holes(col, highest, test_grid) == [0, -2]
# testing whole game
@pytest.fixture
def input_function():
return lambda _ : 2
def test_game(input_function):
surface = pygame.display.set_mode((100, 100))
score = game(surface, input_function, [9, 8, 2, 3, 5, 20], [5, 20, 20])
assert isclose(score[0], 1.24, abs_tol=1e-9) and isclose(score[1], 0)

@ -0,0 +1,331 @@
"""
Independent tetris module
tetriminos:
0 - S - green
1 - Z - red
2 - I - cyan
3 - O - yellow
4 - J - blue
5 - L - orange
6 - T - purple
"""
import sys
import random
from itertools import chain
import numpy as np
import pygame
import gui
from config import turn_count, max_block_count, shape_colors, s_width, s_height, shape_list
# global variables
# false positive
# pylint: disable=no-member
# class to represent each of the pieces
class Piece():
"""
Tetromino object
"""
def __init__(self, x, y, shape, shapes):
self.x = x
self.y = y
self.shape = shape
self.shapes = shapes
# choose color from the shape_color list
self.color = shape_colors[shapes.index(shape)]
self.rotation = 0 # chooses the rotation according to index
def get_cell_positions(self):
"""col
Converts the object type and rotation into an array of cell positions
"""
positions = []
# get the desired rotated shape from piece
shape_format = self.shape[self.rotation % len(self.shape)]
# i gives index; line gives string
for i, line in enumerate(shape_format):
row_list = list(line) # makes a list of char from string
# j gives index of char; column gives char
for j, column in enumerate(row_list):
if column == '0':
positions.append((self.x + j, self.y + i))
for i, pos in enumerate(positions):
# offset according to the input given with dot and zero
positions[i] = (pos[0] - 2, pos[1] - 4)
return positions
@staticmethod
def get_random_shape(shapes):
"""
Chooses a shape randomly from shapes list
"""
return Piece(5, 2, random.choice(shapes), shapes)
def create_grid(locked_pos, row, col):
"""
Initialise the grid
"""
grid = [[(0, 0, 0) for x in range(col)]
for y in range(row)] # grid represented rgb tuples
# locked_positions dictionary
# (x,y):(r,g,b)
for y in range(row):
for x in range(col):
if (x, y) in locked_pos:
# get the value color (r,g,b) from the locked_positions dictionary using key (x,y)
color = locked_pos[(x, y)]
grid[y][x] = color # set grid position to color
return grid
def valid_space(piece, grid, row, col):
"""
Checks if current position of piece in grid is valid
"""
# makes a 2D list of all the possible (x,y)
accepted_pos = [[(x, y) for x in range(col) if grid[y]
[x] == (0, 0, 0)] for y in range(row)]
# removes sub lists and puts (x,y) in one list; easier to search
accepted_pos = [x for item in accepted_pos for x in item]
formatted_shape = piece.get_cell_positions()
for pos in formatted_shape:
if pos not in accepted_pos:
if pos[1] >= 0:
return False
return True
def check_lost(positions):
"""
Check if the peice does not fit into the board
"""
for pos in positions:
if pos[1] < 1:
return True
return False
def clear_rows(grid, locked):
"""
clear a row when it is filled
"""
# need to check if row is clear then shift every other row above one down
cleared = 0
index = len(grid) - 1
while index > 0:
grid_row = grid[index]
if (0, 0, 0) not in grid_row:
for j in range(len(grid_row)):
try:
# delete every locked element in the row
del locked[(j, index + cleared)]
except ValueError:
continue
cleared += 1 # increase score
# delete filled bottom row
# add another empty row on the top
# move down one step
for key in sorted(list(locked), key=lambda a: a[1])[::-1]:
x, y = key
if y < index + cleared:
new_key = (x, y + 1) # shift position to down
locked[new_key] = locked.pop(key)
index -= 1 # decrease index
return cleared
def user_input():
"""
Handles all in game user input events
0 - Nothing
1 - Left
2 - Right
3 - Down
4 - Rotate
"""
result_action = 0
for event in pygame.event.get():
if event.key == pygame.K_LEFT:
result_action = 1
elif event.key == pygame.K_RIGHT:
result_action = 2
elif event.key == pygame.K_DOWN:
result_action = 3
elif event.key == pygame.K_UP:
result_action = 4
return result_action
def handle_input(action_value, current_piece, grid, row, col):
"""
Handles inputed action value
0 - Nothing
1 - Left
2 - Right
3 - Down
4 - Rotate
"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.display.quit()
sys.exit(0)
if action_value == 1:
current_piece.x -= 1 # move x position left
if not valid_space(current_piece, grid, row, col):
current_piece.x += 1
elif action_value == 2:
current_piece.x += 1 # move x position right
if not valid_space(current_piece, grid, row, col):
current_piece.x -= 1
elif action_value == 3:
current_piece.y += 1 # move y position down
while valid_space(current_piece, grid, row, col):
current_piece.y += 1
current_piece.y -= 1
elif action_value == 4:
# rotate shape
current_piece.rotation = current_piece.rotation + \
1 % len(current_piece.shape)
if not valid_space(current_piece, grid, row, col):
current_piece.rotation = current_piece.rotation - \
1 % len(current_piece.shape)
def get_highest_list(col, grid):
"""
get the position of the highest cells
"""
highest_list = [0] * col
for y, current_row in enumerate(grid[::-1]):
for x, cell in enumerate(current_row):
if cell > (0, 0, 0):
highest_list[x] = y + 1
return highest_list
def get_holes(col, highest, grid):
"""
get the number of closed holes
"""
holes = []
np_grid = np.array(grid)
for x in range(col):
holes.append(highest[x] - np.count_nonzero(np_grid[:,x]))
return holes
def game(surface, input_function, settings, dimensions, graphics_mode=False):
"""
Main game function
"""
(row, col, number_of_blocks, _, _, rng_seed) = settings
(block_size, play_width, play_height) = dimensions
shapes = shape_list[0:number_of_blocks]
locked_positions = {}
change_piece = False
random.seed(rng_seed)
current_piece = Piece.get_random_shape(shapes)
next_piece = Piece.get_random_shape(shapes)
fall_count = 0
score = 0
height_score = 0
block_count = 0
last_holes = 0
while True:
grid = create_grid(locked_positions, row, col)
fall_count += 1
# wait for given nuber of turns
if fall_count > turn_count:
fall_count = 0
current_piece.y += 1
height_score += current_piece.y * 0.01
if not valid_space(current_piece, grid, row, col) and current_piece.y > 0:
current_piece.y -= 1
change_piece = True
# give points for placing the block as low as possible
# gather game data and pass it to the input function
# positions of all cells
# [highest cell in each column]
piece_pos = current_piece.get_cell_positions()
#game_data = [shapes.index(current_piece.shape),
# current_piece.rotation,
# current_piece.x,
# current_piece.y]
game_data = list(chain.from_iterable(piece_pos))
highest_list = get_highest_list(col, grid)
game_data += highest_list
# get input
input_value = input_function(game_data)
# handle input
if not change_piece:
handle_input(input_value, current_piece, grid, row, col)
height_score += 0.005 if input_value == 4 else 0
else:
holes = sum(get_holes(col, highest_list, grid))
height_score -= (holes - last_holes) * 0.01 if holes > last_holes else 0
last_holes = holes
# draw the piece on the grid by giving color in the piece locations
for x, y in piece_pos:
if y >= 0:
grid[y][x] = current_piece.color
if change_piece: # if the piece is locked
for pos in piece_pos:
p = (pos[0], pos[1])
# add the key and value in the dictionary
locked_positions[p] = current_piece.color
current_piece = next_piece
next_piece = Piece.get_random_shape(shapes)
change_piece = False
# increment score, give exponentialy more points for multiple rows cleared
score += (2 ** clear_rows(grid, locked_positions) - 1) * 2
#block_count += 1
if graphics_mode:
gui.draw_window(surface, grid, block_size, (play_width, play_height), score)
gui.draw_next_shape(next_piece, surface, block_size, play_width)
pygame.display.update(0, 0, s_width / 2, s_height)
# break to prevent infinite games
if check_lost(locked_positions) or block_count > max_block_count:
break
return (height_score, score)
Loading…
Cancel
Save

Powered by TurnKey Linux.