2016-08-15 08:22:33 +12:00
|
|
|
from discord.ext import commands
|
|
|
|
import discord
|
|
|
|
|
|
|
|
from .utils import config
|
|
|
|
from .utils import checks
|
|
|
|
|
|
|
|
import re
|
|
|
|
import random
|
|
|
|
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
class Board:
|
|
|
|
def __init__(self, player1, player2):
|
2016-08-18 08:46:20 +12:00
|
|
|
# Our board just needs to be a 3x3 grid. To keep formatting nice, each one is going to be a space to start
|
2016-08-16 23:12:36 +12:00
|
|
|
self.board = [[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
|
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# Randomize who goes first when the board is created
|
|
|
|
if random.SystemRandom().randint(0, 1):
|
|
|
|
self.challengers = {'x': player1, 'o': player2}
|
|
|
|
else:
|
|
|
|
self.challengers = {'x': player2, 'o': player1}
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# X's always go first
|
|
|
|
self.X_turn = True
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 09:03:39 +12:00
|
|
|
def full(self):
|
2016-08-18 08:46:20 +12:00
|
|
|
# For this check we just need to see if there is a space anywhere, if there is then we're not full
|
2016-08-15 09:03:39 +12:00
|
|
|
for row in self.board:
|
|
|
|
if ' ' in row:
|
|
|
|
return False
|
|
|
|
return True
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
def can_play(self, player):
|
2016-08-18 08:46:20 +12:00
|
|
|
# Simple check to see if the player is the one that's up
|
2016-08-15 08:22:33 +12:00
|
|
|
if self.X_turn:
|
|
|
|
return player == self.challengers['x']
|
|
|
|
else:
|
|
|
|
return player == self.challengers['o']
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
def update(self, x, y):
|
2016-08-18 08:46:20 +12:00
|
|
|
# If it's x's turn, we place an x, otherwise place an o
|
2016-08-15 08:55:55 +12:00
|
|
|
letter = 'x' if self.X_turn else 'o'
|
2016-08-18 08:46:20 +12:00
|
|
|
# Make sure the place we're trying to update is blank, we can't override something
|
2016-08-15 08:55:55 +12:00
|
|
|
if self.board[x][y] == ' ':
|
2016-08-15 08:57:12 +12:00
|
|
|
self.board[x][y] = letter
|
2016-08-15 08:55:55 +12:00
|
|
|
else:
|
|
|
|
return False
|
2016-08-18 08:46:20 +12:00
|
|
|
# If we were succesful in placing the piece, we need to switch whose turn it is
|
2016-08-15 08:22:33 +12:00
|
|
|
self.X_turn = not self.X_turn
|
2016-08-15 08:55:55 +12:00
|
|
|
return True
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
def check(self):
|
|
|
|
# Checking all possiblities will be fun...
|
|
|
|
# First base off the top-left corner, see if any possiblities with that match
|
2016-08-18 08:46:20 +12:00
|
|
|
# We need to also make sure that the place is not blank, so that 3 in a row that are blank doesn't cause a 'win'
|
|
|
|
# Top-left, top-middle, top right
|
2016-08-15 08:36:10 +12:00
|
|
|
if self.board[0][0] == self.board[0][1] and self.board[0][0] == self.board[0][2] and self.board[0][0] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[0][0]]
|
|
|
|
# Top-left, middle-left, bottom-left
|
2016-08-15 08:36:10 +12:00
|
|
|
if self.board[0][0] == self.board[1][0] and self.board[0][0] == self.board[2][0] and self.board[0][0] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[0][0]]
|
|
|
|
# Top-left, middle, bottom-right
|
2016-08-15 08:36:10 +12:00
|
|
|
if self.board[0][0] == self.board[1][1] and self.board[0][0] == self.board[2][2] and self.board[0][0] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[0][0]]
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# Next check the top-right corner, not re-checking the last possiblity that included it
|
2016-08-18 08:46:20 +12:00
|
|
|
# Top-right, middle-right, bottom-right
|
2016-08-15 08:36:10 +12:00
|
|
|
if self.board[0][2] == self.board[1][2] and self.board[0][2] == self.board[2][2] and self.board[0][2] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[0][2]]
|
|
|
|
# Top-right, middle, bottom-left
|
2016-08-15 08:37:45 +12:00
|
|
|
if self.board[0][2] == self.board[1][1] and self.board[0][2] == self.board[2][0] and self.board[0][2] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[0][2]]
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# Next up, bottom-right corner, only one possiblity to check here, other two have been checked
|
2016-08-18 08:46:20 +12:00
|
|
|
# Bottom-right, bottom-middle, bottom-left
|
2016-08-15 08:36:10 +12:00
|
|
|
if self.board[2][2] == self.board[2][1] and self.board[2][2] == self.board[2][0] and self.board[2][2] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[2][2]]
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# No need to check the bottom-left, all posiblities have been checked now
|
|
|
|
# Base things off the middle now, as we only need the two 'middle' possiblites that aren't diagonal
|
2016-08-18 08:46:20 +12:00
|
|
|
# Top-middle, middle, bottom-middle
|
2016-08-15 08:36:10 +12:00
|
|
|
if self.board[1][1] == self.board[0][1] and self.board[1][1] == self.board[2][1] and self.board[1][1] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[1][1]]
|
|
|
|
# Left-middle, middle, right-middle
|
2016-08-15 08:36:10 +12:00
|
|
|
if self.board[1][1] == self.board[1][0] and self.board[1][1] == self.board[1][2] and self.board[1][1] != ' ':
|
2016-08-18 08:46:20 +12:00
|
|
|
return self.challengers[self.board[1][1]]
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# Otherwise nothing has been found, return None
|
|
|
|
return None
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
def __str__(self):
|
2016-08-18 08:46:20 +12:00
|
|
|
# Simple formatting here when you look at it, enough spaces to even out where everything is
|
|
|
|
# Place whatever is at the grid in place, whether it's x, o, or blank
|
2016-08-15 08:31:05 +12:00
|
|
|
_board = " {} | {} | {}\n".format(self.board[0][0], self.board[0][1], self.board[0][2])
|
|
|
|
_board += "———————————————\n"
|
|
|
|
_board += " {} | {} | {}\n".format(self.board[1][0], self.board[1][1], self.board[1][2])
|
|
|
|
_board += "———————————————\n"
|
|
|
|
_board += " {} | {} | {}\n".format(self.board[2][0], self.board[2][1], self.board[2][2])
|
2016-08-15 08:22:33 +12:00
|
|
|
return "```\n{}```".format(_board)
|
2016-08-16 23:12:36 +12:00
|
|
|
|
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
class TicTacToe:
|
|
|
|
def __init__(self, bot):
|
|
|
|
self.bot = bot
|
|
|
|
self.boards = {}
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
def create(self, server_id, player1, player2):
|
|
|
|
self.boards[server_id] = Board(player1, player2)
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# Return whoever is x's so that we know who is going first
|
|
|
|
return self.boards[server_id].challengers['x']
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:28:01 +12:00
|
|
|
@commands.group(pass_context=True, aliases=['tic', 'tac', 'toe'], no_pm=True, invoke_without_command=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
2016-08-15 08:25:38 +12:00
|
|
|
async def tictactoe(self, ctx, *, option: str):
|
2016-08-15 09:11:32 +12:00
|
|
|
"""Updates the current server's tic-tac-toe board
|
|
|
|
You obviously need to be one of the players to use this
|
|
|
|
It also needs to be your turn
|
|
|
|
Provide top, left, bottom, right, middle as you want to mark where to play on the board"""
|
2016-08-15 08:22:33 +12:00
|
|
|
player = ctx.message.author
|
|
|
|
board = self.boards.get(ctx.message.server.id)
|
2016-08-18 08:46:20 +12:00
|
|
|
# Need to make sure the board exists before allowing someone to play
|
2016-08-15 08:22:33 +12:00
|
|
|
if not board:
|
|
|
|
await self.bot.say("There are currently no Tic-Tac-Toe games setup!")
|
|
|
|
return
|
2016-08-18 08:46:20 +12:00
|
|
|
# Now just make sure the person can play, this will fail if o's are up and x tries to play
|
|
|
|
# Or if someone else entirely tries to play
|
2016-08-15 08:22:33 +12:00
|
|
|
if not board.can_play(player):
|
|
|
|
await self.bot.say("You cannot play right now!")
|
|
|
|
return
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:22:33 +12:00
|
|
|
# Search for the positions in the option given, the actual match doesn't matter, just need to check if it exists
|
|
|
|
top = re.search('top', option)
|
|
|
|
middle = re.search('middle', option)
|
|
|
|
bottom = re.search('bottom', option)
|
|
|
|
left = re.search('left', option)
|
|
|
|
right = re.search('right', option)
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-18 08:46:20 +12:00
|
|
|
# Just a bit of logic to ensure nothing that doesn't make sense is given
|
2016-08-15 08:22:33 +12:00
|
|
|
if top and bottom:
|
|
|
|
await self.bot.say("That is not a valid location! Use some logic, come on!")
|
|
|
|
return
|
|
|
|
if left and right:
|
|
|
|
await self.bot.say("That is not a valid location! Use some logic, come on!")
|
|
|
|
return
|
2016-08-18 08:46:20 +12:00
|
|
|
# Make sure at least something was given
|
2016-08-15 08:22:33 +12:00
|
|
|
if not top and not bottom and not left and not right and not middle:
|
|
|
|
await self.bot.say("Please provide a valid location to play!")
|
|
|
|
return
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-31 10:33:46 +12:00
|
|
|
x = 0
|
|
|
|
y = 0
|
2016-08-15 09:11:32 +12:00
|
|
|
# Simple assignments
|
2016-08-15 08:22:33 +12:00
|
|
|
if top:
|
|
|
|
x = 0
|
|
|
|
if bottom:
|
|
|
|
x = 2
|
|
|
|
if left:
|
|
|
|
y = 0
|
|
|
|
if right:
|
|
|
|
y = 2
|
2016-08-18 08:46:20 +12:00
|
|
|
# If middle was given and nothing else, we need the exact middle
|
2016-08-15 09:11:32 +12:00
|
|
|
if middle and not (top or bottom or left or right):
|
|
|
|
x = 1
|
|
|
|
y = 1
|
2016-08-18 08:46:20 +12:00
|
|
|
# If just top or bottom was given, we assume this means top-middle or bottom-middle
|
|
|
|
# We don't need to do anything fancy with top/bottom as it's already assigned, just assign middle
|
2016-08-15 09:11:32 +12:00
|
|
|
if (top or bottom) and not (left or right):
|
|
|
|
y = 1
|
2016-08-18 08:46:20 +12:00
|
|
|
# If just left or right was given, we assume this means left-middle or right-middle
|
|
|
|
# We don't need to do anything fancy with left/right as it's already assigned, just assign middle
|
2016-08-15 09:11:32 +12:00
|
|
|
elif (left or right) and not (top or bottom):
|
|
|
|
x = 1
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-18 08:46:20 +12:00
|
|
|
# If all checks have been made, x and y should now be defined
|
|
|
|
# Correctly based on the matches, and we can go ahead and update the board
|
|
|
|
# We've already checked if the author can play, so there's no need to make any additional checks here
|
|
|
|
# board.update will handle which letter is placed
|
|
|
|
# If it returns false however, then someone has already played in that spot and nothing was updated
|
2016-08-15 09:04:37 +12:00
|
|
|
if not board.update(x, y):
|
|
|
|
await self.bot.say("Someone has already played there!")
|
|
|
|
return
|
2016-08-18 08:46:20 +12:00
|
|
|
# Next check if there's a winner
|
2016-08-15 08:22:33 +12:00
|
|
|
winner = board.check()
|
|
|
|
if winner:
|
2016-08-18 08:46:20 +12:00
|
|
|
# Get the loser based on whether or not the winner is x's
|
|
|
|
# If the winner is x's, the loser is o's...obviously, and vice-versa
|
2016-08-15 08:22:33 +12:00
|
|
|
loser = board.challengers['x'] if board.challengers['x'] != winner else board.challengers['o']
|
2016-08-16 23:12:36 +12:00
|
|
|
await self.bot.say("{} has won this game of TicTacToe, better luck next time {}".format(winner.display_name,
|
|
|
|
loser.display_name))
|
2016-08-18 08:46:20 +12:00
|
|
|
# Handle updating ratings based on the winner and loser
|
2016-09-29 12:39:34 +13:00
|
|
|
await config.update_records('tictactoe', winner, loser)
|
2016-08-18 08:46:20 +12:00
|
|
|
# This game has ended, delete it so another one can be made
|
2016-08-15 09:03:39 +12:00
|
|
|
del self.boards[ctx.message.server.id]
|
2016-08-15 08:22:33 +12:00
|
|
|
else:
|
2016-08-18 08:46:20 +12:00
|
|
|
# If no one has won, make sure the game is not full. If it has, delete the board and say it was a tie
|
2016-08-15 09:03:39 +12:00
|
|
|
if board.full():
|
|
|
|
await self.bot.say("This game has ended in a tie!")
|
|
|
|
del self.boards[ctx.message.server.id]
|
2016-08-18 08:46:20 +12:00
|
|
|
# If no one has won, and the game has not ended in a tie, print the new updated board
|
2016-08-15 09:03:39 +12:00
|
|
|
else:
|
2016-08-29 08:00:34 +12:00
|
|
|
player_turn = board.challengers.get('x') if board.X_turn else board.challengers.get('o')
|
2016-08-31 10:33:46 +12:00
|
|
|
fmt = str(board) + "\n{} It is now your turn to play!".format(player_turn.display_name)
|
|
|
|
await self.bot.say(fmt)
|
2016-08-16 23:12:36 +12:00
|
|
|
|
|
|
|
@tictactoe.command(name='start', aliases=['challenge', 'create'], pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
2016-08-15 08:26:55 +12:00
|
|
|
async def start_game(self, ctx, player2: discord.Member):
|
|
|
|
"""Starts a game of tictactoe with another player"""
|
|
|
|
player1 = ctx.message.author
|
2016-08-18 08:46:20 +12:00
|
|
|
# For simplicities sake, only allow one game on a server at a time.
|
|
|
|
# Things can easily get confusing (on the server's end) if we allow more than one
|
2016-08-16 23:12:36 +12:00
|
|
|
if self.boards.get(ctx.message.server.id) is not None:
|
2016-08-15 11:10:31 +12:00
|
|
|
await self.bot.say("Sorry but only one Tic-Tac-Toe game can be running per server!")
|
|
|
|
return
|
2016-08-21 13:55:52 +12:00
|
|
|
# Make sure we're not being challenged, I always win anyway
|
|
|
|
if player2 == ctx.message.server.me:
|
|
|
|
await self.bot.say("You want to play? Alright lets play.\n\nI win, so quick you didn't even notice it.")
|
|
|
|
return
|
2016-08-31 10:33:46 +12:00
|
|
|
|
2016-08-18 08:46:20 +12:00
|
|
|
# Create the board and return who has been decided to go first
|
2016-08-15 08:26:55 +12:00
|
|
|
x_player = self.create(ctx.message.server.id, player1, player2)
|
|
|
|
fmt = "A tictactoe game has just started between {} and {}".format(player1.display_name, player2.display_name)
|
2016-08-18 08:46:20 +12:00
|
|
|
# Print the board too just because
|
2016-08-15 08:26:55 +12:00
|
|
|
fmt += str(self.boards[ctx.message.server.id])
|
2016-08-31 10:33:46 +12:00
|
|
|
|
|
|
|
# We don't need to do anything weird with assigning x_player to something
|
|
|
|
# it is already a member object, just use it
|
2016-10-17 13:41:55 +13:00
|
|
|
fmt += "I have decided at random, and {} is going to be x's this game. It is your turn first! " \
|
2016-08-31 10:33:46 +12:00
|
|
|
"Use the {}tictactoe command, and a position, to choose where you want to play"\
|
|
|
|
.format(x_player.display_name, ctx.prefix)
|
2016-08-15 08:26:55 +12:00
|
|
|
await self.bot.say(fmt)
|
2016-08-31 10:33:46 +12:00
|
|
|
|
2016-08-22 13:13:39 +12:00
|
|
|
@tictactoe.command(name='delete', aliases=['stop', 'remove', 'end'], pass_context=True, no_pm=True)
|
2016-08-22 08:23:47 +12:00
|
|
|
@checks.custom_perms(kick_members=True)
|
|
|
|
async def stop_game(self, ctx):
|
|
|
|
"""Force stops a game of tictactoe
|
|
|
|
This should realistically only be used in a situation like one player leaves
|
|
|
|
Hopefully a moderator will not abuse it, but there's not much we can do to avoid that"""
|
|
|
|
if self.boards.get(ctx.message.server.id) is None:
|
|
|
|
await self.bot.say("There are no tictactoe games running on this server!")
|
|
|
|
return
|
2016-08-31 10:33:46 +12:00
|
|
|
|
2016-08-22 08:23:47 +12:00
|
|
|
del self.boards[ctx.message.server.id]
|
|
|
|
await self.bot.say("I have just stopped the game of TicTacToe, a new should be able to be started now!")
|
2016-08-15 08:24:31 +12:00
|
|
|
|
2016-08-16 23:12:36 +12:00
|
|
|
|
2016-08-15 08:24:31 +12:00
|
|
|
def setup(bot):
|
|
|
|
bot.add_cog(TicTacToe(bot))
|