Compare commits

...

5 Commits

Author SHA1 Message Date
fb0a8dd856 fixed mod operator 2025-08-07 12:17:18 +02:00
945c7df194 README hinzugefügt 2025-08-07 12:13:32 +02:00
1e0559f386 Test all game versions 2025-08-06 12:36:47 +02:00
5232310e3e wins 100% with optimal strategy 2025-08-06 11:59:31 +02:00
45edb546f7 lastMove not bad 2025-08-06 11:37:29 +02:00
2 changed files with 102 additions and 14 deletions

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Nimm-Spiel
Dieses Programm ist eine einfache KI, die das Nimm-Spiel spielt. Damit kann einfach gezeigt werden
wie das Training einer KI funktioniert.
## Regeln
Auf einen Haufen liegen zwölf Hölzchen. Zwei Spieler müssen abwechselnd ein bis drei Hölzchen vom
Haufen nehmen. Der Spieler, der das letzte Hölzchen nimmt, hat verloren.
## Optimale Strategie
Wenn man dran ist und nur ein Hölzchen hat, hat man verloren, da man es nehmen muss. Wenn also zwei
bis vier Hölzchen hat, kann man immer so viele Hölzchen wegnehmen, dass der andere nur noch eins
hat, sodass er verloren hat und man selbst gewonnen. Mit fünf Hölzchen hat man dann wieder verloren,
da man so viel Hölzchen wegnehmen muss, dass der andere zwei bis vier hat.
Dieses Muster setzt sich weiter fort und kann zu folgender Regel verallgemeinert werden: Hat ein
Spieler $n$ Hölzchen, dann gibt es für seiner Gegner eine Gewinnstrategie genau dann, wenn $n \bmod 4 = 1$.
Daraus resultiert die optimale Strategie, mit der man versucht so viele Hölzchen wegzunehmen, dass
noch $n \bmod 4 = 1$ da sind. Wenn das nicht geht, da man selbst $n \bmod 4 = 1$ Hölzchen hat, ist es
egal, was man macht. Diese Strategie habe ich in der Funktion `optimal_move` umgesetzt.
## KI-Implementierung
Für die Implementierung der KI habe ich die Programmiersprache Python gewählt, da ich mit ihr
vertraut bin. Dabei fängt die KI immer mit dem ersten Zug an, da sie sonst gar nicht gewinnen kann.
Training und Evaluation habe ich mit allen Kombinationen aus der optimalen und zufälligen Strategie
gemacht, da es erstens interessant ist, ob die KI auch von einem Gegner, der schlecht spielt, gut
lernen kann. Zweitens hätte es auch vorkommen können, dass, wenn man mit der optimalen Strategie
trainiert, die KI bei der Evaluation mit der zufälligen Strategie gar nicht so gut ist, weil der
zufällige Gegner auch Züge macht, die die KI vorher noch nie gesehen hat.
### Erste Version
Die erste Version der KI nimmt immer nur den letzten Zug, den sie gemacht hat raus, wenn sie
verloren hat. Wenn sie keinen der Züge bevorzugt, dann wählt sie einfach einen zufälligen Zug aus.
Mit dieser KI werden allerdings nur wenige Spiele gewonnen:
```
KI: erste Version trainiert mit random_move und evaluiert mit random_move
Die KI hat 70.078% der Spiele gewonnen.
KI: erste Version trainiert mit random_move und evaluiert mit optimal_move
Die KI hat 4.884% der Spiele gewonnen.
KI: erste Version trainiert mit optimal_move und evaluiert mit random_move
Die KI hat 70.192% der Spiele gewonnen.
KI: erste Version trainiert mit optimal_move und evaluiert mit optimal_move
Die KI hat 4.984999999999999% der Spiele gewonnen.
```
### bessere KI
Die bessere Version erweitert ihr Wissen stückweise. Sie markiert auch wie die erste Version Züge,
die unmittelbar verlieren als verlierend. Allerdings markiert sie zusätzlich, wenn alle Züge
verlierend sind, auch den letzten Zug, den sie gemacht hat als verlierend. Diese KI schafft es alle
Spiele zu gewinnen:
```
KI: optimale Version trainiert mit random_move und evaluiert mit random_move
Die KI hat 100.0% der Spiele gewonnen.
KI: optimale Version trainiert mit random_move und evaluiert mit optimal_move
Die KI hat 100.0% der Spiele gewonnen.
KI: optimale Version trainiert mit optimal_move und evaluiert mit random_move
Die KI hat 100.0% der Spiele gewonnen.
KI: optimale Version trainiert mit optimal_move und evaluiert mit optimal_move
Die KI hat 100.0% der Spiele gewonnen.
```

