זה נושא דיון מלווה לערך המקורי ב- https://www.tocode.co.il/bundles/python/lessons/25-code-sharing-lab
זה נושא דיון מלווה לערך המקורי ב- https://www.tocode.co.il/bundles/python/lessons/25-code-sharing-lab
הקוד שלי למשחק איקס-עיגול לפי הוראות התרגיל:
"""A Tic-Tac-Toe game.
Two players, one human and the other an AI, will take terns adding
their shape to squares in the board. Their goal is to win by placing
3 in a line - horizontal, vertical or diagonal.
After a game ends, with one of the players winning or with a tie, a
new game will start.
Overall scoreboard will be displayed at the end of each game.
"""
import random
from collections import deque
from time import sleep
class Board:
"""Tic-Tac-Toe game board."""
shapes = ('X', 'O') # Allowed shapes
def __init__(self):
"""Initialize with a clear board."""
self.board = []
self.clear()
def clear(self):
"""Set the game board representation with empty squares (starting position)
:return: None
"""
self.board = [[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
def display(self):
"""Printout the bored with the current squares shapes and coordinates.
:return: None
"""
print(' ' + ' '.join([str(i) for i in range(len(self.board))]))
for y in range(len(self.board)):
print(str(y) + ' ' + ' | '.join(self.board[y]) + ' ')
if len(self.board) != y + 1:
print(' ---' + '+---' * (len(self.board[y]) - 1))
print()
def move(self, x, y, shape):
"""Make a move on the board - placing given shape in given board coordinates.
An exception will be raised if no valid shape is given, or if
the board square at given coordinates isn't empty.
:param x: int column coordinate.
:param y: int row coordinate.
:param shape: str name of the shape to be placed.
:return: None
"""
if shape not in Board.shapes:
raise UserWarning(f"Given shape '{shape}' is not a valid shape.")
if ' ' != self.board[y][x]:
raise UserWarning(f'Square {x},{y} is not empty.')
self.board[y][x] = shape
def scan_board(self, cell_callback, default_return_val=None):
"""A utility for scanning the squares in the board.
:param cell_callback: function to be called for each square in the board.
:param default_return_val: [optional] the value to be returned if nothing was returned earlier.
:return: None or what was defined at cell_callback/default_return_val.
"""
for y in range(len(self.board)):
for x in range(len(self.board[y])):
callback_res = cell_callback(self.board[y][x], x, y)
if callback_res is not None:
return callback_res
return default_return_val
def has_more_moves(self):
"""Check if there are any more moves available in the game board.
:return: True if there are more moves; False if there aren't.
"""
return self.scan_board(lambda cell, *_: True if ' ' == cell else None, False)
def board_to_list(self):
"""A conversion utility from a 2D matrix (list of lists) to a 1D vector list.
:return: list representing the board with 9 indexes.
"""
board_list = []
for y in range(len(self.board)):
for x in range(len(self.board[y])):
board_list.append(len(board_list) if 'X' != self.board[y][x] and 'O' != self.board[y][x] else self.board[y][x])
return board_list
@staticmethod
def list_index_to_board_coordinates(index):
"""A conversion utility from a 1D board list index to 2D board coordinates.
:param index: int index of 1D board list.
:return: int, int representing 2D board coordinates.
"""
y = int(index / 3)
x = index - (y * 3)
return x, y
def winning(self, shape, b=None):
"""Check if given shape is in winning state at game board.
A game is won if the shape is places 3 times in a line - horizontal, vertical or diagonal.
:param shape: str shape name.
:param b: list [optional] 1D board list. If not given, the current game board will be used.
:return: True if game has being won by given shape; False othewise.
"""
if b is None:
b = self.board_to_list()
if (b[0] == shape and b[1] == shape and b[2] == shape) or \
(b[3] == shape and b[4] == shape and b[5] == shape) or \
(b[6] == shape and b[7] == shape and b[8] == shape) or \
(b[0] == shape and b[3] == shape and b[6] == shape) or \
(b[1] == shape and b[4] == shape and b[7] == shape) or \
(b[2] == shape and b[5] == shape and b[8] == shape) or \
(b[0] == shape and b[4] == shape and b[8] == shape) or \
(b[2] == shape and b[4] == shape and b[6] == shape):
return True
else:
return False
class Player:
"""A Tic-Tac-Toe player.
A player will have its shape and number of wins.
"""
def __init__(self, shape):
if shape not in Board.shapes:
raise UserWarning(f'Invalid shape given ({shape})')
self.shape = shape
self.wins = 0
class HumanPlayer(Player):
"""A Tic-Tac-Toe human player."""
def __init__(self, name='Jon', shape='X'):
super().__init__(shape)
self.name = name
def next_move(self):
"""Ask human player for its next move coordinates.
:return: int, int representing 2D board coordinates.
"""
next_move = input()
x, y = next_move.split(',')
return x, y
class AIPlayer(Player):
"""A Tic-Tac-Toe computer AI (Artificial intelligence) player."""
def __init__(self, shape='O'):
super().__init__(shape)
self.name = "Bot"
def next_move(self):
"""Compute next move coordinates using minimax algorithm.
:return: int, int representing 2D board coordinates.
"""
original_board = bord.board_to_list()
shapes_map = {'AI': 'X', 'HUM': 'O'} if 'X' == self.shape else {'AI': 'O', 'HUM': 'X'}
best_move = self.minmax(original_board, 'AI', shapes_map)
return Board.list_index_to_board_coordinates(best_move['index'])
def minmax(self, new_board, player, shapes_map):
"""Minimax algorithm to find best next move recursively.
Go over each possible move and score it, returning highest score move.
Stop condition is one of the players winning or no more moves.
:param new_board: list 1D game board representation.
:param player: str player representation (AI or HUM).
:param shapes_map: dict mapping players to their shapes.
:return: dict with keys: 'index' representing next move; 'score' for the move.
"""
avil_moves = [i for i in new_board if 'X' != i and 'O' != i]
if bord.winning(shapes_map['HUM'], new_board):
return {'score': -10}
elif bord.winning(shapes_map['AI'], new_board):
return {'score': 10}
elif len(avil_moves) == 0: # TIE
return {'score': 0}
moves = []
for move_index in avil_moves:
move = {'index': move_index}
move_board = new_board.copy()
move_board[move_index] = shapes_map[player]
minmax_res = self.minmax(move_board, 'AI' if 'AI' != player else 'HUM', shapes_map)
move['score'] = minmax_res['score']
moves.append(move)
best_move = None
best_score = -100 if 'AI' == player else 100
for move in moves:
if ('AI' == player and move['score'] > best_score) or ('HUM' == player and move['score'] < best_score):
best_score = move['score']
best_move = move
return best_move
def make_players_and_insert_to_queue():
"""Make human and AI Playeres, and insert them to the global players_queue.
Prompt human player for its name and use it in its object creation.
:return: None
"""
human_player_name = input('What is your name human player? ')
players_queue.append(HumanPlayer(human_player_name))
players_queue.append(AIPlayer())
def randomly_order_players():
"""Players are randomly ordered in the global players_queue (a deque).
1st player in queue will be the X shape and will start the game.
2nd player will get the O shape and play next.
:return: None
"""
if random.random() > 0.5: # 50% to change existing order.
players_queue.append(players_queue.popleft())
players_queue[0].shape = Board.shapes[0]
players_queue[1].shape = Board.shapes[1]
def get_and_make_player_next_move(player):
"""Get next move of given player, and make in on the game board.
Ready to handle some exceptions by giving an error message and
asking again for player next move.
:param player: a Player object (with next_move function)
:return: None
"""
while True:
print(f"{player.name}, what's your next move?")
try:
x, y = player.next_move()
except ValueError:
print('Invalid move format. Use: column,row (0,0 is top left; 2,2 is bottom right).')
continue
try:
bord.move(int(x), int(y), player.shape)
except UserWarning as e:
print(f'Invalid move: {e}')
continue
except IndexError:
print(f'Invalid move: {x},{y} is out of range. Valid coordinate range is 0-2.')
continue
except ValueError:
print('Invalid move format. Use numbers only for column and row.')
continue
return None
def display_scoreboard():
"""Printout the scores of the players."""
print('Scoreboard - ', end='')
for player in sorted(players_queue, key=lambda p: p.wins, reverse=True):
print(f'{player.name}: {player.wins};', end=' ')
print()
def play_single_game():
"""A single Tic-Tac-Toe game.
Using the global bord and players_queue.
:return: None
"""
while True:
next_player = players_queue.popleft()
get_and_make_player_next_move(next_player)
players_queue.append(next_player)
bord.display()
if bord.winning(next_player.shape):
next_player.wins += 1
print(f'{next_player.name} wins the game.')
return None
if not bord.has_more_moves():
print(f'No more moves. Game ended with no winner.')
return None
sleep(1)
""" pre-games setup """
bord = Board()
players_queue = deque()
make_players_and_insert_to_queue()
""" Games main loop """
games_count = 0
while True:
games_count += 1
randomly_order_players()
bord.clear()
# pre-game instructions and [empty] board display
print(f"""
Game number: {games_count}
{players_queue[0].name} will start, playing '{players_queue[0].shape}'.
{players_queue[1].name} will play next with '{players_queue[1].shape}'.
Each turn type the square position for your next move as: column,row (0,0 is top left; 2,2 is bottom right)
""")
bord.display()
sleep(2)
play_single_game()
display_scoreboard()
sleep(4)
2 לייקים
נראה מעולה ואהבתי את השימוש ב minmax, נשמע שיהיה קשה מאוד לנצח את שחקן המחשב שלך.
רק שתי מחשבות בזמן שקראתי את הקוד-
-
למה הפונקציה display לא השתמש ב scan_board ?
-
לגבי ההמרה מלוח (רשימה-של-רשימות) לרשימה רגילה, אפשר גם להשתמש במשהו כזה:
flattened_list = [y for x in list_of_lists for y in x]
מפה התחנה הבאה זה פרק פיתוח GUI עם Python ו Qt ואז לכתוב UI גרפי יפה למשחק
הפוקציה הזו נראת אצלי כך:
print(' ' + ' '.join([str(i) for i in range(len(self.board))]))
for y in range(len(self.board)):
print(str(y) + ' ' + ' | '.join(self.board[y]) + ' ')
if len(self.board) != y + 1:
print(' ---' + '+---' * (len(self.board[y]) - 1))
print()
והפלט שלה נראה כך:
0 1 2
0 | |
---+---+---
1 | |
---+---+---
2 | |
[זה כשהלוח עדיין ריק. בשלבים מתקדמים במשחק גם הצורות של השחקנים יהיו בו.]
שימוש ב-scan_board כדי להשיג את אותה תוצאה היה דורש מאמץ, שלא מצדיק את עצמו בעיני.
לא ידעתי על האפשרות לשירשור כזה בתוך list comprehension. תודה על שיידעת אותי!
לייק 1
זה מדהים! ואוו כל הכבוד!
הנה הטייק שלי לתרגיל
# tic-tac-toe
from random import randint
class Game:
def __init__(self, humanplayername):
self.Marks = ('x','o')
self.players = (HumanPlayer(humanplayername),AIPlayer())
self.score = {self.players[0].name: 0, self.players[1].name: 0}
self.AnotherRound = True
while(self.AnotherRound):
self._GameReset()
self.Run()
self._AnotherGameQuery()
if(self.score[self.players[0].name] > self.score[self.players[1].name]):
print(f"Final result: {self.score}, {self.players[0].name} wins!")
elif(self.score[self.players[0].name] < self.score[self.players[1].name]):
print(f"Final result: {self.score}, {self.players[1].name} wins!")
else:
print(f"Final result: {self.score}, it's a draw!")
def Win(self, playername):
self.score[playername] += 1
def _GameReset(self):
self.CurrentGameDone = False
# Determine marks for the 2 players
coinflip = randint(0, 1)
if (coinflip == 0): # Heads
self.players[0].mark = self.Marks[0]
self.players[1].mark = self.Marks[1]
self.CurrentTurn = self.players[0]
else: # Tails
self.players[0].mark = self.Marks[1]
self.players[1].mark = self.Marks[0]
self.CurrentTurn = self.players[1]
self.board = [['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
print(f"Starting new game.\nPlayers: {self.players[0].name}(x) vs {self.players[1].name}(o)")
print(f"Score status: {self.score}")
self._PrintBoard()
def _PrintBoard(self):
print('Board status:')
for i in range(len(self.board)):
for j in range(len(self.board[i])):
print(f"{self.board[i][j]:3}", end="")
print()
def _WinCon(self):
if((self.board[0][0] == self.board[0][1] == self.board[0][2] or
self.board[0][0] == self.board[1][0] == self.board[2][0] or
self.board[0][0] == self.board[1][1] == self.board[2][2]) and not self.board[0][0] == '.'): # series in place (0,0)
WinningMark = self.board[0][0]
elif(self.board[0][1] == self.board[1][1] == self.board[2][1] and not self.board[0][1] == '.'): # series in place (0,1)
WinningMark = self.board[0][1]
elif((self.board[0][2] == self.board[1][2] == self.board[2][2] or
self.board[0][2] == self.board[1][1] == self.board[2][0]) and not self.board[0][2] == '.'): # series in place (0,2)
WinningMark = self.board[0][2]
elif(self.board[1][0] == self.board[1][1] == self.board[1][2] and not self.board[1][0] == '.'): # series in place (1,0)
WinningMark = self.board[1][0]
elif(self.board[2][0] == self.board[2][1] == self.board[2][2] and not self.board[2][0] == '.'): # series in place (2,0)
WinningMark = self.board[2][0]
else:
WinningMark = None
if(WinningMark is not None):
for player in self.players:
if(player.mark == WinningMark):
self.Win(player.name)
self.CurrentGameDone = True
return None
def _AnotherGameQuery(self):
ans = input('Would you like to play anothe round? (Y/N)')
if(ans == 'Y'):
self.AnotherRound = True
return None
elif(ans == 'N'):
self.AnotherRound = False
return None
else:
print('Invalid input')
self._AnotherGameQuery()
def Run(self):
while(not self.CurrentGameDone):
print(f"Current turn: {self.CurrentTurn.name}")
i, j = self.CurrentTurn.NextMove(self.board)
self.board[i][j] = self.CurrentTurn.mark
self._PrintBoard()
for player in self.players: # Switch turn to the other player
if(not player == self.CurrentTurn):
self.CurrentTurn = player
break
self._WinCon()
class HumanPlayer:
def __init__(self,name: str):
self.name = name
self.mark = None
def NextMove(self, board):
i_idx_Done = False
j_idx_Done = False
next_move = tuple(input("What's your next move? type the square position as (row, column)"))
for char in next_move:
try:
integer = int(char)
if(not i_idx_Done):
i = integer
i_idx_Done = True
elif(not j_idx_Done):
j = integer
j_idx_Done = True
else:
print('Invalid input, try again')
i, j = self.NextMove(board)
break
except ValueError:
continue
if(not i_idx_Done or not j_idx_Done):
print('Invalid input, try again')
i, j = self.NextMove(board)
if((i < 0 or i > 2) or (j < 0 or j > 2)):
print('Input out of bounds, try again')
i, j = self.NextMove(board)
if(not board[i][j] == '.'):
print('Place is already occupied, try again')
i, j = self.NextMove(board)
return i, j
class AIPlayer:
def __init__(self, name = 'Bot'):
self.name = name
self.mark = None
def NextMove(self,board):
for i in range(len(board)):
for j in range(len(board[i])):
if(board[i][j] == '.'):
return i, j
# Running script
Game('Human')