├── res ├── icon.ico ├── icon.png ├── charmy.png └── title.png ├── Start Game.lnk ├── icons ├── icon.png └── icon.psd ├── .gitignore ├── docs ├── img │ ├── client-game.png │ ├── server-error.png │ ├── server-log.png │ ├── server-start.png │ ├── client-result.png │ ├── server-running.png │ ├── client-connect-error.png │ └── client-connect-success.png ├── MANUAL-CLIENT.md ├── REFERENCES.md └── MANUAL-SERVER.md ├── screenshots ├── gui-about.png ├── gui-main.png ├── exercise-1a.png ├── exercise-1b.png ├── exercise-1c.png ├── exercise-2.png ├── exercise-3.png ├── exercise-4a.png ├── exercise-4b.png ├── multi-clients.png ├── publish-ubuntu.png ├── server-crash.png ├── data-validation.png ├── gui-about-scene.png ├── man-start-client.png ├── man-start-server.png ├── result-checking.png ├── gui-main-game-scene.png ├── gui-welcome-scene.png ├── server-crash-prevented.png ├── thread-synchronization.png └── tic-tac-toe-game-logic.png ├── ttt-service.conf ├── .gitattributes ├── LICENSE.md ├── start-game.bat ├── README.md ├── ttt_client.py ├── ttt_server.py └── ttt_client_gui.py /res/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/res/icon.ico -------------------------------------------------------------------------------- /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/res/icon.png -------------------------------------------------------------------------------- /Start Game.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/Start Game.lnk -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/icons/icon.png -------------------------------------------------------------------------------- /icons/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/icons/icon.psd -------------------------------------------------------------------------------- /res/charmy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/res/charmy.png -------------------------------------------------------------------------------- /res/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/res/title.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Log files 6 | *.log -------------------------------------------------------------------------------- /docs/img/client-game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/client-game.png -------------------------------------------------------------------------------- /docs/img/server-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/server-error.png -------------------------------------------------------------------------------- /docs/img/server-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/server-log.png -------------------------------------------------------------------------------- /docs/img/server-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/server-start.png -------------------------------------------------------------------------------- /screenshots/gui-about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/gui-about.png -------------------------------------------------------------------------------- /screenshots/gui-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/gui-main.png -------------------------------------------------------------------------------- /docs/img/client-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/client-result.png -------------------------------------------------------------------------------- /docs/img/server-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/server-running.png -------------------------------------------------------------------------------- /screenshots/exercise-1a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/exercise-1a.png -------------------------------------------------------------------------------- /screenshots/exercise-1b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/exercise-1b.png -------------------------------------------------------------------------------- /screenshots/exercise-1c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/exercise-1c.png -------------------------------------------------------------------------------- /screenshots/exercise-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/exercise-2.png -------------------------------------------------------------------------------- /screenshots/exercise-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/exercise-3.png -------------------------------------------------------------------------------- /screenshots/exercise-4a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/exercise-4a.png -------------------------------------------------------------------------------- /screenshots/exercise-4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/exercise-4b.png -------------------------------------------------------------------------------- /screenshots/multi-clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/multi-clients.png -------------------------------------------------------------------------------- /screenshots/publish-ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/publish-ubuntu.png -------------------------------------------------------------------------------- /screenshots/server-crash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/server-crash.png -------------------------------------------------------------------------------- /screenshots/data-validation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/data-validation.png -------------------------------------------------------------------------------- /screenshots/gui-about-scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/gui-about-scene.png -------------------------------------------------------------------------------- /screenshots/man-start-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/man-start-client.png -------------------------------------------------------------------------------- /screenshots/man-start-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/man-start-server.png -------------------------------------------------------------------------------- /screenshots/result-checking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/result-checking.png -------------------------------------------------------------------------------- /docs/img/client-connect-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/client-connect-error.png -------------------------------------------------------------------------------- /docs/img/client-connect-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/docs/img/client-connect-success.png -------------------------------------------------------------------------------- /screenshots/gui-main-game-scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/gui-main-game-scene.png -------------------------------------------------------------------------------- /screenshots/gui-welcome-scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/gui-welcome-scene.png -------------------------------------------------------------------------------- /screenshots/server-crash-prevented.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/server-crash-prevented.png -------------------------------------------------------------------------------- /screenshots/thread-synchronization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/thread-synchronization.png -------------------------------------------------------------------------------- /screenshots/tic-tac-toe-game-logic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumin-chen/tic-tac-toe-in-python/HEAD/screenshots/tic-tac-toe-game-logic.png -------------------------------------------------------------------------------- /ttt-service.conf: -------------------------------------------------------------------------------- 1 | # Tic-Tac-Toe Server Service 2 | # 3 | # Upstart file at /etc/init/ttt-service.conf 4 | 5 | description "Tic-Tac-Toe Server" 6 | author "Charlie@CharmySoft.com" 7 | 8 | start on runlevel [2345] 9 | stop on runlevel [016] 10 | 11 | respawn 12 | exec python3 /ttt_server.py 8080 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 CharmySoft 2 | ------------------------ 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /start-game.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Copy the icon to the temp folder 4 | COPY RES\icon.ico %TEMP%\tic-tac-toe-icon.ico 5 | 6 | :: Run 'pythonw --version' and put it into variable PYTHON_VERSION 7 | FOR /F "tokens=* USEBACKQ" %%F IN (`pythonw --version`) DO ( 8 | SET PYTHON_VERSION=%%F 9 | ) 10 | 11 | :: Check if Python 3 is installed 12 | IF "%PYTHON_VERSION:~0,8%" == "Python 2" ( 13 | :: If Python 2 is installed, show the message 14 | ECHO Your Python version is %PYTHON_VERSION%. You need to install Python 3 and set its path first. 15 | mshta javascript:alert^("Your Python version is %PYTHON_VERSION%. \nYou need to install Python 3 and set its path first."^);close^(^); 16 | start "" http://www.python.org/downloads/ 17 | exit /b 18 | ) 19 | IF NOT "%PYTHON_VERSION:~0,8%" == "Python 3" ( 20 | :: If Python 3 is not installed, show the message 21 | ECHO You need to install Python 3 first. 22 | mshta javascript:alert^("You need to install Python 3 first."^);close^(^); 23 | start "" http://www.python.org/downloads/ 24 | exit /b 25 | ) 26 | 27 | :: Run the GUI python script 28 | start "" pythonw ttt_client_gui.py -------------------------------------------------------------------------------- /docs/MANUAL-CLIENT.md: -------------------------------------------------------------------------------- 1 | Tic-Tac-Toe Client Manual 2 | ======================== 3 | Please read the updated version at [http://chenyumin.com/p/tic-tac-toe-client-manual](http://chenyumin.com/p/tic-tac-toe-client-manual). 4 | 5 | These are the instructions to use the command-line based Tic-Tac-Toe Client program. For the GUI version, please read [here](../README.md). 6 | 7 | To start the client, run [ttt_client.py](http://github.com/CharmySoft/tic-tac-toe-in-python/raw/master/ttt_client.py) with Python 3: 8 | 9 | python3 ttt_client.py [server_address] [port_number] 10 | 11 | Where the argument *server_address* is a string that represents the server address; *port_number* is the port that the tic-tac-toe server is listening to. You can also run the client script with no arguments, and you will then be asked to enter the server address and port number. 12 | 13 | ![Client Connect Error](./img/client-connect-error.png?raw=true "Client Connect Error") 14 | If the server is not running, or the provided server address and port number are incorrect and it fails to connect to the server, you will be asked to choose to abort, change address and port number, or retry. 15 | 16 | There is a Tic-Tac-Toe server script set up running on *s.CharmySoft.com* with port *8080*. You can test the client with this server if you are connected to the Internet: 17 | 18 | python3 ttt_client.py s.CharmySoft.com 8080 19 | 20 | To test the client with the server running on your local machine, please read [MANUAL-SERVER.md](MANUAL-SERVER.md) and follow the instructions to start the server first. 21 | 22 | ![Client Connect Success](./img/client-connect-success.png?raw=true "Client Connect Success") 23 | If the connection with the server is successfully established, you will see a welcome message as above. And you will be waiting for another player to join the game. However, if you are testing this with the server running on your local machine, you will have to start another client by yourself and connect to the server to get the game started. 24 | 25 | ![Client Game](./img/client-game.png?raw=true "Client Game") 26 | Once the game is started, you can follow the instructions and enter the position number to make a move. If the position number you entered is, however, already taken or invalid, you will be asked to re-enter the position number. 27 | 28 | ![Client Result](./img/client-result.png?raw=true "Client Result") 29 | Repeat until the game is finished and you shall get the result if you win or lose. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |   **Tic Tac Toe Online in Python** 2 | ======================== 3 | Details of this project can be found on the [Tic Tac Toe project page][2] under: 4 | [*http://chenyumin.com/tic-tac-toe*][2] 5 | 6 | Introduction 7 | ------------------------ 8 | [Tic Tac Toe Online in Python][2] is a socket-based Client-Server application in Python that allows multi-players to connect to the server and play Tic Tac Toe online with other players. 9 | 10 | 11 | Manual 12 | ------------------------ 13 | [Tic Tac Toe Online in Python][2] is a cross-platform game that should work on any modern desktop operating systems. The instructions below are demonstrated on a Linux distro, but you should be able to run this on Windows and Mac OS X as well. 14 | 15 | All the Python scripts in this project are written for Python 3.x. The client GUI requires Python Tkinter module. You need to set up the Python 3 environment before you start. 16 | 17 | To set up the server, please read [The Server Manual](http://chenyumin.com/p/tic-tac-toe-server-manual) or [docs/MANUAL-SERVER.md](docs/MANUAL-SERVER.md). 18 | 19 | To learn about the command-line based client script, please read [The Client Manual](http://chenyumin.com/p/tic-tac-toe-client-manual) or [docs/MANUAL-CLIENT.md](docs/MANUAL-CLIENT.md). 20 | 21 | To use the client GUI, directly run ttt_client_gui.py with python3. 22 | 23 | python3 ttt_client_gui.py 24 | 25 | 26 | Screenshots 27 | ------------------------ 28 | ![Welcome Scene](/screenshots/gui-welcome-scene.png?raw=true "Welcome Scene") 29 | 30 | ![Main Game Scene](/screenshots/gui-main-game-scene.png?raw=true "Main Game Scene") 31 | 32 | ![About Scene](/screenshots/gui-about-scene.png?raw=true "About Scene") 33 | 34 | 35 | References 36 | ------------------------ 37 | Please see the file [docs/REFERENCES.md](docs/REFERENCES.md). 38 | 39 | 40 | Licensing 41 | ------------------------ 42 | Please see the file named [LICENSE.md](LICENSE.md). 43 | 44 | 45 | Author 46 | ------------------------ 47 | * [Chen Yumin][3] 48 | founder of [CharmySoft][1] 49 | 50 | 51 | Contact 52 | ------------------------ 53 | * CharmySoft: [*http://www.CharmySoft.com/*][1] 54 | * Chen Yumin: [*http://chenyumin.com*][3] 55 | * Email: [*hello@chenyumin.com*](mailto:hello@chenyumin.com) 56 | 57 | [1]: http://www.CharmySoft.com/ "CharmySoft" 58 | [2]: http://chenyumin.com/tic-tac-toe "Tic Tac Toe Online in Python" 59 | [3]: http://chenyumin.com "Chen Yumin" -------------------------------------------------------------------------------- /docs/REFERENCES.md: -------------------------------------------------------------------------------- 1 | References 2 | ======================== 3 | Here is a list of external sources that I learned from when I was doing this project. 4 | 5 | 6 | * Python Documentation: Logging Cookbook 7 | 8 | 9 | 10 | * Stack overflow: Does Python have a ternary conditional operator? 11 | 12 | 13 | 14 | * Python: Lambda Functions 15 | 16 | 17 | 18 | * Stack overflow: Getting the widget that triggered an Event? 19 | 20 | 21 | 22 | * effbot.org: What is 'if __name__ == "__main__"' for? 23 | 24 | 25 | 26 | * The Python Standard Library: 6. Built-in Exceptions 27 | 28 | 29 | 30 | * Stack overflow: Static methods in Python? 31 | 32 | 33 | 34 | * Stack overflow: Call to operating system to open url? 35 | 36 | 37 | 38 | * effbot.org: tkinterbook :: Events and Bindings 39 | 40 | 41 | 42 | * Stack overflow: How to get tkinter canvas to dynamically resize to window width? 43 | 44 | 45 | 46 | * Python Course: Tkinter Tutorial - Canvas Widgets 47 | 48 | 49 | 50 | * effbot.org: tkinterbook :: The Tkinter Canvas Widget 51 | 52 | 53 | 54 | * Tutorials Point: Python Tkinter Canvas 55 | 56 | 57 | 58 | * Tutorials Point: Python GUI Programming (Tkinter) 59 | 60 | 61 | 62 | * Stack overflow: Change one character in a string in Python? 63 | 64 | 65 | 66 | * Python Tutorial: Classes - Odds and Ends 67 | 68 | 69 | 70 | * Stack overflow: How can I make a time delay in Python? 71 | 72 | 73 | 74 | * Thread Synchronization Mechanisms in Python 75 | 76 | 77 | 78 | * Stack overflow: Multiple Clients Cannot Listen and Write at the Same Time 79 | -------------------------------------------------------------------------------- /docs/MANUAL-SERVER.md: -------------------------------------------------------------------------------- 1 | Tic-Tac-Toe Server Manual 2 | ======================== 3 | Please read the updated version at [The Server Manual](http://chenyumin.com/p/tic-tac-toe-server-manual). 4 | 5 | To set up the server, you can run the following shell commands. 6 | ```bash 7 | # Change this to your desired path 8 | TTT_SERVER_SCRIPT_PATH="/ttt_server.py" 9 | 10 | # Download the server script to your specified destination 11 | sudo wget -O $TTT_SERVER_SCRIPT_PATH http://github.com/CharmySoft/tic-tac-toe-in-python/raw/master/ttt_server.py 12 | 13 | # Download the upstart configuration script 14 | sudo wget -O /etc/init/ttt-service.conf http://github.com/CharmySoft/tic-tac-toe-in-python/raw/master/ttt-service.conf 15 | 16 | # Update the server script path on the downloaded upstart conf file 17 | sudo sed -i 's|'/ttt_server.py'|'${TTT_SERVER_SCRIPT_PATH}'|' /etc/init/ttt-service.conf 18 | ``` 19 | After setting up the upstart configuration script, if you have installed [upstart](http://upstart.ubuntu.com/) on your server machine, the server script will automatically run on system start up. And respawn will start it back up if it is killed or exits non-zero (like an uncaught exception), so the server script can always keep running on your server. 20 | 21 | If you want to test the server script on your local machine, you can run [ttt_server.py](http://github.com/CharmySoft/tic-tac-toe-in-python/raw/master/ttt_server.py) with python3: 22 | 23 | python3 ttt_server.py [port_number] 24 | 25 | Where the argument *port_number* is a 16-bit unsigned integer port number used for the TCP/IP protocol addressing. You can also run the server script with no arguments, and you will then be asked to enter the and port number. 26 | 27 | ![Server Error](./img/server-error.png?raw=true "Server Error") 28 | If the server fails to bind the port, you will see an error message as above. You can then choose to abort, change port number, or retry, as you wish. Usually this is due to port conflicts, or the port is reserved by the system. 29 | 30 | ![Server Start](./img/server-start.png?raw=true "Server Start") 31 | When the server is successfully started, you will see some messages as above. And then the server will be able to accept clients. 32 | 33 | ![Server Running](./img/server-running.png?raw=true "Server Running") 34 | You will be informed when clients are connected, when they get matched and start a game, when they disconnect, when some unexpected error happens, and when they finish their game, etc. Once the server gets started, it should be able to keep running without getting interrupted. Even when the clients' connection fails, or some unexpected messages are received from the client, the server can handle those exceptions gracefully and inform you of the exceptions. 35 | 36 | ![Server Log](./img/server-log.png?raw=true "Server Log") 37 | All the information, warnings, and exceptions, will also be logged into the file *ttt_server.log*. -------------------------------------------------------------------------------- /ttt_client.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | # Import the socket module 4 | import socket 5 | # Import command line arguments 6 | from sys import argv 7 | 8 | class TTTClient: 9 | """TTTClient deals with networking and communication with the TTTServer.""" 10 | 11 | def __init__(self): 12 | """Initializes the client and create a client socket.""" 13 | # Create a TCP/IP socket 14 | self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM); 15 | 16 | def connect(self, address, port_number): 17 | """Keeps repeating connecting to the server and returns True if 18 | connected successfully.""" 19 | while True: 20 | try: 21 | print("Connecting to the game server..."); 22 | # Connection time out 10 seconds 23 | self.client_socket.settimeout(10); 24 | # Connect to the specified host and port 25 | self.client_socket.connect((address, int(port_number))); 26 | # Return True if connected successfully 27 | return True; 28 | except: 29 | # Caught an error 30 | print("There is an error when trying to connect to " + 31 | str(address) + "::" + str(port_number)); 32 | self.__connect_failed__(); 33 | return False; 34 | 35 | def __connect_failed__(self): 36 | """(Private) This function will be called when the attempt to connect 37 | failed. This function might be overridden by the GUI program.""" 38 | # Ask the user what to do with the error 39 | choice = input("[A]bort, [C]hange address and port, or [R]etry?"); 40 | if(choice.lower() == "a"): 41 | exit(); 42 | elif(choice.lower() == "c"): 43 | address = input("Please enter the address:"); 44 | port_number = input("Please enter the port:"); 45 | 46 | def s_send(self, command_type, msg): 47 | """Sends a message to the server with an agreed command type token 48 | to ensure the message is delivered safely.""" 49 | # A 1 byte command_type character is put at the front of the message 50 | # as a communication convention 51 | try: 52 | self.client_socket.send((command_type + msg).encode()); 53 | except: 54 | # If any error occurred, the connection might be lost 55 | self.__connection_lost(); 56 | 57 | def s_recv(self, size, expected_type): 58 | """Receives a packet with specified size from the server and check 59 | its integrity by comparing its command type token with the expected 60 | one.""" 61 | try: 62 | msg = self.client_socket.recv(size).decode(); 63 | # If received a quit signal from the server 64 | if(msg[0] == "Q"): 65 | why_quit = ""; 66 | try: 67 | # Try receiving the whole reason why quit 68 | why_quit = self.client_socket.recv(1024).decode(); 69 | except: 70 | pass; 71 | # Print the resaon 72 | print(msg[1:] + why_quit); 73 | # Throw an error 74 | raise Exception; 75 | # If received an echo signal from the server 76 | elif(msg[0] == "E"): 77 | # Echo the message back to the server 78 | self.s_send("e", msg[1:]); 79 | # Recursively retrive the desired message 80 | return self.s_recv(size, expected_type); 81 | # If the command type token is not the expected type 82 | elif(msg[0] != expected_type): 83 | print("The received command type \"" + msg[0] + "\" does not " + 84 | "match the expected type \"" + expected_type + "\"."); 85 | # Connection lost 86 | self.__connection_lost(); 87 | # If received an integer from the server 88 | elif(msg[0] == "I"): 89 | # Return the integer 90 | return int(msg[1:]); 91 | # In other case 92 | else: 93 | # Return the message 94 | return msg[1:]; 95 | # Simply return the raw message if anything unexpected happended 96 | # because it shouldn't matter any more 97 | return msg; 98 | except: 99 | # If any error occurred, the connection might be lost 100 | self.__connection_lost(); 101 | return None; 102 | 103 | def __connection_lost(self): 104 | """(Private) This function will be called when the connection is lost.""" 105 | print("Error: connection lost."); 106 | try: 107 | # Try and send a message back to the server to notify connection lost 108 | self.client_socket.send("q".encode()); 109 | except: 110 | pass; 111 | # Raise an error to finish 112 | raise Exception; 113 | 114 | def close(self): 115 | """Shut down the socket and close it""" 116 | # Shut down the socket to prevent further sends/receives 117 | self.client_socket.shutdown(socket.SHUT_RDWR); 118 | # Close the socket 119 | self.client_socket.close(); 120 | 121 | class TTTClientGame(TTTClient): 122 | """TTTClientGame deals with the game logic on the client side.""" 123 | 124 | def __init__(self): 125 | """Initializes the client game object.""" 126 | TTTClient.__init__(self); 127 | 128 | def start_game(self): 129 | """Starts the game and gets basic game information from the server.""" 130 | # Receive the player's ID from the server 131 | self.player_id = int(self.s_recv(128, "A")); 132 | # Confirm the ID has been received 133 | self.s_send("c","1"); 134 | 135 | # Tell the user that connection has been established 136 | self.__connected__(); 137 | 138 | # Receive the assigned role from the server 139 | self.role = str(self.s_recv(2, "R")); 140 | # Confirm the assigned role has been received 141 | self.s_send("c","2"); 142 | 143 | # Receive the mactched player's ID from the server 144 | self.match_id = int(self.s_recv(128, "I")); 145 | # Confirm the mactched player's ID has been received 146 | self.s_send("c","3"); 147 | 148 | print(("You are now matched with player " + str(self.match_id) 149 | + "\nYou are the \"" + self.role + "\"")); 150 | 151 | # Call the __game_started() function, which might be implemented by 152 | # the GUI program to interact with the user interface. 153 | self.__game_started__(); 154 | 155 | # Start the main loop 156 | self.__main_loop(); 157 | 158 | def __connected__(self): 159 | """(Private) This function is called when the client is successfully 160 | connected to the server. This might be overridden by the GUI program.""" 161 | # Welcome the user 162 | print("Welcome to Tic Tac Toe online, player " + str(self.player_id) 163 | + "\nPlease wait for another player to join the game..."); 164 | 165 | def __game_started__(self): 166 | """(Private) This function is called when the game is getting started.""" 167 | # This is a virtual function 168 | # The actual implementation is in the subclass (the GUI program) 169 | return; 170 | 171 | def __main_loop(self): 172 | """The main game loop.""" 173 | while True: 174 | # Get the board content from the server 175 | board_content = self.s_recv(10, "B"); 176 | # Get the command from the server 177 | command = self.s_recv(2, "C"); 178 | # Update the board 179 | self.__update_board__(command, board_content); 180 | 181 | if(command == "Y"): 182 | # If it's this player's turn to move 183 | self.__player_move__(board_content); 184 | elif(command == "N"): 185 | # If the player needs to just wait 186 | self.__player_wait__(); 187 | # Get the move the other player made from the server 188 | move = self.s_recv(2, "I"); 189 | self.__opponent_move_made__(move); 190 | elif(command == "D"): 191 | # If the result is a draw 192 | print("It's a draw."); 193 | break; 194 | elif(command == "W"): 195 | # If this player wins 196 | print("You WIN!"); 197 | # Draw winning path 198 | self.__draw_winning_path__(self.s_recv(4, "P")); 199 | # Break the loop and finish 200 | break; 201 | elif(command == "L"): 202 | # If this player loses 203 | print("You lose."); 204 | # Draw winning path 205 | self.__draw_winning_path__(self.s_recv(4, "P")); 206 | # Break the loop and finish 207 | break; 208 | else: 209 | # If the server sends back anything unrecognizable 210 | # Simply print it 211 | print("Error: unknown message was sent from the server"); 212 | # And finish 213 | break; 214 | 215 | def __update_board__(self, command, board_string): 216 | """(Private) Updates the board. This function might be overridden by 217 | the GUI program.""" 218 | if(command == "Y"): 219 | # If it's this player's turn to move, print out the current 220 | # board with " " converted to the corresponding position number 221 | print("Current board:\n" + TTTClientGame.format_board( 222 | TTTClientGame.show_board_pos(board_string))); 223 | else: 224 | # Print out the current board 225 | print("Current board:\n" + TTTClientGame.format_board( 226 | board_string)); 227 | 228 | def __player_move__(self, board_string): 229 | """(Private) Lets the user input the move and sends it back to the 230 | server. This function might be overridden by the GUI program.""" 231 | while True: 232 | # Prompt the user to enter a position 233 | try: 234 | position = int(input('Please enter the position (1~9):')); 235 | except: 236 | print("Invalid input."); 237 | continue; 238 | 239 | # Ensure user-input data is valid 240 | if(position >= 1 and position <= 9): 241 | # If the position is between 1 and 9 242 | if(board_string[position - 1] != " "): 243 | # If the position is already been taken, 244 | # Print out a warning 245 | print("That position has already been taken." + 246 | "Please choose another one."); 247 | else: 248 | # If the user input is valid, break the loop 249 | break; 250 | else: 251 | print("Please enter a value between 1 and 9 that" + 252 | "corresponds to the position on the grid board."); 253 | # Loop until the user enters a valid value 254 | 255 | # Send the position back to the server 256 | self.s_send("i", str(position)); 257 | 258 | def __player_wait__(self): 259 | """(Private) Lets the user know it's waiting for the other player to 260 | make a move. This function might be overridden by the GUI program.""" 261 | print("Waiting for the other player to make a move..."); 262 | 263 | def __opponent_move_made__(self, move): 264 | """(Private) Shows the user the move that the other player has taken. 265 | This function might be overridden by the GUI program.""" 266 | print("Your opponent took up number " + str(move)); 267 | 268 | def __draw_winning_path__(self, winning_path): 269 | """(Private) Shows to the user the path that has caused the game to 270 | win or lose. This function might be overridden by the GUI program.""" 271 | # Generate a new human readable path string 272 | readable_path = ""; 273 | for c in winning_path: 274 | readable_path += str(int(c) + 1) + ", " 275 | 276 | print("The path is: " + readable_path[:-2]); 277 | 278 | 279 | def show_board_pos(s): 280 | """(Static) Converts the empty positions " " (a space) in the board 281 | string to its corresponding position index number.""" 282 | 283 | new_s = list("123456789"); 284 | for i in range(0, 8): 285 | if(s[i] != " "): 286 | new_s[i] = s[i]; 287 | return "".join(new_s); 288 | 289 | def format_board(s): 290 | """(Static) Formats the grid board.""" 291 | 292 | # If the length of the string is not 9 293 | if(len(s) != 9): 294 | # Then print out an error message 295 | print("Error: there should be 9 symbols."); 296 | # Throw an error 297 | raise Exception; 298 | 299 | # Draw the grid board 300 | #print("|1|2|3|"); 301 | #print("|4|5|6|"); 302 | #print("|7|8|9|"); 303 | return("|" + s[0] + "|" + s[1] + "|" + s[2] + "|\n" 304 | + "|" + s[3] + "|" + s[4] + "|" + s[5] + "|\n" 305 | + "|" + s[6] + "|" + s[7] + "|" + s[8] + "|\n"); 306 | 307 | # Define the main program 308 | def main(): 309 | # If there are more than 3 arguments 310 | if(len(argv) >= 3): 311 | # Set the address to argument 1, and port number to argument 2 312 | address = argv[1]; 313 | port_number = argv[2]; 314 | else: 315 | # Ask the user to input the address and port number 316 | address = input("Please enter the address:"); 317 | port_number = input("Please enter the port:"); 318 | 319 | # Initialize the client object 320 | client = TTTClientGame(); 321 | # Connect to the server 322 | client.connect(address, port_number); 323 | try: 324 | # Start the game 325 | client.start_game(); 326 | except: 327 | print(("Game finished unexpectedly!")); 328 | finally: 329 | # Close the client 330 | client.close(); 331 | 332 | if __name__ == "__main__": 333 | # If this script is running as a standalone program, 334 | # start the main program. 335 | main(); -------------------------------------------------------------------------------- /ttt_server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | # Import the socket module 4 | import socket 5 | # Import multi-threading module 6 | import threading 7 | # Import the time module 8 | import time 9 | # Import command line arguments 10 | from sys import argv 11 | # Import logging 12 | import logging 13 | 14 | 15 | # Set up logging to file 16 | logging.basicConfig(level=logging.DEBUG, 17 | format='[%(asctime)s] %(levelname)s: %(message)s', 18 | datefmt='%Y-%m-%d %H:%M:%S', 19 | filename='ttt_server.log'); 20 | # Define a Handler which writes INFO messages or higher to the sys.stderr 21 | # This will print all the INFO messages or higer at the same time 22 | console = logging.StreamHandler(); 23 | console.setLevel(logging.INFO); 24 | # Add the handler to the root logger 25 | logging.getLogger('').addHandler(console); 26 | 27 | class TTTServer: 28 | """TTTServer deals with networking and communication with the TTTClient.""" 29 | 30 | def __init__(self): 31 | """Initializes the server object with a server socket.""" 32 | # Create a TCP/IP socket 33 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM); 34 | 35 | def bind(self, port_number): 36 | """Binds the server with the designated port and start listening to 37 | the binded address.""" 38 | while True: 39 | try: 40 | # Bind to an address with the designated port 41 | # The empty string "" is a symbolic name 42 | # meaning all available interfaces 43 | self.server_socket.bind(("", int(port_number))); 44 | logging.info("Reserved port " + str(port_number)); 45 | # Start listening to the binded address 46 | self.server_socket.listen(1); 47 | logging.info("Listening to port " + str(port_number)); 48 | # Break the while loop if no error is caught 49 | break; 50 | except: 51 | # Caught an error 52 | logging.warning("There is an error when trying to bind " + 53 | str(port_number)); 54 | # Ask the user what to do with the error 55 | choice = input("[A]bort, [C]hange port, or [R]etry?"); 56 | if(choice.lower() == "a"): 57 | exit(); 58 | elif(choice.lower() == "c"): 59 | port_number = input("Please enter the port:"); 60 | 61 | def close(self): 62 | # Close the socket 63 | self.server_socket.close(); 64 | 65 | class TTTServerGame(TTTServer): 66 | """TTTServerGame deals with the game logic on the server side.""" 67 | 68 | def __init__(self): 69 | """Initializes the server game object.""" 70 | TTTServer.__init__(self); 71 | 72 | def start(self): 73 | """Starts the server and let it accept clients.""" 74 | # Create an array object to store connected players 75 | self.waiting_players = []; 76 | # Use a simple lock to synchronize access when matching players 77 | self.lock_matching = threading.Lock(); 78 | # Start the main loop 79 | self.__main_loop(); 80 | 81 | def __main_loop(self): 82 | """(Private) The main loop.""" 83 | # Loop to infinitely accept new clients 84 | while True: 85 | # Accept a connection from a client 86 | connection, client_address = self.server_socket.accept(); 87 | logging.info("Received connection from " + str(client_address)); 88 | 89 | # Initialize a new Player object to store all the client's infomation 90 | new_player = Player(connection); 91 | # Push this new player object into the players array 92 | self.waiting_players.append(new_player); 93 | 94 | try: 95 | # Start a new thread to deal with this client 96 | threading.Thread(target=self.__client_thread, 97 | args=(new_player,)).start(); 98 | except: 99 | logging.error("Failed to create thread."); 100 | 101 | def __client_thread(self, player): 102 | """(Private) This is the client thread.""" 103 | # Wrap the whole client thread with a try and catch so that the 104 | # server would not be affected even if a client messes up 105 | try: 106 | # Send the player's ID back to the client 107 | player.send("A", str(player.id)); 108 | # Send the client didn't confirm the message 109 | if(player.recv(2, "c") != "1"): 110 | # An error happened 111 | logging.warning("Client " + str(player.id) + 112 | " didn't confirm the initial message."); 113 | # Finish 114 | return; 115 | 116 | while player.is_waiting: 117 | # If the player is still waiting for another player to join 118 | # Try to match this player with other waiting players 119 | match_result = self.matching_player(player); 120 | 121 | if(match_result is None): 122 | # If not matched, wait for a second (to keep CPU usage low) 123 | time.sleep(1); 124 | # Check if the player is still connected 125 | player.check_connection(); 126 | else: 127 | # If matched with another player 128 | 129 | # Initialize a new Game object to store the game's infomation 130 | new_game = Game(); 131 | # Assign both players 132 | new_game.player1 = player; 133 | new_game.player2 = match_result; 134 | # Create an empty string for empty board content 135 | new_game.board_content = list(" "); 136 | 137 | try: 138 | # Game starts 139 | new_game.start(); 140 | except: 141 | logging.warning("Game between " + str(new_game.player1.id) + 142 | " and " + str(new_game.player2.id) + 143 | " is finished unexpectedly."); 144 | # End this thread 145 | return; 146 | except: 147 | print("Player " + str(player.id) + " disconnected."); 148 | finally: 149 | # Remove the player from the waiting list 150 | self.waiting_players.remove(player); 151 | 152 | def matching_player(self, player): 153 | """Goes through the players list and try to match the player with 154 | another player who is also waiting to play. Returns any matched 155 | player if found.""" 156 | # Try acquiring the lock 157 | self.lock_matching.acquire(); 158 | try: 159 | # Loop through each player 160 | for p in self.waiting_players: 161 | # If another player is found waiting and its not the player itself 162 | if(p.is_waiting and p is not player): 163 | # Matched player with p 164 | # Set their match 165 | player.match = p; 166 | p.match = player; 167 | # Set their roles 168 | player.role = "X"; 169 | p.role = "O"; 170 | # Set the player is not waiting any more 171 | player.is_waiting = False; 172 | p.is_waiting = False; 173 | # Then return the player's ID 174 | return p; 175 | finally: 176 | # Release the lock 177 | self.lock_matching.release(); 178 | # Return None if nothing is found 179 | return None; 180 | 181 | class Player: 182 | """Player class describes a client with connection to the server and 183 | as a player in the tic tac toe game.""" 184 | 185 | # Count the players (for generating unique IDs) 186 | count = 0; 187 | 188 | def __init__(self, connection): 189 | """Initialize a player with its connection to the server""" 190 | # Generate a unique id for this player 191 | Player.count = Player.count + 1 192 | self.id = Player.count; 193 | # Assign the corresponding connection 194 | self.connection = connection; 195 | # Set the player waiting status to True 196 | self.is_waiting = True; 197 | 198 | def send(self, command_type, msg): 199 | """Sends a message to the client with an agreed command type token 200 | to ensure the message is delivered safely.""" 201 | # A 1 byte command_type character is put at the front of the message 202 | # as a communication convention 203 | try: 204 | self.connection.send((command_type + msg).encode()); 205 | except: 206 | # If any error occurred, the connection might be lost 207 | self.__connection_lost(); 208 | 209 | def recv(self, size, expected_type): 210 | """Receives a packet with specified size from the client and check 211 | its integrity by comparing its command type token with the expected 212 | one.""" 213 | try: 214 | msg = self.connection.recv(size).decode(); 215 | # If received a quit signal from the client 216 | if(msg[0] == "q"): 217 | # Print why the quit signal 218 | logging.info(msg[1:]); 219 | # Connection lost 220 | self.__connection_lost(); 221 | # If the message is not the expected type 222 | elif(msg[0] != expected_type): 223 | # Connection lost 224 | self.__connection_lost(); 225 | # If received an integer from the client 226 | elif(msg[0] == "i"): 227 | # Return the integer 228 | return int(msg[1:]); 229 | # In other case 230 | else: 231 | # Return the message 232 | return msg[1:]; 233 | # Simply return the raw message if anything unexpected happended 234 | # because it shouldn't matter any more 235 | return msg; 236 | except: 237 | # If any error occurred, the connection might be lost 238 | self.__connection_lost(); 239 | return None; 240 | 241 | def check_connection(self): 242 | """Sends a meesage to check if the client is still properly connected.""" 243 | # Send the client an echo signal to ask it to repeat back 244 | self.send("E", "z"); 245 | # Check if "e" gets sent back 246 | if(self.recv(2, "e") != "z"): 247 | # If the client didn't confirm, the connection might be lost 248 | self.__connection_lost(); 249 | 250 | def send_match_info(self): 251 | """Sends a the matched information to the client, which includes 252 | the assigned role and the matched player.""" 253 | # Send to client the assigned role 254 | self.send("R", self.role); 255 | # Waiting for client to confirm 256 | if(self.recv(2,"c") != "2"): 257 | self.__connection_lost(); 258 | # Sent to client the matched player's ID 259 | self.send("I", str(self.match.id)); 260 | # Waiting for client to confirm 261 | if(self.recv(2,"c") != "3"): 262 | self.__connection_lost(); 263 | 264 | def __connection_lost(self): 265 | """(Private) This function will be called when the connection is lost.""" 266 | # This player has lost connection with the server 267 | logging.warning("Player " + str(self.id) + " connection lost."); 268 | # Tell the other player that the game is finished 269 | try: 270 | self.match.send("Q", "The other player has lost connection" + 271 | " with the server.\nGame over."); 272 | except: 273 | pass; 274 | # Raise an error so that the client thread can finish 275 | raise Exception; 276 | 277 | class Game: 278 | """Game class describes a game with two different players.""" 279 | 280 | def start(self): 281 | """Starts the game.""" 282 | # Send both players the match info 283 | self.player1.send_match_info(); 284 | self.player2.send_match_info(); 285 | 286 | # Print the match info onto screen 287 | logging.info("Player " + str(self.player1.id) + 288 | " is matched with player " + str(self.player2.id)); 289 | 290 | while True: 291 | # Player 1 move 292 | if(self.move(self.player1, self.player2)): 293 | return; 294 | # Player 2 move 295 | if(self.move(self.player2, self.player1)): 296 | return; 297 | 298 | def move(self, moving_player, waiting_player): 299 | """Lets a player make a move.""" 300 | # Send both players the current board content 301 | moving_player.send("B", ("".join(self.board_content))); 302 | waiting_player.send("B", ("".join(self.board_content))); 303 | # Let the moving player move, Y stands for yes it's turn to move, 304 | # and N stands for no and waiting 305 | moving_player.send("C", "Y"); 306 | waiting_player.send("C", "N"); 307 | # Receive the move from the moving player 308 | move = int(moving_player.recv(2, "i")); 309 | # Send the move to the waiting player 310 | waiting_player.send("I", str(move)); 311 | # Check if the position is empty 312 | if(self.board_content[move - 1] == " "): 313 | # Write the it into the board 314 | self.board_content[move - 1] = moving_player.role; 315 | else: 316 | logging.warning("Player " + str(moving_player.id) + 317 | " is attempting to take a position that's already " + 318 | "been taken."); 319 | # # This player is attempting to take a position that's already 320 | # # taken. HE IS CHEATING, KILL HIM! 321 | # moving_player.send("Q", "Please don't cheat!\n" + 322 | # "You are running a modified client program."); 323 | # waiting_player.send("Q", "The other playing is caught" + 324 | # "cheating. You win!"); 325 | # # Throw an error to finish this game 326 | # raise Exception; 327 | 328 | # Check if this will result in a win 329 | result, winning_path = self.check_winner(moving_player); 330 | if(result >= 0): 331 | # If there is a result 332 | # Send back the latest board content 333 | moving_player.send("B", ("".join(self.board_content))); 334 | waiting_player.send("B", ("".join(self.board_content))); 335 | 336 | if(result == 0): 337 | # If this game ends with a draw 338 | # Send the players the result 339 | moving_player.send("C", "D"); 340 | waiting_player.send("C", "D"); 341 | print("Game between player " + str(self.player1.id) + " and player " 342 | + str(self.player2.id) + " ends with a draw."); 343 | return True; 344 | if(result == 1): 345 | # If this player wins the game 346 | # Send the players the result 347 | moving_player.send("C", "W"); 348 | waiting_player.send("C", "L"); 349 | # Send the players the winning path 350 | moving_player.send("P", winning_path); 351 | waiting_player.send("P", winning_path); 352 | print("Player " + str(self.player1.id) + " beats player " 353 | + str(self.player2.id) + " and finishes the game."); 354 | return True; 355 | return False; 356 | 357 | def check_winner(self, player): 358 | """Checks if the player wins the game. Returns 1 if wins, 359 | 0 if it's a draw, -1 if there's no result yet.""" 360 | s = self.board_content; 361 | 362 | # Check columns 363 | if(len(set([s[0], s[1], s[2], player.role])) == 1): 364 | return 1, "012"; 365 | if(len(set([s[3], s[4], s[5], player.role])) == 1): 366 | return 1, "345"; 367 | if(len(set([s[6], s[7], s[8], player.role])) == 1): 368 | return 1, "678"; 369 | 370 | # Check rows 371 | if(len(set([s[0], s[3], s[6], player.role])) == 1): 372 | return 1, "036"; 373 | if(len(set([s[1], s[4], s[7], player.role])) == 1): 374 | return 1, "147"; 375 | if(len(set([s[2], s[5], s[8], player.role])) == 1): 376 | return 1, "258"; 377 | 378 | # Check diagonal 379 | if(len(set([s[0], s[4], s[8], player.role])) == 1): 380 | return 1, "048"; 381 | if(len(set([s[2], s[4], s[6], player.role])) == 1): 382 | return 1, "246"; 383 | 384 | # If there's no empty position left, draw 385 | if " " not in s: 386 | return 0, ""; 387 | 388 | # The result cannot be determined yet 389 | return -1, ""; 390 | 391 | # Define the main program 392 | def main(): 393 | # If there are more than 2 arguments 394 | if(len(argv) >= 2): 395 | # Set port number to argument 1 396 | port_number = argv[1]; 397 | else: 398 | # Ask the user to input port number 399 | port_number = input("Please enter the port:"); 400 | 401 | try: 402 | # Initialize the server object 403 | server = TTTServerGame(); 404 | # Bind the server with the port 405 | server.bind(port_number); 406 | # Start the server 407 | server.start(); 408 | # Close the server 409 | server.close(); 410 | except BaseException as e: 411 | logging.critical("Server critical failure.\n" + str(e)); 412 | 413 | if __name__ == "__main__": 414 | # If this script is running as a standalone program, 415 | # start the main program. 416 | main(); 417 | 418 | -------------------------------------------------------------------------------- /ttt_client_gui.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | # Import the GUI library Tkinter 4 | import tkinter 5 | # Import the messagebox module explicitly 6 | from tkinter import messagebox 7 | # Import the webbroswer module for opening a link 8 | import webbrowser 9 | # Import the client module 10 | from ttt_client import TTTClientGame 11 | # Import multi-threading module 12 | import threading 13 | # Import socket 14 | import socket 15 | 16 | # Constants 17 | C_WINDOW_WIDTH = 640; 18 | C_WINDOW_HEIGHT = 480; 19 | C_WINDOW_MIN_WIDTH = 480; 20 | C_WINDOW_MIN_HEIGHT = 360; 21 | C_COLOR_BLUE_LIGHT = "#e4f1fe"; 22 | C_COLOR_BLUE_DARK = "#304e62"; 23 | C_COLOR_BLUE = "#a8d4f2"; 24 | 25 | class CanvasWidget: 26 | """(Abstract) The base class for all the canvas widgets.""" 27 | 28 | __count = 0; # Count the number of widgets initialized 29 | 30 | def __init__(self, canvas): 31 | """Initializes the widget.""" 32 | self.canvas = canvas; 33 | # Generate a unique id for each widget (for tags) 34 | self.id = str(CanvasWidget.__count); 35 | CanvasWidget.__count = CanvasWidget.__count + 1; 36 | # Generate a unique tag for each widget 37 | self.tag_name = self.__class__.__name__ + self.id; 38 | # Initialize instance variables 39 | self.__disabled__ = False; 40 | # Set default colors 41 | self.normal_color = C_COLOR_BLUE; 42 | self.hovered_color = C_COLOR_BLUE_DARK; 43 | 44 | def set_clickable(self, clickable): 45 | """Sets if the widget can be clicked.""" 46 | if(clickable): 47 | self.canvas.tag_bind(self.tag_name, "", 48 | self.__on_click__); 49 | else: 50 | self.canvas.tag_unbind(self.tag_name, ""); 51 | 52 | def __on_click__(self, event): 53 | """(Private) This function will be called when the user clicks on 54 | the widget.""" 55 | if(self.__disabled__): 56 | return False; 57 | if self.command is not None: 58 | self.command(); 59 | return True; 60 | else: 61 | print("Error: " + self.__class__.__name__ + " " + 62 | self.id + " does not have a command"); 63 | raise AttributeError; 64 | return False; 65 | 66 | def set_hoverable(self, hoverable): 67 | """Sets if the widget can be hovered.""" 68 | if(hoverable): 69 | self.canvas.tag_bind(self.tag_name, "", 70 | self.__on_enter__); 71 | self.canvas.tag_bind(self.tag_name, "", 72 | self.__on_leave__); 73 | else: 74 | self.canvas.tag_unbind(self.tag_name, ""); 75 | self.canvas.tag_unbind(self.tag_name, ""); 76 | 77 | def __on_enter__(self, event): 78 | """(Private) This function will be called when the mouse enters 79 | into the widget.""" 80 | if(self.__disabled__): 81 | return False; 82 | self.canvas.itemconfig(self.tag_name, fill=self.hovered_color); 83 | return True; 84 | 85 | def __on_leave__(self, event): 86 | """(Private) This function will be called when the mouse leaves 87 | the widget.""" 88 | if(self.__disabled__): 89 | return False; 90 | self.canvas.itemconfig(self.tag_name, fill=self.normal_color); 91 | return True; 92 | 93 | def disable(self): 94 | """Disables the widget so it won't respond to any events.""" 95 | self.__disabled__ = True; 96 | 97 | def enable(self): 98 | """Enables the widget so it starts to respond to events.""" 99 | self.__disabled__ = False; 100 | 101 | def is_enabled(self): 102 | """Returns True if the widget is disabled.""" 103 | return self.__disabled__; 104 | 105 | def config(self, **kwargs): 106 | """Configures the widget's options.""" 107 | return self.canvas.itemconfig(self.tag_name, **kwargs); 108 | 109 | def delete(self): 110 | self.canvas.delete(self.tag_name); 111 | 112 | class CanvasClickableLabel(CanvasWidget): 113 | """A clickable label that shows text and can respond to user 114 | click events.""" 115 | 116 | def __init__(self, canvas, x, y, label_text, normal_color, 117 | hovered_color): 118 | """Initializes the clickable label object.""" 119 | 120 | # Initialize super class 121 | CanvasWidget.__init__(self, canvas); 122 | 123 | # Set color scheme for different states 124 | self.normal_color = normal_color; 125 | self.hovered_color = hovered_color; 126 | 127 | # Create the clickable label text 128 | canvas.create_text(x, y, font="Helvetica 14 underline", 129 | text=label_text, fill=self.normal_color, tags=(self.tag_name)); 130 | 131 | # Bind events 132 | self.set_hoverable(True); 133 | self.set_clickable(True); 134 | 135 | class CanvasButton(CanvasWidget): 136 | """A button that responds to mouse clicks.""" 137 | 138 | # Define constant width and height 139 | WIDTH = 196; 140 | HEIGHT = 32; 141 | 142 | def __init__(self, canvas, x, y, button_text, normal_color, 143 | hovered_color, normal_text_color, hovered_text_color): 144 | """Initialize the button object.""" 145 | 146 | # Initialize super class 147 | CanvasWidget.__init__(self, canvas); 148 | 149 | # Set color scheme for different states 150 | self.normal_color = normal_color; 151 | self.hovered_color = hovered_color; 152 | self.normal_text_color = normal_text_color; 153 | self.hovered_text_color = hovered_text_color; 154 | 155 | # Create the rectangle background 156 | canvas.create_rectangle(x - self.WIDTH/2 + self.HEIGHT/2, 157 | y - self.HEIGHT/2, x + self.WIDTH/2 - self.HEIGHT/2, 158 | y + self.HEIGHT/2, fill=self.normal_color, outline="", 159 | tags=(self.tag_name, "rect" + self.id)); 160 | 161 | # Create the two circles on both sides to create a rounded edge 162 | canvas.create_oval(x - self.WIDTH/2, y - self.HEIGHT/2, 163 | x - self.WIDTH/2 + self.HEIGHT, y + self.HEIGHT/2, 164 | fill=self.normal_color, outline="", 165 | tags=(self.tag_name, "oval_l" + self.id)); 166 | 167 | canvas.create_oval(x + self.WIDTH/2 - self.HEIGHT, 168 | y - self.HEIGHT/2, x + self.WIDTH/2, y + self.HEIGHT/2, 169 | fill=self.normal_color, outline="", 170 | tags=(self.tag_name, "oval_r" + self.id)); 171 | 172 | # Create the button text 173 | canvas.create_text(x, y, font="Helvetica 16 bold", 174 | text=button_text, fill=self.normal_text_color, 175 | tags=(self.tag_name, "text" + self.id)); 176 | 177 | # Bind events 178 | self.set_hoverable(True); 179 | self.set_clickable(True); 180 | 181 | def __on_enter__(self, event): 182 | """(Override) Change the text to a different color when the 183 | enter event is triggered.""" 184 | if(super().__on_enter__(event)): 185 | self.canvas.itemconfig("text" + self.id, 186 | fill=self.hovered_text_color); 187 | 188 | def __on_leave__(self, event): 189 | """(Override) Change the text to a different color when the 190 | leave event is triggered.""" 191 | if(super().__on_leave__(event)): 192 | self.canvas.itemconfig("text" + self.id, 193 | fill=self.normal_text_color); 194 | 195 | class CanvasSquare(CanvasWidget): 196 | """A square that responds to mouse click event. This is for the grid 197 | board.""" 198 | 199 | def __init__(self, canvas, x, y, width, normal_color, hovered_color, 200 | disabled_color): 201 | """Initialize the square object.""" 202 | 203 | # Initialize super class 204 | CanvasWidget.__init__(self, canvas); 205 | 206 | # Set color scheme for different states 207 | self.normal_color = normal_color; 208 | self.hovered_color = hovered_color; 209 | self.disabled_color = disabled_color; 210 | 211 | # Create the circle background 212 | canvas.create_rectangle(x - width/2, y - width/2, x + width/2, 213 | y + width/2, fill=self.normal_color, outline="", 214 | tags=(self.tag_name, "oval" + self.id)); 215 | 216 | # Bind events 217 | self.set_hoverable(True); 218 | self.set_clickable(True); 219 | 220 | def disable(self): 221 | """(Override) Change the color when the square is disabled.""" 222 | super().disable(); 223 | self.canvas.itemconfig(self.tag_name, fill=self.disabled_color); 224 | 225 | def enable(self): 226 | """(Override) Change the color back to normal when the square 227 | is disabled.""" 228 | super().enable(); 229 | self.canvas.itemconfig(self.tag_name, fill=self.normal_color); 230 | 231 | def set_temp_color(self, color): 232 | self.canvas.itemconfig(self.tag_name, fill=color); 233 | 234 | class BaseScene(tkinter.Canvas): 235 | """(Abstract) The base class for all scenes. BaseScene deals with 236 | general widgets and handles window resizing event.""" 237 | 238 | def __init__(self, parent): 239 | """Initializes the scene.""" 240 | 241 | # Initialize the superclass Canvas 242 | tkinter.Canvas.__init__(self, parent, bg=C_COLOR_BLUE_LIGHT, 243 | width=C_WINDOW_WIDTH, height=C_WINDOW_HEIGHT); 244 | 245 | # Bind the window-resizing event 246 | self.bind("", self.__on_resize__); 247 | 248 | # Set self.width and self.height for later use 249 | self.width = C_WINDOW_WIDTH; 250 | self.height = C_WINDOW_HEIGHT; 251 | 252 | def __on_resize__(self, event): 253 | """(Private) This function is called when the window is being 254 | resied.""" 255 | 256 | # Determine the ratio of old width/height to new width/height 257 | self.wscale = float(event.width)/self.width; 258 | self.hscale = float(event.height)/self.height; 259 | self.width = event.width; 260 | self.height = event.height; 261 | 262 | # Resize the canvas 263 | self.config(width=self.width, height=self.height); 264 | 265 | # Rescale all the objects tagged with the "all" tag 266 | self.scale("all", 0, 0, self.wscale, self.hscale); 267 | 268 | def create_button(self, x, y, button_text, 269 | normal_color=C_COLOR_BLUE, hovered_color=C_COLOR_BLUE_DARK, 270 | normal_text_color=C_COLOR_BLUE_DARK, 271 | hovered_text_color=C_COLOR_BLUE_LIGHT): 272 | """Creates a button widget and returns it. Note this will 273 | return a CanvasButton object, not the ID as other standard 274 | Tkinter canvas widgets usually returns.""" 275 | 276 | return CanvasButton(self, x, y, button_text, 277 | normal_color, hovered_color, 278 | normal_text_color, hovered_text_color); 279 | 280 | def create_square(self, x, y, width, 281 | normal_color=C_COLOR_BLUE, hovered_color=C_COLOR_BLUE_DARK, 282 | disabled_color=C_COLOR_BLUE_LIGHT): 283 | """Creates a square widget and returns it. Note this will 284 | return a CanvasSquare object, not the ID as other standard 285 | Tkinter canvas widgets usually returns.""" 286 | 287 | return CanvasSquare(self, x, y, width, 288 | normal_color, hovered_color, disabled_color); 289 | 290 | def create_clickable_label(self, x, y, button_text, 291 | normal_color=C_COLOR_BLUE_DARK, hovered_color=C_COLOR_BLUE_LIGHT): 292 | """Creates a clickable label widget and returns it. Note this 293 | will return a CanvasClickableLabel object, not the ID as other 294 | standard Tkinter canvas widgets usually returns.""" 295 | 296 | return CanvasClickableLabel(self, x, y, button_text, 297 | normal_color, hovered_color); 298 | 299 | class WelcomeScene(BaseScene): 300 | """WelcomeScene is the first scene to show when the GUI starts.""" 301 | 302 | def __init__(self, parent): 303 | """Initializes the welcome scene.""" 304 | 305 | # Initialize BaseScene 306 | super().__init__(parent); 307 | 308 | # Create a blue arch at the top of the canvas 309 | self.create_arc((-64, -368, C_WINDOW_WIDTH + 64, 192), 310 | start=0, extent=-180, fill=C_COLOR_BLUE, outline=""); 311 | 312 | try: 313 | # From the logo image file create a PhotoImage object 314 | self.logo_image = tkinter.PhotoImage(file="res/icon.png"); 315 | # Create the logo image at the center of the canvas 316 | logo = self.create_image((C_WINDOW_WIDTH/2, 317 | C_WINDOW_HEIGHT/2 - 96), image=self.logo_image); 318 | # From the title image file create a PhotoImage object 319 | self.title_image = tkinter.PhotoImage(file="res/title.png"); 320 | # Create the logo image at the center of the canvas 321 | title = self.create_image((C_WINDOW_WIDTH/2, 322 | C_WINDOW_HEIGHT/2 + 48), image=self.title_image); 323 | except: 324 | # An error has been caught when creating the logo image 325 | tkinter.messagebox.showerror("Error", "Can't create images.\n" + 326 | "Please make sure the res folder is in the same directory" + 327 | " as this script."); 328 | 329 | # Create the Play button 330 | play_btn = self.create_button(C_WINDOW_WIDTH/2, 331 | C_WINDOW_HEIGHT/2 + 136, "Play"); 332 | play_btn.command = self.__on_play_clicked__; 333 | # Create the About button 334 | about_btn = self.create_button(C_WINDOW_WIDTH/2, 335 | C_WINDOW_HEIGHT/2 + 192, "About"); 336 | about_btn.command = self.__on_about_clicked__; 337 | 338 | # Tag all of the drawn widgets for later reference 339 | self.addtag_all("all"); 340 | 341 | def __on_play_clicked__(self): 342 | """(Private) Switches to the main game scene when the play 343 | button is clicked.""" 344 | self.pack_forget(); 345 | self.main_game_scene.pack(); 346 | 347 | def __on_about_clicked__(self): 348 | """(Private) Switches to the about scene when the about button 349 | is clicked.""" 350 | self.pack_forget(); 351 | self.about_scene.pack(); 352 | 353 | class AboutScene(BaseScene): 354 | """AboutScene shows the developer and copyright information.""" 355 | 356 | def __init__(self, parent): 357 | """Initializes the about scene object.""" 358 | 359 | # Initialize the base scene 360 | super().__init__(parent); 361 | 362 | # Create a blue arch at the bottom of the canvas 363 | self.create_arc((-128, C_WINDOW_HEIGHT - 128, 364 | C_WINDOW_WIDTH + 128, C_WINDOW_HEIGHT + 368), 365 | start=0, extent=180, fill=C_COLOR_BLUE, outline=""); 366 | 367 | try: 368 | # From the Charmy image file create a PhotoImage object 369 | self.charmy_image = tkinter.PhotoImage(file="res/charmy.png"); 370 | # Create the logo image on the left of the canvas 371 | logo = self.create_image((C_WINDOW_WIDTH/2 - 192, 372 | C_WINDOW_HEIGHT/2 - 48), image=self.charmy_image); 373 | # From the title image file create a PhotoImage object 374 | self.title_image = tkinter.PhotoImage(file="res/title.png"); 375 | # Resize the image to make it smaller 376 | self.title_image = self.title_image.subsample(2, 2); 377 | # Create the logo image at the center of the canvas 378 | title = self.create_image((C_WINDOW_WIDTH/2 + 64, 379 | C_WINDOW_HEIGHT/2 - 160), image=self.title_image); 380 | except: 381 | # An error has been caught when creating the logo image 382 | tkinter.messagebox.showerror("Error", "Can't create images.\n" + 383 | "Please make sure the res folder is in the same directory" + 384 | " as this script."); 385 | 386 | self.create_text(C_WINDOW_WIDTH/2 - 80, C_WINDOW_HEIGHT/2 - 96, 387 | font="Helvetica 14", text="Developed by Charlie Chen", 388 | anchor="w", fill=C_COLOR_BLUE_DARK); 389 | 390 | link_charmysoft = self.create_clickable_label(C_WINDOW_WIDTH/2 - 80, 391 | C_WINDOW_HEIGHT/2 - 64, "http://CharmySoft.com", 392 | "#0B0080", "#CC2200"); 393 | link_charmysoft.config(anchor="w"); 394 | link_charmysoft.command = self.__on_charmysoft_clicked__; 395 | 396 | self.create_text(C_WINDOW_WIDTH/2 - 80, C_WINDOW_HEIGHT/2, 397 | anchor="w", font="Helvetica 14", fill=C_COLOR_BLUE_DARK, 398 | text="Tic Tac Toe Online in Python is \n" + 399 | "open source under the MIT license"); 400 | 401 | link_project = self.create_clickable_label(C_WINDOW_WIDTH/2 - 80, 402 | C_WINDOW_HEIGHT/2 + 40, "http://CharmySoft.com/app/ttt-python.htm", 403 | "#0B0080", "#CC2200"); 404 | link_project.config(anchor="w"); 405 | link_project.command = self.__on_project_link_clicked__; 406 | 407 | self.create_text(C_WINDOW_WIDTH/2 + 64, C_WINDOW_HEIGHT/2 + 96, 408 | font="Helvetica 16", text="Copyright (c) 2016 CharmySoft", 409 | fill=C_COLOR_BLUE_DARK); 410 | 411 | # Create the OK button 412 | ok_btn = self.create_button(C_WINDOW_WIDTH/2, C_WINDOW_HEIGHT/2 + 160, 413 | "OK", C_COLOR_BLUE_DARK, C_COLOR_BLUE_LIGHT, C_COLOR_BLUE_LIGHT, 414 | C_COLOR_BLUE_DARK); 415 | ok_btn.command = self.__on_ok_clicked__; 416 | 417 | # Tag all of the drawn widgets for later reference 418 | self.addtag_all("all"); 419 | 420 | def __on_ok_clicked__(self): 421 | """(Private) Switches back to the welcome scene when the ok button 422 | is clicked.""" 423 | self.pack_forget(); 424 | self.welcome_scene.pack(); 425 | 426 | def __on_charmysoft_clicked__(self): 427 | """(Private) Opens CharmySoft.com in the system default browser 428 | when the CharmySoft.com link is clicked.""" 429 | webbrowser.open("http://www.CharmySoft.com/about.htm"); 430 | 431 | def __on_project_link_clicked__(self): 432 | """(Private) Opens the project link in the system default browser 433 | when it is clicked.""" 434 | webbrowser.open("http://www.CharmySoft.com/ttt-python.htm"); 435 | 436 | class MainGameScene(BaseScene): 437 | """MainGameScene deals with the game logic.""" 438 | 439 | def __init__(self, parent): 440 | """Initializes the main game scene object.""" 441 | 442 | # Initialize the base scene 443 | super().__init__(parent); 444 | 445 | # Initialize instance variables 446 | self.board_grids_power = 3; # Make it a 3x3 grid board 447 | self.board_width = 256; # The board is 256x256 wide 448 | 449 | # Create a blue arch at the bottom of the canvas 450 | self.create_arc((-128, C_WINDOW_HEIGHT - 64, C_WINDOW_WIDTH + 128, 451 | C_WINDOW_HEIGHT + 368), start=0, extent=180, fill=C_COLOR_BLUE, 452 | outline=""); 453 | 454 | # Create the return button 455 | return_btn = self.create_button(C_WINDOW_WIDTH - 128, 32, "Go back"); 456 | return_btn.command = self.__on_return_clicked__; 457 | 458 | self.draw_board(); 459 | 460 | # Create the player_self_text 461 | player_self_text = self.create_text(96, 128, font="Helvetica 16", 462 | fill=C_COLOR_BLUE_DARK, tags=("player_self_text"), anchor="n"); 463 | # Create the player_match_text 464 | player_match_text = self.create_text(C_WINDOW_WIDTH - 96, 128, 465 | font="Helvetica 16", fill=C_COLOR_BLUE_DARK, 466 | tags=("player_match_text"), anchor="n"); 467 | 468 | # Create the notif text 469 | notif_text = self.create_text(8, C_WINDOW_HEIGHT-8, anchor="sw", 470 | font="Helvetica 16", fill=C_COLOR_BLUE_DARK, tags=("notif_text")); 471 | 472 | # Set restart button to None so it won't raise AttributeError 473 | self.restart_btn = None; 474 | 475 | # Tag all of the drawn widgets for later reference 476 | self.addtag_all("all"); 477 | 478 | def pack(self): 479 | """(Override) When the scene packs, start the client thread.""" 480 | super().pack(); 481 | # Start a new thread to deal with the client communication 482 | threading.Thread(target=self.__start_client__).start(); 483 | 484 | def draw_board(self, board_line_width = 4): 485 | """Draws the board at the center of the screen, parameter 486 | board_line_width determines the border line width.""" 487 | 488 | # Create squares for the grid board 489 | self.squares = [None] * self.board_grids_power ** 2; 490 | for i in range(0, self.board_grids_power): 491 | for j in range(0, self.board_grids_power): 492 | self.squares[i+j*3] = self.create_square( 493 | (C_WINDOW_WIDTH - self.board_width)/2 + 494 | self.board_width/self.board_grids_power * i + 495 | self.board_width / self.board_grids_power / 2, 496 | (C_WINDOW_HEIGHT - self.board_width)/2 + 497 | self.board_width/self.board_grids_power * j + 498 | self.board_width / self.board_grids_power / 2, 499 | self.board_width / self.board_grids_power); 500 | # Disable those squares to make them unclickable 501 | self.squares[i+j*3].disable(); 502 | 503 | # Draw the border lines 504 | for i in range(1, self.board_grids_power): 505 | # Draw horizontal lines 506 | self.create_line((C_WINDOW_WIDTH - self.board_width)/2, 507 | (C_WINDOW_HEIGHT - self.board_width)/2 + 508 | self.board_width/self.board_grids_power * i, 509 | (C_WINDOW_WIDTH + self.board_width)/2, 510 | (C_WINDOW_HEIGHT - self.board_width)/2 + 511 | self.board_width/self.board_grids_power * i, 512 | fill=C_COLOR_BLUE_DARK, width=board_line_width); 513 | # Draw vertical lines 514 | self.create_line((C_WINDOW_WIDTH - self.board_width)/2 + 515 | self.board_width/self.board_grids_power * i, 516 | (C_WINDOW_HEIGHT - self.board_width)/2, 517 | (C_WINDOW_WIDTH - self.board_width)/2 + 518 | self.board_width/self.board_grids_power * i, 519 | (C_WINDOW_HEIGHT + self.board_width)/2, 520 | fill=C_COLOR_BLUE_DARK, width=board_line_width); 521 | 522 | def __start_client__(self): 523 | """(Private) Starts the client side.""" 524 | # Initialize the client object 525 | self.client = TTTClientGameGUI(); 526 | # Gives the client a reference to self 527 | self.client.canvas = self; 528 | try: 529 | # Get the host IP address 530 | host = socket.gethostbyname('s.CharmySoft.com'); 531 | except: 532 | # If can't get the host IP from the domain 533 | tkinter.messagebox.showerror("Error", "Failed to get the game "+ 534 | "host address from the web domain.\n" + 535 | "Plase check your connection."); 536 | self.__on_return_clicked__(); 537 | return; 538 | # Set the notif text 539 | self.set_notif_text("Connecting to the game server " + host + "..."); 540 | # Connect to the server 541 | if(self.client.connect(host, "8080")): 542 | # If connected to the server 543 | # Start the game 544 | self.client.start_game(); 545 | # Close the client 546 | self.client.close(); 547 | 548 | def __on_return_clicked__(self): 549 | """(Private) Switches back to the welcome scene when the return 550 | button is clicked.""" 551 | # Clear screen 552 | self.__clear_screen(); 553 | # Set the client to None so the client thread will stop due to error 554 | self.client.client_socket = None; 555 | self.client = None; 556 | # Switch to the welcome scene 557 | self.pack_forget(); 558 | self.welcome_scene.pack(); 559 | 560 | def set_notif_text(self, text): 561 | """Sets the notification text.""" 562 | self.itemconfig("notif_text", text=text); 563 | 564 | def update_board_content(self, board_string): 565 | """Redraws the board content with new board_string.""" 566 | if(len(board_string) != self.board_grids_power ** 2): 567 | # If board_string is in valid 568 | print("The board string should be " + 569 | str(self.board_grids_power ** 2) + " characters long."); 570 | # Throw an error 571 | raise Exception; 572 | 573 | # Delete everything on the board 574 | self.delete("board_content"); 575 | 576 | p = 16; # Padding 577 | 578 | # Draw the board content 579 | for i in range(0, self.board_grids_power): 580 | for j in range(0, self.board_grids_power): 581 | 582 | if(board_string[i+j*3] == "O"): 583 | # If this is an "O" 584 | self.create_oval( 585 | (C_WINDOW_WIDTH - self.board_width)/2 + 586 | self.board_width/self.board_grids_power * i + p, 587 | (C_WINDOW_HEIGHT - self.board_width)/2 + 588 | self.board_width/self.board_grids_power * j + p, 589 | (C_WINDOW_WIDTH - self.board_width)/2 + 590 | self.board_width/self.board_grids_power * (i + 1) - p, 591 | (C_WINDOW_HEIGHT - self.board_width)/2 + 592 | self.board_width/self.board_grids_power * (j + 1) - p, 593 | fill="", outline=C_COLOR_BLUE_DARK, width=4, 594 | tags="board_content"); 595 | elif(board_string[i+j*3] == "X"): 596 | # If this is an "X" 597 | self.create_line( 598 | (C_WINDOW_WIDTH - self.board_width)/2 + 599 | self.board_width/self.board_grids_power * i + p, 600 | (C_WINDOW_HEIGHT - self.board_width)/2 + 601 | self.board_width/self.board_grids_power * j + p, 602 | (C_WINDOW_WIDTH - self.board_width)/2 + 603 | self.board_width/self.board_grids_power * (i + 1) - p, 604 | (C_WINDOW_HEIGHT - self.board_width)/2 + 605 | self.board_width/self.board_grids_power * (j + 1) - p, 606 | fill=C_COLOR_BLUE_DARK, width=4, 607 | tags="board_content"); 608 | self.create_line( 609 | (C_WINDOW_WIDTH - self.board_width)/2 + 610 | self.board_width/self.board_grids_power * (i + 1) - p, 611 | (C_WINDOW_HEIGHT - self.board_width)/2 + 612 | self.board_width/self.board_grids_power * j + p, 613 | (C_WINDOW_WIDTH - self.board_width)/2 + 614 | self.board_width/self.board_grids_power * i + p, 615 | (C_WINDOW_HEIGHT - self.board_width)/2 + 616 | self.board_width/self.board_grids_power * (j + 1) - p, 617 | fill=C_COLOR_BLUE_DARK, width=4, 618 | tags="board_content"); 619 | 620 | def draw_winning_path(self, winning_path): 621 | """Marks on the board the path that leads to the win result.""" 622 | # Loop through the board 623 | for i in range(0, self.board_grids_power ** 2): 624 | if str(i) in winning_path: 625 | # If the current item is in the winning path 626 | self.squares[i].set_temp_color("#db2631"); 627 | 628 | 629 | def show_restart(self): 630 | """Creates a restart button for the user to choose to restart a 631 | new game.""" 632 | self.restart_btn = self.create_button(C_WINDOW_WIDTH/2, C_WINDOW_HEIGHT - 32, 633 | "Restart", C_COLOR_BLUE_DARK, C_COLOR_BLUE_LIGHT, C_COLOR_BLUE_LIGHT, 634 | C_COLOR_BLUE_DARK); 635 | self.restart_btn.command = self.__on_restart_clicked__; 636 | 637 | def __clear_screen(self): 638 | """(Private) Clears all the existing content from the old game.""" 639 | # Clear everything from the past game 640 | for i in range(0, self.board_grids_power ** 2): 641 | self.squares[i].disable(); 642 | self.squares[i].set_temp_color(C_COLOR_BLUE_LIGHT); 643 | self.update_board_content(" " * self.board_grids_power ** 2); 644 | self.itemconfig("player_self_text", text=""); 645 | self.itemconfig("player_match_text", text=""); 646 | # Delete the button from the scene 647 | if self.restart_btn is not None: 648 | self.restart_btn.delete(); 649 | self.restart_btn = None; 650 | 651 | def __on_restart_clicked__(self): 652 | """(Private) Switches back to the welcome scene when the return 653 | button is clicked.""" 654 | # Clear screen 655 | self.__clear_screen(); 656 | # Start a new thread to deal with the client communication 657 | threading.Thread(target=self.__start_client__).start(); 658 | 659 | 660 | class TTTClientGameGUI(TTTClientGame): 661 | """The client implemented with GUI.""" 662 | 663 | def __connect_failed__(self): 664 | """(Override) Updates the GUI to notify the user that the connection 665 | couldn't be established.""" 666 | # Write the notif text 667 | self.canvas.set_notif_text("Can't connect to the game server.\n" + 668 | "It might be down or blocked by your firewall."); 669 | # Throw an error and finish the client thread 670 | raise Exception; 671 | 672 | def __connected__(self): 673 | """(Override) Updates the GUI to notify the user that the connection 674 | has been established.""" 675 | self.canvas.set_notif_text("Server connected. \n" + 676 | "Waiting for other players to join..."); 677 | 678 | def __game_started__(self): 679 | """(Override) Updates the GUI to notify the user that the game is 680 | getting started.""" 681 | self.canvas.set_notif_text("Game started. " + 682 | "You are the \"" + self.role + "\""); 683 | self.canvas.itemconfig("player_self_text", 684 | text="You:\n\nPlayer " + str(self.player_id) + 685 | "\n\nRole: " + self.role); 686 | self.canvas.itemconfig("player_match_text", 687 | text="Opponent:\n\nPlayer " + str(self.match_id) + 688 | "\n\nRole: " + ("O" if self.role == "X" else "X") ); 689 | 690 | def __update_board__(self, command, board_string): 691 | """(Override) Updates the board.""" 692 | # Print the command-line board for debugging purpose 693 | super().__update_board__(command, board_string); 694 | # Draw the GUI board 695 | self.canvas.update_board_content(board_string); 696 | if(command == "D"): 697 | # If the result is a draw 698 | self.canvas.set_notif_text("It's a draw."); 699 | # Show the restart button 700 | self.canvas.show_restart(); 701 | elif(command == "W"): 702 | # If this player wins 703 | self.canvas.set_notif_text("You WIN!"); 704 | # Show the restart button 705 | self.canvas.show_restart(); 706 | elif(command == "L"): 707 | # If this player loses 708 | self.canvas.set_notif_text("You lose."); 709 | # Show the restart button 710 | self.canvas.show_restart(); 711 | 712 | def __player_move__(self, board_string): 713 | """(Override) Lets the user to make a move and sends it back to the 714 | server.""" 715 | 716 | # Set user making move to be true 717 | self.making_move = True; 718 | 719 | for i in range(0, self.canvas.board_grids_power ** 2): 720 | # Check the board content and see if it's empty 721 | if(board_string[i] == " "): 722 | # Enable those squares to make them clickable 723 | self.canvas.squares[i].enable(); 724 | # Bind their commands 725 | self.canvas.squares[i].command = (lambda self=self, i=i: 726 | self.__move_made__(i)); 727 | 728 | while self.making_move: 729 | # Wait until the user has clicked on something 730 | pass; 731 | 732 | def __player_wait__(self): 733 | """(Override) Lets the user know it's waiting for the other player 734 | to make a move.""" 735 | # Print the command-line notif for debugging purpose 736 | super().__player_wait__(); 737 | # Set the notif text on the GUI 738 | self.canvas.set_notif_text("Waiting for the other player to make a move..."); 739 | 740 | def __opponent_move_made__(self, move): 741 | """(Override) Shows the user the move that the other player has taken.""" 742 | # Print the command-line notif for debugging purpose 743 | super().__opponent_move_made__(move); 744 | # Set the notif text on the GUI 745 | self.canvas.set_notif_text("Your opponent took up number " + str(move) + ".\n" 746 | "It's now your turn, please make a move."); 747 | 748 | def __move_made__(self, index): 749 | """(Private) This function is called when the user clicks on the 750 | board to make a move.""" 751 | 752 | print("User chose " + str(index + 1)); 753 | 754 | for i in range(0, self.canvas.board_grids_power ** 2): 755 | # Disable those squares to make them unclickable 756 | self.canvas.squares[i].disable(); 757 | # Remove their commands 758 | self.canvas.squares[i].command = None; 759 | 760 | # Send the position back to the server 761 | self.s_send("i", str(index + 1)); 762 | 763 | # Set user making move to be false 764 | self.making_move = False; 765 | 766 | def __draw_winning_path__(self, winning_path): 767 | """(Override) Shows to the user the path that has caused the game to 768 | win or lose.""" 769 | # Print the command-line winning path for debugging purpose 770 | super().__draw_winning_path__(winning_path); 771 | # Draw GUI the winning path 772 | self.canvas.draw_winning_path(winning_path); 773 | 774 | # Define the main program 775 | def main(): 776 | # Create a Tkinter object 777 | root = tkinter.Tk(); 778 | # Set window title 779 | root.title("Tic Tac Toe"); 780 | # Set window minimun size 781 | root.minsize(C_WINDOW_MIN_WIDTH, C_WINDOW_MIN_HEIGHT); 782 | # Set window size 783 | root.geometry(str(C_WINDOW_WIDTH) + "x" + str(C_WINDOW_HEIGHT)); 784 | 785 | try: 786 | # Set window icon 787 | root.iconbitmap("res/icon.ico"); 788 | except: 789 | # An error has been caught when setting the icon 790 | # tkinter.messagebox.showerror("Error", "Can't set the window icon."); 791 | print("Can't set the window icon."); 792 | 793 | # Initialize the welcome scene 794 | welcome_scene = WelcomeScene(root); 795 | # Initialize the about scene 796 | about_scene = AboutScene(root); 797 | # Initialize the main game scene 798 | main_game_scene = MainGameScene(root); 799 | 800 | # Give a reference for switching between scenes 801 | welcome_scene.about_scene = about_scene; 802 | welcome_scene.main_game_scene = main_game_scene; 803 | about_scene.welcome_scene = welcome_scene; 804 | main_game_scene.welcome_scene = welcome_scene; 805 | 806 | # Start showing the welcome scene 807 | welcome_scene.pack(); 808 | 809 | # Main loop 810 | root.mainloop(); 811 | 812 | if __name__ == "__main__": 813 | # If this script is running as a standalone program, 814 | # start the main program. 815 | main(); --------------------------------------------------------------------------------