4
\$\begingroup\$

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:

  1. The board is divided into 9 smaller games of tic tac toe, arranged in a tic tac toe grid.
  2. Winning 3 boards in a row on the larger grid wins you the game.
  3. 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
  4. 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:

  1. 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.
  2. 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.
  3. I won't include the message code because

    1. it's partially taken from another developer who seems to have pretty solid react skills and
    2. 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.
  4. 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:

  1. Again, let me know if any glaring security holes exist in my code.
  2. 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.

\$\endgroup\$
3
  • \$\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
    Commented 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\$ Commented 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\$ Commented Mar 21, 2020 at 14:47

1 Answer 1

1
\$\begingroup\$

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:
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.