Coverage for src/ensae_teaching_cs/td_1a/cp2048.py: 92%
83 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
1"""
2@file
3@brief Simple strategy for :epkg:`2048`.
4"""
5import random
6import numpy
9class GameOverException(RuntimeError):
10 """
11 Raised when the game is over.
12 """
13 pass
16class Game2048State:
17 """
18 To store additional information while guessing the best
19 move.
20 """
22 def __init__(self, game):
23 self.game = game
26class Game2048:
27 """
28 Implements the logic of the game :epkg:`2048`.
29 """
31 def __init__(self, game=None):
32 """
33 :param game: None or matrix 4x4
34 """
35 self.game = (game if game is not None
36 else numpy.zeros((4, 4), dtype=int))
37 self.moves = []
38 self.state = Game2048State(self)
40 def __str__(self):
41 "Displays the game as a string."
42 if len(self.moves) > 3:
43 last_moves = self.moves[-3:]
44 else:
45 last_moves = self.moves
46 return f"{str(self.game)}\n{str(last_moves)}"
48 def gameover(self):
49 "Checks the game is over or not. Returns True in that case."
50 return numpy.ma.masked_not_equal(self.game, 0).count() == 0 # pylint: disable=E1101
52 def copy(self):
53 "Makes a copy of the game."
54 return Game2048(self.game.copy())
56 def next_turn(self):
57 "Adds a number in the game."
58 if self.gameover():
59 raise GameOverException("Game Over\n" + str(self.game))
60 else:
61 while True:
62 i = random.randint(0, self.game.shape[0] - 1)
63 j = random.randint(0, self.game.shape[1] - 1)
64 if self.game[i, j] == 0:
65 n = random.randint(0, 3)
66 self.game[i, j] = 4 if n == 0 else 2
67 self.moves.append((i, j, self.game[i, j]))
68 break
70 @staticmethod
71 def process_line(line):
72 """
73 Moves numbers inside a vector whether this vector represents
74 a row or a column.
76 .. runpython::
77 :showcode:
79 from ensae_teaching_cs.td_1a.cp2048 import Game2048
80 print(Game2048.process_line([0, 2, 2, 4]))
81 """
82 res = []
83 for n in line:
84 if n == 0:
85 # Zero: skipped.
86 continue
87 if len(res) == 0:
88 # First number: add.
89 res.append(n)
90 else:
91 prev = res[-1]
92 if prev == n:
93 # The number is identical: combine.
94 res[-1] = 2 * n
95 else:
96 # Otherwise: add.
97 res.append(n)
98 while len(res) < len(line):
99 res.append(0)
100 return res
102 def play(self, direction):
103 "Updates the game after a direction was chosen."
104 if direction == 0:
105 lines = [Game2048.process_line(self.game[i, :])
106 for i in range(self.game.shape[0])]
107 self.game = numpy.array(lines)
108 elif direction == 1:
109 lines = [Game2048.process_line(self.game[:, i])
110 for i in range(self.game.shape[1])]
111 self.game = numpy.array(lines).T
112 elif direction == 2:
113 lines = [list(reversed(Game2048.process_line(self.game[i, ::-1])))
114 for i in range(self.game.shape[0])]
115 self.game = numpy.array(lines)
116 elif direction == 3:
117 lines = [list(reversed(Game2048.process_line(self.game[::-1, i])))
118 for i in range(self.game.shape[1])]
119 self.game = numpy.array(lines).T
121 def score(self):
122 "Returns the maximum values."
123 return numpy.max(self.game)
125 def best_move(self, game=None, state=None, moves=None):
126 """
127 Selects the best move knowing the current game.
128 By default, selects a random direction.
129 This function must not modify the game.
131 @param game 4x4 matrix or None for the current matrix
132 @param moves all moves since the begining
133 @return one integer
134 """
135 if game is None:
136 game = self.game
137 if state is None:
138 state = self.state
139 if moves is None:
140 moves = self.moves
141 if moves is None:
142 raise ValueError("moves cannot be None")
143 if not isinstance(game, numpy.ndarray) or game.shape != (4, 4):
144 raise ValueError("game must be a matrix (4x4).")
145 return random.randint(0, 3)
148def evaluate_strategy(fct_strategy, ntries=10):
149 """
150 Applies method *best_move* until gameover
151 starting from the current position. Repeats *ntries* times
152 and the maximum number in every try.
154 @param fct_strategy a function which returns the best move
155 (see below)
156 @return enumerator on scores
158 One example to show how to test a strategy:
160 .. runpython::
161 :showcode:
163 import random
164 from ensae_teaching_cs.td_1a.cp2048 import evaluate_strategy
166 def random_strategy(game, state, moves):
167 return random.randint(0, 3)
169 scores = list(evaluate_strategy(random_strategy))
170 print(scores)
171 """
172 for i in range(0, ntries):
173 g = Game2048()
174 while True:
175 try:
176 g.next_turn()
177 except (GameOverException, RuntimeError):
178 break
179 d = fct_strategy(g.game, g.state, g.moves)
180 g.play(d)
181 yield g.score()