Skip to content

Commit 3622f53

Browse files
authored
Merge pull request #308 from realpython/python-minimax-nim
Add materials for Nim Minimax tutorial
2 parents b085cf5 + 6ec3e00 commit 3622f53

File tree

11 files changed

+329
-0
lines changed

11 files changed

+329
-0
lines changed

python-minimax-nim/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Minimax in Python: Learn How to Lose the Game of Nim
2+
3+
Here you can find supplementary material for the Real Python tutorial [Minimax in Python: Learn How to Lose the Game of Nim](https://realpython.com/python-minimax-nim/).
4+
5+
This directory contains source code from the tutorial. Additionally, [`nim/`](nim/) contains an implementation of a small game engine that allows you to play Nim against a minimax player.
6+
7+
Run the game as follows:
8+
9+
```console
10+
$ cd nim/
11+
$ python nim.py
12+
```
13+
14+
Make your choices by entering a corresponding character and hit enter. You can choose between the different variants that are described in the tutorial.
15+
16+
If you want to add a variant yourself, you can do so by adding a new file named with a `game_` prefix. Inside this file, you need to implement the following functions:
17+
18+
- `initial_state()` should set up the initial game state.
19+
- `possible_new_states(state)` should list the possible states that you can move to from the current state.
20+
- `evaluate(state, is_maximizing)` should evaluate an end game state and return `None` if the game isn't over.
21+
22+
See the existing `game_*.py` files for examples.
23+
24+
## Author
25+
26+
- **Geir Arne Hjelle**, E-mail: [[email protected]]([email protected])
27+
28+
## License
29+
30+
Distributed under the MIT license. See [`LICENSE`](../LICENSE) for more information.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from functools import cache
2+
3+
4+
@cache
5+
def minimax(state, is_maximizing, alpha=-1, beta=1):
6+
if (score := evaluate(state, is_maximizing)) is not None:
7+
return score
8+
9+
scores = []
10+
for new_state in possible_new_states(state):
11+
scores.append(
12+
score := minimax(new_state, not is_maximizing, alpha, beta)
13+
)
14+
if is_maximizing:
15+
alpha = max(alpha, score)
16+
else:
17+
beta = min(beta, score)
18+
if beta <= alpha:
19+
break
20+
return (max if is_maximizing else min)(scores)
21+
22+
23+
def best_move(state):
24+
return max(
25+
(minimax(new_state, is_maximizing=False), new_state)
26+
for new_state in possible_new_states(state)
27+
)
28+
29+
30+
def possible_new_states(state):
31+
for pile, counters in enumerate(state):
32+
for remain in range(counters):
33+
yield state[:pile] + (remain,) + state[pile + 1 :]
34+
35+
36+
def evaluate(state, is_maximizing):
37+
if all(counters == 0 for counters in state):
38+
return 1 if is_maximizing else -1

python-minimax-nim/minimax_nim.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from functools import cache
2+
3+
4+
@cache
5+
def minimax(state, is_maximizing):
6+
if (score := evaluate(state, is_maximizing)) is not None:
7+
return score
8+
9+
return (max if is_maximizing else min)(
10+
minimax(new_state, is_maximizing=not is_maximizing)
11+
for new_state in possible_new_states(state)
12+
)
13+
14+
15+
def best_move(state):
16+
return max(
17+
(minimax(new_state, is_maximizing=False), new_state)
18+
for new_state in possible_new_states(state)
19+
)
20+
21+
22+
def possible_new_states(state):
23+
for pile, counters in enumerate(state):
24+
for remain in range(counters):
25+
yield state[:pile] + (remain,) + state[pile + 1 :]
26+
27+
28+
def evaluate(state, is_maximizing):
29+
if all(counters == 0 for counters in state):
30+
return 1 if is_maximizing else -1
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from functools import cache
2+
3+
4+
@cache
5+
def minimax(state, is_maximizing):
6+
if (score := evaluate(state, is_maximizing)) is not None:
7+
return score
8+
9+
return (max if is_maximizing else min)(
10+
minimax(new_state, is_maximizing=not is_maximizing)
11+
for new_state in possible_new_states(state)
12+
)
13+
14+
15+
def best_move(state):
16+
return max(
17+
(minimax(new_state, is_maximizing=False), new_state)
18+
for new_state in possible_new_states(state)
19+
)
20+
21+
22+
def possible_new_states(state):
23+
for pile, counters in enumerate(state):
24+
for take in range(1, (counters + 1) // 2):
25+
yield state[:pile] + (counters - take, take) + state[pile + 1 :]
26+
27+
28+
def evaluate(state, is_maximizing):
29+
if all(counters <= 2 for counters in state):
30+
return -1 if is_maximizing else 1
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from functools import cache
2+
3+
4+
@cache
5+
def minimax(state, is_maximizing):
6+
if (score := evaluate(state, is_maximizing)) is not None:
7+
return score
8+
9+
return (max if is_maximizing else min)(
10+
minimax(new_state, is_maximizing=not is_maximizing)
11+
for new_state in possible_new_states(state)
12+
)
13+
14+
15+
def best_move(state):
16+
return max(
17+
(minimax(new_state, is_maximizing=False), new_state)
18+
for new_state in possible_new_states(state)
19+
)
20+
21+
22+
def possible_new_states(state):
23+
for pile, counters in enumerate(state):
24+
for remain in range(counters):
25+
yield state[:pile] + (remain,) + state[pile + 1 :]
26+
27+
28+
def evaluate(state, is_maximizing):
29+
if all(counters == 0 for counters in state):
30+
return -1 if is_maximizing else 1
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from functools import cache
2+
3+
4+
@cache
5+
def minimax(state, is_maximizing):
6+
if (score := evaluate(state, is_maximizing)) is not None:
7+
return score
8+
9+
return (max if is_maximizing else min)(
10+
minimax(new_state, is_maximizing=not is_maximizing)
11+
for new_state in possible_new_states(state)
12+
)
13+
14+
15+
def best_move(state):
16+
return max(
17+
(minimax(new_state, is_maximizing=False), new_state)
18+
for new_state in possible_new_states(state)
19+
)
20+
21+
22+
def possible_new_states(state):
23+
return [state - take for take in (1, 2, 3) if take <= state]
24+
25+
26+
def evaluate(state, is_maximizing):
27+
if state == 0:
28+
return 1 if is_maximizing else -1

python-minimax-nim/nim/game_nim.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import random
2+
3+
4+
def initial_state():
5+
return tuple(random.randint(3, 9) for _ in range(random.randint(3, 5)))
6+
7+
8+
def possible_new_states(state):
9+
for pile, counters in enumerate(state):
10+
for remain in range(counters):
11+
yield state[:pile] + (remain,) + state[pile + 1 :]
12+
13+
14+
def evaluate(state, is_maximizing):
15+
if all(counters == 0 for counters in state):
16+
return 1 if is_maximizing else -1
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import random
2+
3+
4+
def initial_state():
5+
return random.randint(10, 25)
6+
7+
8+
def possible_new_states(state):
9+
return [state - take for take in (1, 2, 3) if take <= state]
10+
11+
12+
def evaluate(state, is_maximizing):
13+
if state == 0:
14+
return 1 if is_maximizing else -1
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import random
2+
3+
4+
def initial_state():
5+
return (random.randint(6, 18),)
6+
7+
8+
def possible_new_states(state):
9+
for pile, counters in enumerate(state):
10+
for take in range(1, (counters + 1) // 2):
11+
yield state[:pile] + (counters - take, take) + state[pile + 1 :]
12+
13+
14+
def evaluate(state, is_maximizing):
15+
if all(counters <= 2 for counters in state):
16+
return -1 if is_maximizing else 1
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import random
2+
3+
4+
def initial_state():
5+
return tuple(random.randint(3, 9) for _ in range(random.randint(3, 5)))
6+
7+
8+
def possible_new_states(state):
9+
for pile, counters in enumerate(state):
10+
for remain in range(counters):
11+
yield state[:pile] + (remain,) + state[pile + 1 :]
12+
13+
14+
def evaluate(state, is_maximizing):
15+
if all(counters == 0 for counters in state):
16+
return -1 if is_maximizing else 1

0 commit comments

Comments
 (0)