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.
- 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.