"""
Simple strategy for :epkg:`2048`.
:githublink:`%|py|5`
"""
import random
import numpy
[docs]class GameOverException(RuntimeError):
"""
Raised when the game is over.
:githublink:`%|py|12`
"""
pass
[docs]class Game2048State:
"""
To store additional information while guessing the best
move.
:githublink:`%|py|20`
"""
[docs] def __init__(self, game):
self.game = game
[docs]class Game2048:
"""
Implements the logic of the game :epkg:`2048`.
:githublink:`%|py|29`
"""
[docs] def __init__(self, game=None):
"""
:param game: None or matrix 4x4
:githublink:`%|py|34`
"""
self.game = (game if game is not None
else numpy.zeros((4, 4), dtype=int))
self.moves = []
self.state = Game2048State(self)
[docs] def __str__(self):
"Displays the game as a string."
if len(self.moves) > 3:
last_moves = self.moves[-3:]
else:
last_moves = self.moves
return "{}\n{}".format(str(self.game), str(last_moves))
[docs] def gameover(self):
"Checks the game is over or not. Returns True in that case."
return numpy.ma.masked_not_equal(self.game, 0).count() == 0 # pylint: disable=E1101
[docs] def copy(self):
"Makes a copy of the game."
return Game2048(self.game.copy())
[docs] def next_turn(self):
"Adds a number in the game."
if self.gameover():
raise GameOverException("Game Over\n" + str(self.game))
else:
while True:
i = random.randint(0, self.game.shape[0] - 1)
j = random.randint(0, self.game.shape[1] - 1)
if self.game[i, j] == 0:
n = random.randint(0, 3)
self.game[i, j] = 4 if n == 0 else 2
self.moves.append((i, j, self.game[i, j]))
break
[docs] @staticmethod
def process_line(line):
"""
Moves numbers inside a vector whether this vector represents
a row or a column.
.. runpython::
:showcode:
from ensae_teaching_cs.td_1a.cp2048 import Game2048
print(Game2048.process_line([0, 2, 2, 4]))
:githublink:`%|py|81`
"""
res = []
for n in line:
if n == 0:
# Zero: skipped.
continue
if len(res) == 0:
# First number: add.
res.append(n)
else:
prev = res[-1]
if prev == n:
# The number is identical: combine.
res[-1] = 2 * n
else:
# Otherwise: add.
res.append(n)
while len(res) < len(line):
res.append(0)
return res
[docs] def play(self, direction):
"Updates the game after a direction was chosen."
if direction == 0:
lines = [Game2048.process_line(self.game[i, :])
for i in range(self.game.shape[0])]
self.game = numpy.array(lines)
elif direction == 1:
lines = [Game2048.process_line(self.game[:, i])
for i in range(self.game.shape[1])]
self.game = numpy.array(lines).T
elif direction == 2:
lines = [list(reversed(Game2048.process_line(self.game[i, ::-1])))
for i in range(self.game.shape[0])]
self.game = numpy.array(lines)
elif direction == 3:
lines = [list(reversed(Game2048.process_line(self.game[::-1, i])))
for i in range(self.game.shape[1])]
self.game = numpy.array(lines).T
[docs] def score(self):
"Returns the maximum values."
return numpy.max(self.game)
[docs] def best_move(self, game=None, state=None, moves=None):
"""
Selects the best move knowing the current game.
By default, selects a random direction.
This function must not modify the game.
:param game: 4x4 matrix or None for the current matrix
:param moves: all moves since the begining
:return: one integer
:githublink:`%|py|134`
"""
if game is None:
game = self.game
if state is None:
state = self.state
if moves is None:
moves = self.moves
if moves is None:
raise ValueError("moves cannot be None")
if not isinstance(game, numpy.ndarray) or game.shape != (4, 4):
raise ValueError("game must be a matrix (4x4).")
return random.randint(0, 3)
[docs]def evaluate_strategy(fct_strategy, ntries=10):
"""
Applies method *best_move* until gameover
starting from the current position. Repeats *ntries* times
and the maximum number in every try.
:param fct_strategy: a function which returns the best move
(see below)
:return: enumerator on scores
One example to show how to test a strategy:
.. runpython::
:showcode:
import random
from ensae_teaching_cs.td_1a.cp2048 import evaluate_strategy
def random_strategy(game, state, moves):
return random.randint(0, 3)
scores = list(evaluate_strategy(random_strategy))
print(scores)
:githublink:`%|py|171`
"""
for i in range(0, ntries):
g = Game2048()
while True:
try:
g.next_turn()
except (GameOverException, RuntimeError):
break
d = fct_strategy(g.game, g.state, g.moves)
g.play(d)
yield g.score()