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-18 08:46:20 +12:00
def update_records ( winner , loser ) :
# This is the exact same formula as the battling update.
# The only difference is I use the word "match" instead of "battle"
2016-08-19 18:13:03 +12:00
matches = config . get_content ( ' tictactoe ' ) or { }
2016-08-18 08:46:20 +12:00
winner_stats = matches . get ( winner . id ) or { }
winner_rating = winner_stats . get ( ' rating ' ) or 1000
loser_stats = matches . get ( loser . id ) or { }
loser_rating = loser_stats . get ( ' rating ' ) or 1000
difference = abs ( winner_rating - loser_rating )
rating_change = 0
count = 25
while count < = difference :
if count > 300 :
break
rating_change + = 1
count + = 25
if winner_rating > loser_rating :
winner_rating + = 16 - rating_change
loser_rating - = 16 - rating_change
else :
winner_rating + = 16 + rating_change
loser_rating - = 16 + rating_change
winner_wins = winner_stats . get ( ' wins ' ) or 0
winner_losses = winner_stats . get ( ' losses ' ) or 0
loser_wins = loser_stats . get ( ' wins ' ) or 0
loser_losses = loser_stats . get ( ' losses ' ) or 0
winner_wins + = 1
loser_losses + = 1
winner_stats = { ' wins ' : winner_wins , ' losses ' : winner_losses , ' rating ' : winner_rating }
loser_stats = { ' wins ' : loser_wins , ' losses ' : loser_losses , ' rating ' : loser_rating }
matches [ winner . id ] = winner_stats
matches [ loser . id ] = loser_stats
return config . save_content ( ' tictactoe ' , matches )
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-31 08:23:12 +12:00
@commands.command ( pass_context = True , no_pm = True , name = ' test ' )
@checks.custom_perms ( send_messages = True )
async def test_sharding ( self , ctx ) :
await self . bot . say ( " Responded on shard {} \n board is {} " . format ( config . shard_id , self . boards . get ( ctx . message . server . id ) ) )
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-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
update_records ( winner , loser )
# 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
# TODO: edit in place instead of printing?
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 ' )
fmt = str ( board ) + " \n It is now your turn "
2016-08-15 09:03:39 +12:00
await self . bot . say ( str ( board ) )
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 \n I win, so quick you didn ' t even notice it. " )
return
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-18 08:46:20 +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-08-29 09:41:32 +12:00
fmt + = " I have decided at random, and {} is going to be x ' s this game. It is your turn first! \n 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-22 08:23:47 +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
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 ) )