def _playout(self, color, board, move, pid=None): """ 終了までゲームを進めて勝敗を返す """ remain = board.size * board.size - (board._black_score + board._white_score) if remain <= self.remain: playout_board = copy.deepcopy(board) # 現在の盤面をコピー playout_board.put_disc(color, *move) # 調べたい手を打つ # 勝敗が決まるまでゲームを進める next_color = 'white' if color == 'black' else 'black' # 相手の色を調べる game = Game(self._black_player, self._white_player, playout_board, NoneDisplay(), next_color) game.play() # 結果を返す win, ret = Game.BLACK_WIN if color == 'black' else Game.WHITE_WIN, -1 if game.result.winlose == win: ret = 1 # 勝った場合 elif game.result.winlose == Game.DRAW: ret = 0 # 引き分けた場合 else: ret = 0 # 盤面サイズが大きい場合は残り手数が減るまでしばらくランダムに打つ return ret
def do_shallow_scan(matrix, player, avail_moves, get_all=False): """Make a shallow scan on the surface of matrix""" result_list = [] for position in avail_moves: val = Game.predict_score(player, position, matrix) result_list.append([position, Utilities.calc_value(player, val)]) if get_all: return result_list return Game.get_best_pair(result_list)
def switch_player(self): """Switch current player""" opponent = None if self.current_player == Player.PLAYER: opponent = Player.COMPUTER elif self.current_player == Player.COMPUTER: opponent = Player.PLAYER # Check if there's any available moves for the opponent if not Game.get_available_moves(opponent, self.matrix): # Check if both players have no moves if not Game.get_available_moves(self.current_player, self.matrix): self.stop_game() if self.player_score > self.computer_score: dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "Well Done! You're good.") dialog.format_secondary_text("You beat computer for " + str(self.player_score - self.computer_score) + " points.") dialog.run() dialog.destroy() elif self.player_score < self.computer_score: dialog = Gtk.MessageDialog( self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "Luck may not always be by your side") dialog.format_secondary_text("Computer got over you by " + str(self.computer_score - self.player_score) + " points.") dialog.run() dialog.destroy() else: dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "I'm actually impressed!") dialog.format_secondary_text( "Wanna take another try? " + "You just got a draw with computer.") dialog.run() dialog.destroy() else: self.current_player = opponent # If there's any valid moves for computer and the game still continues, # let it make the way if self.current_player == Player.COMPUTER \ and self.game_state == GameStatus.PLAYING: self.make_move_ai()
def make_move_ai(self): """Makes move for AI :returns: none """ avail_moves = Game.get_available_moves(self.current_player, self.matrix) pair = [] if self.game_mode == GameMode.EASY: pair = Algorithm.do_shallow_scan(self.matrix, self.current_player, avail_moves) elif self.game_mode == GameMode.NORMAL: pair = Algorithm().do_minimax(self.depth - 1, self.matrix, self.current_player, avail_moves) else: # GameMode.HARD pair = Algorithm().do_alpha_beta_pruning(self.depth - 1, self.matrix, self.current_player, avail_moves) self.make_move(pair[0]) self.switch_player()
def make_move(self, position): """TODO: Make move at the given position to given player :player: current player :position: list of [x, y] :screen: screen to draw :returns: True if move made """ # Make the actual move and get the result score = Game.make_move(self.current_player, position, self.matrix) # Redraw screen self.screen.redraw() if score is False: # Invalid move return False # Update score label if self.current_player == Player.PLAYER: self.player_score += (score + 1) self.computer_score -= score else: self.computer_score += (score + 1) self.player_score -= score # Move to next turn self.panel.update_turn_label() self.panel.set_score(self.player_score, self.computer_score)
def test_game_init(self): class Black(AbstractStrategy): def next_move(self, color, board): return (0, 0) class White(AbstractStrategy): def next_move(self, color, board): return (0, 0) black = Player('black', 'Black', Black()) white = Player('white', 'White', White()) game = Game(black, white) self.assertIsInstance(game.black_player.strategy, Black) self.assertIsInstance(game.white_player.strategy, White) self.assertIsInstance(game.board, BitBoardMethods.CyBoard8_64bit.CythonBitBoard) self.assertEqual(game.players, [black, white]) self.assertEqual(game.black_player, game.players[0]) self.assertEqual(game.white_player, game.players[1]) self.assertIsInstance(game.display, NoneDisplay) self.assertEqual(game.cancel, None) self.assertEqual(game.result, [])
def test_game(self): class TestDisplay: def progress(self, board, black_player, white_player): print('display.progress', '\n' + str(board), black_player, white_player) def turn(self, player, legal_moves): print('display.turn', player, legal_moves) def move(self, player, legal_moves): print('display.move', player.move, legal_moves) def foul(self, player): print('display.foul', player) def win(self, player): print('display.win', player) def draw(self): print('display.draw') class TopLeft(AbstractStrategy): def next_move(self, color, board): return board.get_legal_moves(color)[0] class BottomRight(AbstractStrategy): def next_move(self, color, board): return board.get_legal_moves(color)[-1] class Foul(AbstractStrategy): def next_move(self, color, board): return (board.size // 2, board.size // 2) class TestCancel: class TestEvent: def is_set(self): print('cancel-event is set') return True event = TestEvent() p1 = Player('black', 'TopLeft', TopLeft()) p2 = Player('white', 'BottomRight', BottomRight()) p3 = Player('white', 'TopLeft', TopLeft()) p4 = Player('black', 'Foul', Foul()) p5 = Player('white', 'Foul', Foul()) # init game1 = Game(p1, p2, Board(4), TestDisplay()) self.assertIsInstance(game1.board, Board) self.assertIsInstance(game1.black_player.strategy, TopLeft) self.assertIsInstance(game1.white_player.strategy, BottomRight) self.assertEqual(game1.black_player, game1.players[0]) self.assertEqual(game1.white_player, game1.players[1]) self.assertEqual(game1.cancel, None) self.assertEqual(game1.result, []) game2 = Game(p1, p3, Board(4), TestDisplay()) self.assertIsInstance(game2.board, Board) self.assertIsInstance(game2.black_player.strategy, TopLeft) self.assertIsInstance(game2.white_player.strategy, TopLeft) self.assertEqual(game2.black_player, game2.players[0]) self.assertEqual(game2.white_player, game2.players[1]) self.assertEqual(game2.cancel, None) self.assertEqual(game2.result, []) game3 = Game(p1, p2, Board(4), TestDisplay(), 'white', TestCancel()) self.assertIsInstance(game3.board, Board) self.assertIsInstance(game3.black_player.strategy, TopLeft) self.assertIsInstance(game3.white_player.strategy, BottomRight) self.assertEqual(game3.black_player, game3.players[1]) self.assertEqual(game3.white_player, game3.players[0]) self.assertTrue(game3.cancel, TestCancel) self.assertEqual(game3.result, []) game4 = Game(p4, p2, Board(4), TestDisplay()) self.assertIsInstance(game4.board, Board) self.assertIsInstance(game4.black_player.strategy, Foul) self.assertIsInstance(game4.white_player.strategy, BottomRight) self.assertEqual(game4.black_player, game4.players[0]) self.assertEqual(game4.white_player, game4.players[1]) self.assertEqual(game4.cancel, None) self.assertEqual(game4.result, []) game5 = Game(p1, p5, Board(4), TestDisplay()) self.assertIsInstance(game5.board, Board) self.assertIsInstance(game5.black_player.strategy, TopLeft) self.assertIsInstance(game5.white_player.strategy, Foul) self.assertEqual(game5.black_player, game5.players[0]) self.assertEqual(game5.white_player, game5.players[1]) self.assertEqual(game5.cancel, None) self.assertEqual(game5.result, []) # play-black-win with captured_stdout() as stdout: game1.play() lines = stdout.getvalue().splitlines() output_ret = [ 'display.progress ', ' a b c d', ' 1□□□□', ' 2□●〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●BottomRight', 'display.turn 〇TopLeft [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.move (1, 0) [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.progress ', ' a b c d', ' 1□〇□□', ' 2□〇〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●BottomRight', 'display.turn ●BottomRight [(0, 0), (2, 0), (0, 2)]', 'display.move (0, 2) [(0, 0), (2, 0), (0, 2)]', 'display.progress ', ' a b c d', ' 1□〇□□', ' 2□〇〇□', ' 3●●●□', ' 4□□□□', ' 〇TopLeft ●BottomRight', 'display.turn 〇TopLeft [(0, 3), (1, 3), (2, 3), (3, 3)]', 'display.move (0, 3) [(0, 3), (1, 3), (2, 3), (3, 3)]', 'display.progress ', ' a b c d', ' 1□〇□□', ' 2□〇〇□', ' 3●〇●□', ' 4〇□□□', ' 〇TopLeft ●BottomRight', 'display.turn ●BottomRight [(0, 0), (2, 0)]', 'display.move (2, 0) [(0, 0), (2, 0)]', 'display.progress ', ' a b c d', ' 1□〇●□', ' 2□●●□', ' 3●〇●□', ' 4〇□□□', ' 〇TopLeft ●BottomRight', 'display.turn 〇TopLeft [(3, 0), (0, 1), (3, 2)]', 'display.move (3, 0) [(3, 0), (0, 1), (3, 2)]', 'display.progress ', ' a b c d', ' 1□〇〇〇', ' 2□●〇□', ' 3●〇●□', ' 4〇□□□', ' 〇TopLeft ●BottomRight', 'display.turn ●BottomRight [(3, 1), (1, 3)]', 'display.move (1, 3) [(3, 1), (1, 3)]', 'display.progress ', ' a b c d', ' 1□〇〇〇', ' 2□●〇□', ' 3●●●□', ' 4〇●□□', ' 〇TopLeft ●BottomRight', 'display.turn 〇TopLeft [(0, 1), (2, 3)]', 'display.move (0, 1) [(0, 1), (2, 3)]', 'display.progress ', ' a b c d', ' 1□〇〇〇', ' 2〇〇〇□', ' 3〇●●□', ' 4〇●□□', ' 〇TopLeft ●BottomRight', 'display.turn ●BottomRight [(0, 0)]', 'display.move (0, 0) [(0, 0)]', 'display.progress ', ' a b c d', ' 1●〇〇〇', ' 2〇●〇□', ' 3〇●●□', ' 4〇●□□', ' 〇TopLeft ●BottomRight', 'display.turn 〇TopLeft [(3, 2), (2, 3)]', 'display.move (3, 2) [(3, 2), (2, 3)]', 'display.progress ', ' a b c d', ' 1●〇〇〇', ' 2〇●〇□', ' 3〇〇〇〇', ' 4〇●□□', ' 〇TopLeft ●BottomRight', 'display.turn ●BottomRight [(3, 1), (3, 3)]', 'display.move (3, 3) [(3, 1), (3, 3)]', 'display.progress ', ' a b c d', ' 1●〇〇〇', ' 2〇●〇□', ' 3〇〇●〇', ' 4〇●□●', ' 〇TopLeft ●BottomRight', 'display.turn 〇TopLeft [(2, 3)]', 'display.move (2, 3) [(2, 3)]', 'display.progress ', ' a b c d', ' 1●〇〇〇', ' 2〇●〇□', ' 3〇〇〇〇', ' 4〇〇〇●', ' 〇TopLeft ●BottomRight', 'display.turn ●BottomRight [(3, 1)]', 'display.move (3, 1) [(3, 1)]', 'display.progress ', ' a b c d', ' 1●〇〇〇', ' 2〇●●●', ' 3〇〇〇●', ' 4〇〇〇●', ' 〇TopLeft ●BottomRight', 'display.win 〇TopLeft', ] self.assertEqual(lines, output_ret) self.assertEqual(game1.result.winlose, Game.BLACK_WIN) self.assertEqual(game1.result.black_name, 'TopLeft') self.assertEqual(game1.result.white_name, 'BottomRight') self.assertEqual(game1.result.black_num, 10) self.assertEqual(game1.result.white_num, 6) # play-white-win with captured_stdout() as stdout: game2.play() lines = stdout.getvalue().splitlines() output_ret = [ 'display.progress ', ' a b c d', ' 1□□□□', ' 2□●〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●TopLeft', 'display.turn 〇TopLeft [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.move (1, 0) [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.progress ', ' a b c d', ' 1□〇□□', ' 2□〇〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●TopLeft', 'display.turn ●TopLeft [(0, 0), (2, 0), (0, 2)]', 'display.move (0, 0) [(0, 0), (2, 0), (0, 2)]', 'display.progress ', ' a b c d', ' 1●〇□□', ' 2□●〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●TopLeft', 'display.turn 〇TopLeft [(0, 1), (3, 2), (2, 3)]', 'display.move (0, 1) [(0, 1), (3, 2), (2, 3)]', 'display.progress ', ' a b c d', ' 1●〇□□', ' 2〇〇〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●TopLeft', 'display.turn ●TopLeft [(2, 0), (0, 2)]', 'display.move (2, 0) [(2, 0), (0, 2)]', 'display.progress ', ' a b c d', ' 1●●●□', ' 2〇〇●□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●TopLeft', 'display.turn 〇TopLeft [(3, 0), (3, 1), (3, 2), (3, 3)]', 'display.move (3, 0) [(3, 0), (3, 1), (3, 2), (3, 3)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2〇〇〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●TopLeft', 'display.turn ●TopLeft [(0, 2), (3, 2), (1, 3)]', 'display.move (0, 2) [(0, 2), (3, 2), (1, 3)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2●●〇□', ' 3●●●□', ' 4□□□□', ' 〇TopLeft ●TopLeft', 'display.turn 〇TopLeft [(0, 3), (2, 3)]', 'display.move (0, 3) [(0, 3), (2, 3)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2●●〇□', ' 3●〇●□', ' 4〇□□□', ' 〇TopLeft ●TopLeft', 'display.turn ●TopLeft [(3, 1), (3, 2), (1, 3), (2, 3)]', 'display.move (3, 1) [(3, 1), (3, 2), (1, 3), (2, 3)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2●●●●', ' 3●〇●□', ' 4〇□□□', ' 〇TopLeft ●TopLeft', 'display.turn 〇TopLeft [(3, 2)]', 'display.move (3, 2) [(3, 2)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2●●●〇', ' 3●〇〇〇', ' 4〇□□□', ' 〇TopLeft ●TopLeft', 'display.turn ●TopLeft [(1, 3), (2, 3), (3, 3)]', 'display.move (1, 3) [(1, 3), (2, 3), (3, 3)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2●●●〇', ' 3●●〇〇', ' 4〇●□□', ' 〇TopLeft ●TopLeft', 'display.turn 〇TopLeft [(2, 3)]', 'display.move (2, 3) [(2, 3)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2●●●〇', ' 3●●〇〇', ' 4〇〇〇□', ' 〇TopLeft ●TopLeft', 'display.turn ●TopLeft [(3, 3)]', 'display.move (3, 3) [(3, 3)]', 'display.progress ', ' a b c d', ' 1●●●〇', ' 2●●●〇', ' 3●●●〇', ' 4〇〇〇●', ' 〇TopLeft ●TopLeft', 'display.win ●TopLeft', ] self.assertEqual(lines, output_ret) self.assertEqual(game2.result.winlose, Game.WHITE_WIN) self.assertEqual(game2.result.black_name, 'TopLeft') self.assertEqual(game2.result.white_name, 'TopLeft') self.assertEqual(game2.result.black_num, 6) self.assertEqual(game2.result.white_num, 10) # play-cancel-draw with captured_stdout() as stdout: game3.play() lines = stdout.getvalue().splitlines() output_ret = [ 'display.progress ', ' a b c d', ' 1□□□□', ' 2□●〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●BottomRight', 'cancel-event is set', 'display.draw', ] self.assertEqual(lines, output_ret) self.assertEqual(game3.result.winlose, Game.DRAW) self.assertEqual(game3.result.black_name, 'TopLeft') self.assertEqual(game3.result.white_name, 'BottomRight') self.assertEqual(game3.result.black_num, 2) self.assertEqual(game3.result.white_num, 2) # play-black-foul with captured_stdout() as stdout: game4.play() lines = stdout.getvalue().splitlines() output_ret = [ 'display.progress ', ' a b c d', ' 1□□□□', ' 2□●〇□', ' 3□〇●□', ' 4□□□□', ' 〇Foul ●BottomRight', 'display.turn 〇Foul [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.move (2, 2) [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.progress ', ' a b c d', ' 1□□□□', ' 2□●〇□', ' 3□〇〇□', ' 4□□□□', ' 〇Foul ●BottomRight', 'display.foul 〇Foul', 'display.win ●BottomRight', ] self.assertEqual(lines, output_ret) self.assertEqual(game4.result.winlose, Game.WHITE_WIN) self.assertEqual(game4.result.black_name, 'Foul') self.assertEqual(game4.result.white_name, 'BottomRight') self.assertEqual(game4.result.black_num, 3) self.assertEqual(game4.result.white_num, 1) # play-white-foul with captured_stdout() as stdout: game5.play() lines = stdout.getvalue().splitlines() output_ret = [ 'display.progress ', ' a b c d', ' 1□□□□', ' 2□●〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●Foul', 'display.turn 〇TopLeft [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.move (1, 0) [(1, 0), (0, 1), (3, 2), (2, 3)]', 'display.progress ', ' a b c d', ' 1□〇□□', ' 2□〇〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●Foul', 'display.turn ●Foul [(0, 0), (2, 0), (0, 2)]', 'display.move (2, 2) [(0, 0), (2, 0), (0, 2)]', 'display.progress ', ' a b c d', ' 1□〇□□', ' 2□〇〇□', ' 3□〇●□', ' 4□□□□', ' 〇TopLeft ●Foul', 'display.foul ●Foul', 'display.win 〇TopLeft', ] self.assertEqual(lines, output_ret) self.assertEqual(game5.result.winlose, Game.BLACK_WIN) self.assertEqual(game5.result.black_name, 'TopLeft') self.assertEqual(game5.result.white_name, 'Foul') self.assertEqual(game5.result.black_num, 4) self.assertEqual(game5.result.white_num, 1)
def do_alpha_beta_pruning(depth, matrix, player, avail_moves): """ Do minimax algorithm with alpha-beta pruning to reduce analysis time and improve AI level""" # Raise exception (unhandled case) if not avail_moves: raise Exception("Encountered Error!") result_list = [] # Determine the opponent if player == Player.PLAYER: opponent = Player.COMPUTER else: opponent = Player.PLAYER # # Reached to the deepest part of the given tree # if depth == 0: # Calculate all of the node's values and return the one # with highest value for position in avail_moves: val = Game.predict_score(player, position, matrix) result_list.append( [position, Utilities.calc_value(player, val)]) return Game.get_best_pair(result_list) # # On the normal state of tree. # # Sort input avail_moves = Utilities.sort(avail_moves) # Start to check input for actions for position in avail_moves: # Corner field. Get it at all costs! if position in Field.ADVANTAGE: val = Game.predict_score(player, position, matrix) return [position, Utilities.calc_value(player, val)] # Disadvantage field. Handle with cares. if position in Field.DISADVANTAGE: # Get player's flips and score player_flips = Game.get_flip_traces(player, position, matrix) player_flips.append(position) matrix_virtual = Utilities.clone_matrix(matrix) player_val = Game.make_move(player, position, matrix_virtual) # Dig down 1 level # Get opponent's possible moves opponent_moves = Game.get_available_moves( opponent, matrix_virtual) max_penalty = None # Check if any of player's flip is on the way of opponent for p in opponent_moves: # Who cares if opponent doesn't wanna take border? if p not in Field.BORDER: continue opponent_flips = Game.get_flip_traces( opponent, p, matrix_virtual) for f in player_flips: if f in opponent_flips: if max_penalty is None or \ max_penalty < (len(opponent_flips) + 1): max_penalty = len(opponent_flips) + 1 if max_penalty is None: # No danger thread. Take the advantage of border if position in Field.BORDER_DISADVANTAGE: return [ position, Utilities.calc_value(player, player_val) ] else: # Pass to next case (normal field) pass else: result_list.append([ position, Utilities.calc_value(player, player_val - max_penalty) ]) # Normal field # Check if opponent has move on the virtual matrix matrix_virtual = Utilities.clone_matrix(matrix) val = Game.make_move(player, position, matrix_virtual) # Check for next turn opponent_moves = Game.get_available_moves(opponent, matrix_virtual) if not opponent_moves: # Check if the player has move on the virtual matrix player_moves = Game.get_available_moves(player, matrix_virtual) if not player_moves: # Both has no move, reached to the end game. player_val, computer_val = \ Utilities.calc_matrix_score(matrix_virtual) # Set priority of end game with win flag on top if computer_val > player_val: return [position, 64] if player_val > computer_val: val = -64 result_list.append( [position, Utilities.calc_value(player, val)]) continue result_list.append( [position, Utilities.calc_value(player, 32)]) continue # Opponent has move. Process normally. best_move = Algorithm.do_alpha_beta_pruning( depth - 1, matrix_virtual, opponent, opponent_moves) result_list.append([position, best_move[1]]) return Game.get_best_pair(result_list)
def do_minimax(depth, matrix, player, avail_moves): """Do a plain minimax scan on the matrix for best position""" # Raise exception (unhandled case) if not avail_moves: raise Exception("Encountered Error!") result_list = [] # Determine the opponent if player == Player.PLAYER: opponent = Player.COMPUTER else: opponent = Player.PLAYER # # Reached to the deepest part of the given tree # if depth == 0: # Calculate all of the node's values and return the one # with highest value for position in avail_moves: val = Game.predict_score(player, position, matrix) result_list.append( [position, Utilities.calc_value(player, val)]) return Game.get_best_pair(result_list) for position in avail_moves: matrix_virtual = Utilities.clone_matrix(matrix) val = Game.make_move(player, position, matrix_virtual) # Check for next turn opponent_moves = Game.get_available_moves(opponent, matrix_virtual) if not opponent_moves: # Check if the player has move on the virtual matrix player_moves = Game.get_available_moves(player, matrix_virtual) if not player_moves: # Both has no move, reached to the end game. player_val, computer_val = \ Utilities.calc_matrix_score(matrix_virtual) # Set priority of end game with win flag on top if computer_val > player_val: return [position, 64] if player_val > computer_val: val = -64 result_list.append( [position, Utilities.calc_value(player, val)]) continue # Opponent has no moves at this point # One extra move for current player best_move = Algorithm.do_minimax(depth - 1, matrix_virtual, opponent, opponent_moves) result_list.append([position, best_move[1]]) # Opponent has move. Process normally. best_move = Algorithm.do_minimax(depth - 1, matrix_virtual, opponent, opponent_moves) result_list.append([position, best_move[1]]) return Game.get_best_pair(result_list)
from reversi.game import Game from reversi.players import Player_Human, Player_Weighted if __name__ == "__main__": g = Game(playerB=Player_Human(), playerW=Player_Weighted()) g.play()