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

83 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-01-27 05:44 +0100

1""" 

2@file 

3@brief Simple strategy for :epkg:`2048`. 

4""" 

5import random 

6import numpy 

7 

8 

9class GameOverException(RuntimeError): 

10 """ 

11 Raised when the game is over. 

12 """ 

13 pass 

14 

15 

16class Game2048State: 

17 """ 

18 To store additional information while guessing the best 

19 move. 

20 """ 

21 

22 def __init__(self, game): 

23 self.game = game 

24 

25 

26class Game2048: 

27 """ 

28 Implements the logic of the game :epkg:`2048`. 

29 """ 

30 

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) 

39 

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)}" 

47 

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 

51 

52 def copy(self): 

53 "Makes a copy of the game." 

54 return Game2048(self.game.copy()) 

55 

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 

69 

70 @staticmethod 

71 def process_line(line): 

72 """ 

73 Moves numbers inside a vector whether this vector represents 

74 a row or a column. 

75 

76 .. runpython:: 

77 :showcode: 

78 

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 

101 

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 

120 

121 def score(self): 

122 "Returns the maximum values." 

123 return numpy.max(self.game) 

124 

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. 

130 

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) 

146 

147 

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. 

153 

154 @param fct_strategy a function which returns the best move 

155 (see below) 

156 @return enumerator on scores 

157 

158 One example to show how to test a strategy: 

159 

160 .. runpython:: 

161 :showcode: 

162 

163 import random 

164 from ensae_teaching_cs.td_1a.cp2048 import evaluate_strategy 

165 

166 def random_strategy(game, state, moves): 

167 return random.randint(0, 3) 

168 

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