This commit is contained in:
mia 2026-04-23 09:14:31 +02:00
parent 0d2340b7e8
commit a74be2fc31
11 changed files with 2367 additions and 184 deletions

19
.direnv/bin/nix-direnv-reload Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
if [[ ! -d "/home/mia/git/Nim-AI-template" ]]; then
echo "Cannot find source directory; Did you move it?"
echo "(Looking for "/home/mia/git/Nim-AI-template")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
_nix_direnv_force_reload=1 direnv exec "/home/mia/git/Nim-AI-template" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
touch "/home/mia/git/Nim-AI-template/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
touch -r "/home/mia/git/Nim-AI-template/.envrc" "/home/mia/git/Nim-AI-template/.direnv"/*.rc

View file

@ -0,0 +1 @@
/nix/store/swz054hgfjc292xpp0axf1p089mlcbcd-nix-shell-env

File diff suppressed because it is too large Load diff

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

363
game.py
View file

@ -1,179 +1,184 @@
import pygame import pygame
import sys import sys
import time import time
# Initialize Pygame # Initialize Pygame
pygame.init() pygame.init()
# Constants # Constants
WIDTH, HEIGHT = 700, 500 WIDTH, HEIGHT = 700, 500
FPS = 30 FPS = 30
WHITE = (255, 255, 255) WHITE = (255, 255, 255)
BLACK = (0, 0, 0) BLACK = (0, 0, 0)
RED = (255, 0, 0) RED = (255, 0, 0)
GREEN = (0, 255, 0) GREEN = (0, 255, 0)
LIGHT_GREY = (200, 200, 200) LIGHT_GREY = (200, 200, 200)
DARK_GREY = (50, 50, 50) DARK_GREY = (50, 50, 50)
FONT = pygame.font.Font(None, 36) FONT = pygame.font.Font(None, 36)
# Create screen # Create screen
screen = pygame.display.set_mode((WIDTH, HEIGHT)) screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Nim Game') pygame.display.set_caption('Nim Game')
# Define piles (number of coins in each pile) # Define piles (number of coins in each pile)
piles = [4, 4, 4, 4] # 4 piles with 4 coins each piles = [4, 4, 4, 4] # 4 piles with 4 coins each
selected_stones = [] # To store selected stones for removal selected_stones = [] # To store selected stones for removal
selected_pile = None # Tracks the pile from which coins are selected selected_pile = None # Tracks the pile from which coins are selected
# Players # Players
player_turn = 1 # Player 1 starts (alternates between 1 and 2) player_turn = 1 # Player 1 starts (alternates between 1 and 2)
# Game State # Game State
game_over = False game_over = False
winner = None winner = None
def draw_piles(): def draw_piles():
"""Draws the piles of coins as circles with padding.""" """Draws the piles of coins as circles with padding."""
x_pos = 130 # Start from left with padding x_pos = 130 # Start from left with padding
y_start = 250 y_start = 250
padding = 150 # Increased space between piles for better symmetry padding = 150 # Increased space between piles for better symmetry
radius = 20 radius = 20
for idx, pile in enumerate(piles): for idx, pile in enumerate(piles):
y_pos = y_start y_pos = y_start
for stone in range(pile): for stone in range(pile):
color = RED color = RED
if (idx, stone) in selected_stones: if (idx, stone) in selected_stones:
color = GREEN # Show selected stones as green color = GREEN # Show selected stones as green
pygame.draw.circle(screen, color, (x_pos, y_pos), radius) pygame.draw.circle(screen, color, (x_pos, y_pos), radius)
y_pos -= 2 * radius + 10 # Space between circles y_pos -= 2 * radius + 10 # Space between circles
text = FONT.render(f'Pile {idx + 1}', True, BLACK) text = FONT.render(f'Pile {idx + 1}', True, BLACK)
screen.blit(text, (x_pos - 30, 320)) screen.blit(text, (x_pos - 30, 320))
x_pos += padding # Increase x position and add padding x_pos += padding # Increase x position and add padding
def check_game_over(): def check_game_over():
"""Check if the game is over (all piles empty).""" """Check if the game is over (all piles empty)."""
global winner, game_over global winner, game_over
if all(pile == 0 for pile in piles): if all(pile == 0 for pile in piles):
winner = 2 if player_turn == 1 else 1 # The other player wins winner = 2 if player_turn == 1 else 1 # The other player wins
game_over = True game_over = True
def draw_game_state(): def draw_game_state():
"""Draws the current game state including piles and turn.""" """Draws the current game state including piles and turn."""
screen.fill(LIGHT_GREY) # Background color screen.fill(LIGHT_GREY) # Background color
draw_piles() draw_piles()
if game_over: if game_over:
if player_turn == 1: if player_turn == 1:
text = FONT.render(f'You win, hooray!', True, GREEN) text = FONT.render(f'You win, hooray!', True, GREEN)
screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Adjusted y-position for spacing screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Adjusted y-position for spacing
else: else:
text = FONT.render(f'AI wins!', True, GREEN) text = FONT.render(f'AI wins!', True, GREEN)
screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Adjusted y-position for spacing screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Adjusted y-position for spacing
# Draw the Restart button # Draw the Restart button
pygame.draw.rect(screen, DARK_GREY, (WIDTH // 2 - 60, HEIGHT - 60, 120, 40)) pygame.draw.rect(screen, DARK_GREY, (WIDTH // 2 - 60, HEIGHT - 60, 120, 40))
restart_text = FONT.render("Restart", True, WHITE) restart_text = FONT.render("Restart", True, WHITE)
screen.blit(restart_text, (WIDTH // 2 - restart_text.get_width() // 2, HEIGHT - 50)) screen.blit(restart_text, (WIDTH // 2 - restart_text.get_width() // 2, HEIGHT - 50))
else: else:
if player_turn == 1: if player_turn == 1:
text = FONT.render(f'Your turn!', True, BLACK) text = FONT.render(f'Your turn!', True, BLACK)
screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Centered player label screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Centered player label
else: else:
text = FONT.render(f'Computer thinking... ', True, BLACK) text = FONT.render(f'Computer thinking... ', True, BLACK)
screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Centered player labe screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 30)) # Centered player labe
# Draw the "Remove" button at the bottom # Draw the "Remove" button at the bottom
pygame.draw.rect(screen, BLACK, (WIDTH // 2 - 60, HEIGHT - 120, 120, 40)) pygame.draw.rect(screen, BLACK, (WIDTH // 2 - 60, HEIGHT - 120, 120, 40))
remove_text = FONT.render("Remove", True, WHITE) remove_text = FONT.render("Remove", True, WHITE)
screen.blit(remove_text, (WIDTH // 2 - remove_text.get_width() // 2, HEIGHT - 110)) screen.blit(remove_text, (WIDTH // 2 - remove_text.get_width() // 2, HEIGHT - 110))
def remove_stones(): def remove_stones():
"""Removes the selected stones from the selected pile.""" """Removes the selected stones from the selected pile."""
global player_turn, selected_pile global player_turn, selected_pile
for pile_index, stone_index in selected_stones: if not selected_stones:
piles[pile_index] -= 1 return # Do nothing if nothing is selected
selected_stones.clear() for pile_index, stone_index in selected_stones:
selected_pile = None # Reset selected pile after removal piles[pile_index] -= 1
player_turn = 2 if player_turn == 1 else 1 # Switch turns selected_stones.clear()
check_game_over() selected_pile = None # Reset selected pile after removal
player_turn = 2 if player_turn == 1 else 1 # Switch turns
def handle_selection(pile_index, stone_index): check_game_over()
"""Handles selecting or deselecting stones, ensuring only one pile can be selected."""
global selected_pile def handle_selection(pile_index, stone_index):
if selected_pile is None or selected_pile == pile_index: """Handles selecting or deselecting stones, ensuring only one pile can be selected."""
selected_pile = pile_index # Lock the selection to the current pile global selected_pile
if (pile_index, stone_index) in selected_stones: if selected_pile is None or selected_pile == pile_index:
selected_stones.remove((pile_index, stone_index)) # Deselect if (pile_index, stone_index) in selected_stones:
else: selected_stones.remove((pile_index, stone_index)) # Deselect
selected_stones.append((pile_index, stone_index)) # Select # If no stones are selected anymore, allow switching piles
if not selected_stones:
def restart_game(): selected_pile = None
"""Restarts the game.""" else:
global piles, player_turn, selected_stones, selected_pile, game_over, winner selected_stones.append((pile_index, stone_index)) # Select
piles = [4, 4, 4, 4] # Reset piles selected_pile = pile_index # Lock the selection to the current pile
selected_stones.clear()
selected_pile = None def restart_game():
player_turn = 1 """Restarts the game."""
game_over = False global piles, player_turn, selected_stones, selected_pile, game_over, winner
winner = None piles = [4, 4, 4, 4] # Reset piles
selected_stones.clear()
def start_game(ai): selected_pile = None
"""Starts the game and integrates AI for playing against the computer.""" player_turn = 1
global player_turn, game_over game_over = False
winner = None
# Main game loop
clock = pygame.time.Clock() def start_game(ai):
while True: """Starts the game and integrates AI for playing against the computer."""
clock.tick(FPS) global player_turn, game_over
for event in pygame.event.get(): # Main game loop
if event.type == pygame.QUIT: clock = pygame.time.Clock()
pygame.quit() while True:
sys.exit() clock.tick(FPS)
elif event.type == pygame.MOUSEBUTTONDOWN and not game_over and player_turn == 1:
mouse_x, mouse_y = event.pos for event in pygame.event.get():
# Check for pile selection (clicking on a coin) if event.type == pygame.QUIT:
x_pos = 130 pygame.quit()
y_start = 250 sys.exit()
padding = 150 elif event.type == pygame.MOUSEBUTTONDOWN and not game_over and player_turn == 1:
radius = 20 mouse_x, mouse_y = event.pos
for pile_index, pile in enumerate(piles): # Check for pile selection (clicking on a coin)
y_pos = y_start x_pos = 130
for stone_index in range(pile): y_start = 250
dist = ((mouse_x - x_pos)**2 + (mouse_y - y_pos)**2)**0.5 padding = 150
if dist <= radius: radius = 20
handle_selection(pile_index, stone_index) for pile_index, pile in enumerate(piles):
y_pos -= 2 * radius + 10 y_pos = y_start
x_pos += padding for stone_index in range(pile):
# Check for "Remove" button click dist = ((mouse_x - x_pos)**2 + (mouse_y - y_pos)**2)**0.5
if WIDTH // 2 - 60 <= mouse_x <= WIDTH // 2 + 60 and HEIGHT - 120 <= mouse_y <= HEIGHT - 80: if dist <= radius:
remove_stones() handle_selection(pile_index, stone_index)
y_pos -= 2 * radius + 10
# If game over, check for "Restart" button click x_pos += padding
elif event.type == pygame.MOUSEBUTTONDOWN and game_over: # Check for "Remove" button click
mouse_x, mouse_y = event.pos if WIDTH // 2 - 60 <= mouse_x <= WIDTH // 2 + 60 and HEIGHT - 120 <= mouse_y <= HEIGHT - 80:
if WIDTH // 2 - 60 <= mouse_x <= WIDTH // 2 + 60 and HEIGHT - 60 <= mouse_y <= HEIGHT - 20: remove_stones()
restart_game()
# If game over, check for "Restart" button click
# If it's the AI's turn and the game is not over elif event.type == pygame.MOUSEBUTTONDOWN and game_over:
if player_turn == 2 and not game_over: mouse_x, mouse_y = event.pos
draw_game_state() if WIDTH // 2 - 60 <= mouse_x <= WIDTH // 2 + 60 and HEIGHT - 60 <= mouse_y <= HEIGHT - 20:
pygame.display.flip() restart_game()
# Simulate thinking
time.sleep(2) # If it's the AI's turn and the game is not over
# AI makes its move if player_turn == 2 and not game_over:
action = ai.choose_action(piles, epsilon=False) draw_game_state()
remove_stones_from_ai(action) pygame.display.flip()
# Simulate thinking
draw_game_state() time.sleep(2)
pygame.display.flip() # AI makes its move
action = ai.choose_action(piles, epsilon=False)
def remove_stones_from_ai(action): remove_stones_from_ai(action)
"""Handles AI stone removal."""
pile, count = action draw_game_state()
for i in range(count): pygame.display.flip()
piles[pile] -= 1
global player_turn def remove_stones_from_ai(action):
player_turn = 1 # Switch back to human player """Handles AI stone removal."""
check_game_over() pile, count = action
for i in range(count):
piles[pile] -= 1
global player_turn
player_turn = 1 # Switch back to human player
check_game_over()

33
nim.py
View file

@ -56,11 +56,17 @@ class NimAI():
float: The Q-value associated with the (state, action) pair. float: The Q-value associated with the (state, action) pair.
Returns 0 if the pair is not yet in the Q-table. Returns 0 if the pair is not yet in the Q-table.
""" """
print(self.q) print(self.q, state, action)
try:
return self.q[(tuple(state), action)]
except:
return 0
def update_q_value(self, state, action, old_q, reward, future_q): def update_q_value(self, state, action, old_q, reward, future_q):
""" """
Update the Q-value for a state-action pair using the Q-learning formula. Update the Q-value for a state-action pair using the Q-learning formula.
Q(s, a) Q(s, a) + α * (Belohnung + γ * max_a' Q(s', a') - Q(s, a))
Parameters: Parameters:
state (list): The current game state. state (list): The current game state.
@ -69,10 +75,11 @@ class NimAI():
reward (float): The reward received after taking the action. reward (float): The reward received after taking the action.
future_q (float): The maximum Q-value for the next state. future_q (float): The maximum Q-value for the next state.
""" """
raise NotImplementedError self.q[tuple(state), action] = old_q + self.alpha * (reward + self.epsilon * future_q - old_q)
return 0
def best_future_reward(self, state): def best_future_reward(self, state):
""" """
Determine the highest Q-value among all possible actions in a given state. Determine the highest Q-value among all possible actions in a given state.
Parameters: Parameters:
@ -82,7 +89,15 @@ class NimAI():
float: The highest Q-value among available actions. float: The highest Q-value among available actions.
Returns 0 if no actions are available. Returns 0 if no actions are available.
""" """
raise NotImplementedError # actions = []
# for q in self.q.key:
# if q[0] == state:
# actions.append(q[1])
actions = tuple([key[1] for key in self.q.keys() if key[0] == state])
try:
return max([q for q in self.q[tuple(state), actions]])
except:
return 0
def choose_action(self, state, epsilon=True): def choose_action(self, state, epsilon=True):
""" """
@ -95,7 +110,15 @@ class NimAI():
Returns: Returns:
tuple: The chosen action from the available actions. tuple: The chosen action from the available actions.
""" """
raise NotImplementedError if epsilon:
return random.choice(tuple(Nim.available_actions(state)))
# keys = [key[1] for key in self.q.key if key[0] == state]
# for key in keys:
else:
try:
return max([key[1] for key in self.q.keys() if key[0] == state])
except:
return (0,0)
def train(n): def train(n):
player = NimAI() player = NimAI()

10
shell.nix Normal file
View file

@ -0,0 +1,10 @@
let
pkgs = import <nixpkgs> {};
in pkgs.mkShell {
packages = [
(pkgs.python3.withPackages (python-pkgs: [
python-pkgs.pygame
]))
];
}