├── 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 | 
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 | 
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 | 
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 | 
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 | 
29 |
30 | 
31 |
32 | 
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 | 
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 | 
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 | 
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 | 
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();
--------------------------------------------------------------------------------