I (a junior back-end Java dev) decided I wanted to learn some front-end development skills; in particular, I wanted experience with React and websockets. I decided to write an app that would allow for remote play of a 9X9 variant of tic tac toe one of my college friends made up. The rules are:
- The board is divided into 9 smaller games of tic tac toe, arranged in a tic tac toe grid.
- Winning 3 boards in a row on the larger grid wins you the game.
- Wherever you play in a smaller grid determines where your opponent must play in the larger grid; i.e. if I play in the bottom right corner of any smaller grid, then my opponent is limited to the bottom right corner of the larger grid on their next turn. I'll post screenshots to clarify.X has played in the bottom-right of their sub-square, so O is limited to the bottom right of the larger square
- If the square on the larger grid you would be forced to play in is full, then you can play in any of the larger grid squares instead.
I decided on socket.io to handle websockets for me, and picked flask_socketio to handle things in python. With that done, I wrote a react app to display the game and a python server to handle the logic. Then I added chat capability, just for fun.
Here's the react code.
import SuperBoard from './superboard.js'
import openSocket from 'socket.io-client'
import SocketContext from './socket-context'
import ChatBox from './chatbox.js'
const port = '1337';
//For remote games, change this to the ip of the host machine
const ip = '0.0.0.0';
const socket = openSocket('http://' + ip + ':' + port);
class Game extends React.Component {
constructor(props) {
super(props)
this.state = {
boards: initBoards(),
wonBoards: Array(9).fill(''),
lastPlayed: -1,
yourTurn: false,
status: 'Waiting for another player...',
}
socket.on('boards', boards => {
this.setState({boards: boards})
});
socket.on('wonboards', wonBoards => {
this.setState({wonBoards: wonBoards})
});
socket.on('lastPlayed', lastPlayed => {
this.setState({lastPlayed: lastPlayed})
});
socket.on('x_or_o', x_or_o => {
this.setState({x_or_o: x_or_o})
});
socket.on('turn', player => {
if (player === this.state.x_or_o) {
this.setState({status: "You're up.", yourTurn: true})
} else {
this.setState({status: player + ' is thinking.', yourTurn: false})
}
});
socket.on('victory', player => {
if (player === this.state.color) {
this.setState({status: 'You win!', yourTurn: false})
} else {
this.setState({status: 'You lose!', yourTurn: false})
}
});
}
handleClick(i,j) {
console.log("Sending click: " + i + " " + j);
socket.emit('click', {i:i, j:j});
}
render() {
const boards = this.state.boards;
const wonBoards = this.state.wonBoards;
const lastPlayed = this.state.lastPlayed;
const status = this.state.status;
const username = this.state.x_or_o;
return (
<div className="game">
<div className="game-board">
<SuperBoard
boards={boards}
onClick={(i,j) => this.handleClick(i,j)}
wonBoards={wonBoards}
lastPlayed={lastPlayed}
/>
</div>
<div className="game-info">
<div className="status">{status}</div>
<div>
<SocketContext.Provider value={socket}>
<ChatBox username={username}/>
</SocketContext.Provider>
</div>
</div>
</div>
);
}
}
function initBoards() {
var boards = new Array(9);
for(var i = 0; i < boards.length ;i++){
boards[i] = new Array(9);
boards[i].fill('');
}
return boards;
}
export default Game
Some specific notes:
- I've never written react or socket.io code before. If you see anything that violates the best practices of either framework, let me know.
- I'm also looking for any security holes something like this could leave open. I'd like to try deploying this on an AWS EC2 server eventually, and I'd rather not have any script kiddies messing with my account.
I won't include the message code because
- it's partially taken from another developer who seems to have pretty solid react skills and
- I don't consider it particularly interesting. If you want to look at it, it's in the github repo under message.js and Chatbox.js.
- I'm aware of some minor bugs in the code (like the chat has a bug that causes it to only render on the left side, when your messages should render on the right) but let me know if you find any others. (It's not production-ready code without a few minor bugs, right?)
Here's the python code:
from flask_socketio import SocketIO, emit
from flask_cors import CORS
app = Flask(__name__)
#change this in prod
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, cors_allowed_origins="*")
CORS(app)
boards = [['' for i in range(9)] for i in range(9)]
wonBoards = ['' for i in range(9)]
lastPlayed = -1
players = {'X': None, 'O': None}
turn = 'X'
def reset():
boards = [['' for i in range(9)] for i in range(9)]
players = {'X': None, 'O': None}
turn = 'X'
@socketio.on('connect')
def connect():
print("Someone connected to websocket!")
if (players['X'] == None):
print("It was player X!")
players['X'] = request.sid
socketio.emit('x_or_o', 'X', room=players['X'])
socketio.emit('message', {"username":"System", "content":"You're playing as X"}, room=players['X'])
elif (players['O'] == None) :
print("It was player O!")
players['O'] = request.sid
socketio.emit('x_or_o', 'O', room=players['O'])
socketio.emit('message', {"username":"System", "content":"You're playing as O"}, room=players['O'])
socketio.emit('turn', 'X')
@socketio.on('disconnect')
def disconnect():
print("Player disconnected!")
if (players['X'] == request.sid):
players['X'] = None
print("It was x!")
elif (players['O'] == request.sid):
players['O'] = None
print('It was o!')
@socketio.on('post_submit')
def message(object):
[username, content] = object.values()
socketio.emit('message',{"username":username, "content":content})
@socketio.on('click')
def click(object):
[i,j] = object.values()
if (players[turn] != request.sid):
print("Wrong player clicked!")
return
if players['X'] == None or players['O'] == None:
print("Not enough players connected!")
return
#check if space is empty, the correct board is selected, the selected board is not won and the game is not over
rightBoard = (i != lastPlayed and lastPlayed != -1)
if (boards[i][j] != '' or rightBoard or wonBoards[i] or boardWin(wonBoards)):
return
#set the space to X or O
boards[i][j] = turn
#check if the board is won
updateWonBoards(i)
#check if the next board to play on is won
updateLastPlayed(j)
socketio.emit('boards', boards)
socketio.emit('wonboards', wonBoards)
socketio.emit('lastPlayed', lastPlayed)
if (boardWin(wonBoards) != ""):
socketio.emit('victory',boardWin(wonBoards))
reset()
#Toggle the player
togglePlayer()
socketio.emit('turn', turn)
def togglePlayer():
global turn
turn = 'O' if turn == 'X' else 'X'
def updateWonBoards(i):
global wonBoards
global boards
wonBoards[i] = boardWin(boards[i])
def updateLastPlayed(j):
global lastPlayed
global wonBoards
lastPlayed = -1 if wonBoards[j] != '' else j
def boardWin(board):
lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
]
for i in range(0, len(lines)):
[a, b, c] = lines[i]
if (board[a] != '' and board[a] == board[b] and board[a] == board[c]):
return board[a]
#"~" is used to indicate a draw
if "" in board:
return ""
else:
return "~"
if __name__ == '__main__':
reset()
socketio.run(app, port=1337, debug=True, host='0.0.0.0')
Some more notes:
- Again, let me know if any glaring security holes exist in my code.
- If flask_socketio isn't the best library for this, let me know.
Here's the github link for everything. I'm really excited to get some feedback on this. Also, if you have any tips on how to improve this answer or my writing skills, I'd love to hear those as well.
To anyone who tried installing the front-end from github before this post: I forgot to push up the package.json, so it wouldn't install. Everything works now.
-
\$\begingroup\$ To anyone who tried installing the front-end from github before this post: I forgot to push up the package.json, so it wouldn't install. Everything works now. \$\endgroup\$Noah White– Noah White2020年03月21日 05:53:55 +00:00Commented Mar 21, 2020 at 5:53
-
\$\begingroup\$ Welcome to Code Review! When adding additional information you should edit your question instead of adding a comment. I have added that information to your post. Learn more about comments including when to comment and when not to in the Help Center page about Comments. \$\endgroup\$Sᴀᴍ Onᴇᴌᴀ– Sᴀᴍ Onᴇᴌᴀ ♦2020年03月21日 13:27:24 +00:00Commented Mar 21, 2020 at 13:27
-
1\$\begingroup\$ I would suggest thinking about what you would do if you were to nest this idea again, i.e. what if I wanted a 3 x 3 board of superboards, say these are megaboards, what if I wanted a 3 x 3 board of megaboards? If you do this you could remove some duplication. \$\endgroup\$Countingstuff– Countingstuff2020年03月21日 14:47:57 +00:00Commented Mar 21, 2020 at 14:47
1 Answer 1
I'm not fantastic with Typescript; so let's take a look at your Python:
Credentials
app.config['SECRET_KEY'] = 'secret!'
This shouldn't be baked into your code. It should be in a secure wallet of some kind. Resources on the internet abound about how to accomplish this, either in Python or at the operating system level.
Business logic vs. presentation
boards = [['' for i in range(9)] for i in range(9)]
This is a classic example of conflating presentation (a string to be shown to the user) with business logic (is a cell filled in?)
Consider using Enum instances, or maybe Optional[bool].
Logging
Rather than
print("Someone connected to websocket!")
use the actual logging facilities of Python. They don't need to have a complex configuration; using them will better-structure the output and allow for complex configurations in the future if you want.
None comparison
if (players['X'] == None):
should be
if players['X'] is None:
also, parens are not necessary.
Unpacking
[i,j] = object.values()
can be
i, j = object.values()
That said: is object a dict? The order of values, after Python 2, is no longer non-deterministic, but (if I remember correctly) in insertion order. Generally it's a bad idea to rely on this order. You should rethink the way that these are stored and looked up. Can you rely on the key instead?
Globals
def togglePlayer():
global turn
turn = 'O' if turn == 'X' else 'X'
def updateWonBoards(i):
global wonBoards
global boards
wonBoards[i] = boardWin(boards[i])
def updateLastPlayed(j):
global lastPlayed
global wonBoards
lastPlayed = -1 if wonBoards[j] != '' else j
These globals should be in some kind of game state singleton class instead.
Mutability
lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
]
This should be a tuple of tuples, not a list of lists.
Iteration
for i in range(0, len(lines)):
should just be
for line in lines: