# Coverage for src/ensae_teaching_cs/td_1a/cp2048.py: 92%

## 83 statements

, 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:

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:

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()