Archived
1
0
Fork 0
This repository has been archived on 2024-04-26. You can view files and clone it, but cannot push or open issues or pull requests.
akari-bot/modules/tic_tac_toe/__init__.py
多羅狼 fcbeb342a1
upd
2023-11-06 02:07:37 +08:00

314 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import math
import random
from typing import Awaitable, Callable, List, Tuple
from core.builtins import Bot
from core.component import module
from core.petal import gained_petal
tic_tac_toe = module('tic_tac_toe',
desc='{tic_tac_toe.help.desc}', developers=['Dianliang233'],
alias=['ttt', 'tictactoe'])
play_state = {}
def check_winner(board: List[List[int]]):
# left-right diagonal
if board[0][0] == board[1][1] == board[2][2] != 0:
return board[0][0]
# right-left diagonal
elif board[0][2] == board[1][1] == board[2][0] != 0:
return board[0][2]
for i in range(3):
# vertical
if board[i][0] == board[i][1] == board[i][2] != 0:
return board[i][0]
# horizontal
elif board[0][i] == board[1][i] == board[2][i] != 0:
return board[0][i]
return None
GameBoard = List[List[int]]
GameCallback = Callable[[List[List[int]]], Awaitable[Tuple[int]]]
class TerminationError(Exception):
pass
async def game(msg: Bot.MessageSession,
x_callback: GameCallback,
o_callback: GameCallback) -> 0 | 1 | 2:
# 0 = empty; 1 = x; 2 = o
board = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
while True:
if not play_state[msg.target.target_id]['active']:
raise TerminationError
x = await x_callback(board)
board[x[0]][x[1]] = 1
if winner := check_winner(board):
return winner, board
spaces = []
for i in range(3):
for j in range(3):
if board[i][j] == 0:
spaces.append((i, j))
if not spaces:
return 0, board
o = await o_callback(board)
board[o[0]][o[1]] = 2
if winner := check_winner(board):
return winner, board
spaces = []
for i in range(3):
for j in range(3):
if board[i][j] == 0:
spaces.append((i, j))
if not spaces:
return 0, board
def format_board(board: GameBoard):
return '\n'.join([' '.join(['' if i == 1 else '' if i == 2 else '' for i in row]) for row in board])
def generate_human_callback(msg: Bot.MessageSession, player: str):
async def callback(board: List[List[int]]):
await msg.send_message(format_board(board) + f'\n{msg.locale.t("tic_tac_toe.message.turn", player=player)}', quote=False)
while True:
if not play_state[msg.target.target_id]['active']:
raise TerminationError
wait = await msg.wait_anyone()
text = wait.as_display(text_only=True)
if text == 'stop':
raise TerminationError
# remove space
text = text.replace(' ', '')
text = text.replace(',', '')
if len(text) == 1:
try:
digit = int(text)
except ValueError:
continue
x = (digit - 1) // 3
y = (digit - 1) % 3
if board[x][y] == 0:
return x, y
else:
continue
elif len(text) == 2:
try:
x = int(text[0]) - 1
y = int(text[1]) - 1
except ValueError:
continue
if board[x][y] == 0:
return x, y
else:
continue
else:
continue
return callback
def is_move_left(board: GameBoard):
for i in range(3):
for j in range(3):
if (board[i][j] == 0):
return True
return False
def evaluate(b: GameBoard, worst: bool = False) -> int:
if worst:
return 10 if check_winner(b) == 1 else -10 if check_winner(b) == 2 else 0
return 10 if check_winner(b) == 2 else -10 if check_winner(b) == 1 else 0
def minimax(board: GameBoard, depth: int, is_max: bool, worst: bool = False):
score = evaluate(board, worst=worst)
player = 2
opponent = 1
if (score == 10):
return score
if (score == -10):
return score
if (not is_move_left(board)):
return 0
# If this maximizer's move
if (is_max):
best = -1000
# Traverse all cells
for i in range(3):
for j in range(3):
# Check if cell is empty
if (board[i][j] == 0):
# Make the move
board[i][j] = player
# Call minimax recursively and choose
# the maximum value
best = max(best, minimax(board,
depth + 1,
not is_max, worst=worst))
# Undo the move
board[i][j] = 0
return best
# If this minimizer's move
else:
best = 1000
# Traverse all cells
for i in range(3):
for j in range(3):
# Check if cell is empty
if (board[i][j] == 0):
# Make the move
board[i][j] = opponent
# Call minimax recursively and choose
# the minimum value
best = min(best, minimax(board, depth + 1, not is_max, worst=worst))
# Undo the move
board[i][j] = 0
return best
def find_best_move(board, worst=False):
player = 2
# opponent = 1
best_val = -1000
best_move = (-1, -1)
# Traverse all cells, evaluate minimax function for
# all empty cells. And return the cell with optimal
# value.
for i in range(3):
for j in range(3):
# Check if cell is empty
if (board[i][j] == 0):
# Make the move
board[i][j] = player
# compute evaluation function for this
# move.
move_val = minimax(board, 0, False, worst=False)
# Undo the move
board[i][j] = 0
# If the value of the current move is
# more than the best value, then update
# best/
if (move_val > best_val):
best_move = (i, j)
best_val = move_val
return best_move
async def master_bot_callback(board: GameBoard):
return find_best_move(board)
async def noob_bot_callback(board: GameBoard):
return find_best_move(board, worst=True)
async def expert_bot_callback(board: GameBoard):
if random.randint(0, 4) == 0:
return await random_bot_callback(board)
return find_best_move(board)
async def random_bot_callback(board: GameBoard):
random_spaces = []
for i in range(3):
for j in range(3):
if board[i][j] == 0:
random_spaces.append((i, j))
return random.choice(random_spaces)
@tic_tac_toe.command('stop {{game.help.stop}}')
async def terminate(msg: Bot.MessageSession):
state = play_state.get(msg.target.target_id, {}) # 尝试获取 play_state 中是否有此对象的游戏状态
if state: # 若有
if state['active']: # 检查是否为活跃状态
play_state[msg.target.target_id]['active'] = False # 标记为非活跃状态
await msg.finish(msg.locale.t('game.message.stop'))
else:
await msg.finish(msg.locale.t('game.message.stop.none'))
else:
await msg.finish(msg.locale.t('game.message.stop.none'))
@tic_tac_toe.command('{{tic_tac_toe.help}}')
@tic_tac_toe.command('noob {{tic_tac_toe.noob.help}}')
@tic_tac_toe.command('expert {{tic_tac_toe.expert.help}}')
@tic_tac_toe.command('master {{tic_tac_toe.master.help}}')
async def ttt_with_bot(msg: Bot.MessageSession):
if msg.parsed_msg:
if 'expert' in msg.parsed_msg:
game_type = 'expert'
bot_callback = expert_bot_callback
elif 'master' in msg.parsed_msg:
game_type = 'master'
bot_callback = master_bot_callback
elif 'noob' in msg.parsed_msg:
game_type = 'noob'
bot_callback = noob_bot_callback
else:
game_type = 'random'
bot_callback = random_bot_callback
if msg.target.target_id in play_state and play_state[msg.target.target_id]['active']:
await msg.finish(msg.locale.t('game.message.running'))
play_state.update({msg.target.target_id: {'active': True}})
try:
winner, board = await game(msg, generate_human_callback(msg, 'X'), bot_callback)
except TerminationError:
return
play_state[msg.target.target_id]['active'] = False
g_msg = ''
if winner == 0:
await msg.finish(format_board(board) + '\n' + msg.locale.t('tic_tac_toe.message.draw'), quote=False)
if winner == 1:
if game_type == 'random' and (reward := await gained_petal(msg, 1)):
g_msg = '\n' + reward
if game_type == 'expert' and (reward := await gained_petal(msg, 2)):
g_msg = '\n' + reward
await msg.finish(format_board(board) + '\n' + msg.locale.t('tic_tac_toe.message.winner', winner='X' if winner == 1 else 'O') + g_msg, quote=False)
@tic_tac_toe.command('duo {{tic_tac_toe.duo.help}}')
async def ttt_multiplayer(msg: Bot.MessageSession):
if msg.target.target_id in play_state and play_state[msg.target.target_id]['active']:
await msg.finish(msg.locale.t('game.message.running'))
play_state.update({msg.target.target_id: {'active': True}})
try:
winner, board = await game(msg, generate_human_callback(msg, 'X'), generate_human_callback(msg, 'O'))
except TerminationError:
return
play_state[msg.target.target_id]['active'] = False
if winner == 0:
await msg.finish(format_board(board) + '\n' + msg.locale.t('tic_tac_toe.message.draw'), quote=False)
await msg.finish(format_board(board) + '\n' + msg.locale.t('tic_tac_toe.message.winner', winner='X' if winner == 1 else 'O'), quote=False)