31
nimm.py
View File

@ -9,11 +9,14 @@ class Move:
def optimal_move(): def optimal_move():
global state global state
for n in [1, 2, 3]: for n in [1, 2, 3]:
if (state - n) % 4 == 1: if (state - n) % 4 == 1: # Wenn der andere Spieler bei diesem Zug verliert
return n return n
return random.randint(1, 3) # Wenn keine perfekte Wahl, dann irgendein Zug return random.randint(1, 3) # Wenn keine perfekte Wahl, dann irgendein Zug
def ki_move(): def random_move():
return random.randint(1, 3)
def ki_move(lastMove, better_version):
global state global state
antiMoves = lostMoves.get(state) antiMoves = lostMoves.get(state)
if antiMoves == None: if antiMoves == None:
@ -24,6 +27,10 @@ def ki_move():
for i, good in enumerate(availableMoves): for i, good in enumerate(availableMoves):
if good: if good:
return i + 1 return i + 1
# Es gibt keine guten Züge mehr, also muss der letzte Zug schlecht gewesen sein.
if better_version:
addLostMove(lastMove.state, lastMove.move)
return random.randint(1, 3) return random.randint(1, 3)
def makeMove(move): # gibt True zurück, wenn der Spieler verloren hat def makeMove(move): # gibt True zurück, wenn der Spieler verloren hat
@ -39,23 +46,23 @@ def addLostMove(state, move):
if moves == None: if moves == None:
moves = [] moves = []
moves.append(move) moves.append(move)
lostMoves[state] = moves
def game(train): def game(train, opponent, better_version):
global state global state
state = 12 state = 12
lastMove = None lastMove = None
while True: while True:
# KI beginnt, sonst kann sie nicht gewinnen # KI beginnt, sonst kann sie nicht gewinnen
move = ki_move() move = ki_move(lastMove, better_version)
lost = makeMove(move) lost = makeMove(move)
if lost: if lost:
if train: if train:
addLostMove(state + move, move) addLostMove(state + move, move)
addLostMove(lastMove.state, lastMove.move)
return 0 # optimale Strategie hat gewonnen return 0 # optimale Strategie hat gewonnen
lastMove = Move(state + move, move) lastMove = Move(state + move, move)
move = optimal_move() move = opponent()
lost = makeMove(move) lost = makeMove(move)
if lost: if lost:
return 1 # KI hat gewonnen return 1 # KI hat gewonnen
@ -64,13 +71,19 @@ state = 12
lostMoves = {} lostMoves = {}
# train # train
opponents = [random_move, optimal_move]
for ki_version in [False, True]:
for train_opponent in opponents:
for eval_opponent in opponents:
ki_text = "optimale Version" if ki_version else "erste Version"
print(f"KI: {ki_text} trainiert mit {train_opponent.__name__} und evaluiert mit {eval_opponent.__name__}")
for _ in range(1000): for _ in range(1000):
game(True) game(train=True, opponent=train_opponent, better_version=ki_version)
# eval # eval
numberEvalGames = 100000 numberEvalGames = 100000
wonGames = 0 wonGames = 0
for _ in range(numberEvalGames): for _ in range(numberEvalGames):
wonGames += game(False) wonGames += game(False, opponent=eval_opponent, better_version=ki_version)
print(f"Die KI hat {wonGames / numberEvalGames * 100}% der Spiele gewonnen.") print(f"Die KI hat {wonGames / numberEvalGames * 100}% der Spiele gewonnen.\n")