Search code examples
javascriptpythonflaskherokuchess

Tying a unique game id to a user in a Flask app to handle multiple requests to the server


I built a chess app with Python and used Flask to create a site for users to play on. I used Heroku to deploy the app (http://pythonchessapp.herokuapp.com/). I am new to web development and was wondering how I can handle multiple users (on separate laptops or tabs) going on the site to play the app? Something like having a unique game id per user to serve a different game to different requests. Below is some of my code for routes and initializing games. I basically initialize a Board object that handles moves and tracks board states. I use js to send info on moves to the server to make moves. I would also like to end a game after a user exits the site. Does anyone have any ideas?

I've only included the initial route that creates the board and renders the initial page, and the route that deals with executing moves.

from logic.chess_board import Board
from logic.chess_pieces import *

b = Board()

@app.route('/', methods=["GET", "POST"])
@app.route('/chess', methods=["GET", "POST"])
def chess():

    flipped = b.flipped

    img_dict = b.board_html()

    return render_template('base.html', img_dict=img_dict, flipped=flipped)

@app.route('/execute', methods=['GET', 'POST'])
def execute():
    if request.method == "POST":

        castle = None
        error = False
        outcome = False
        empty = None

        sq_one = eval(request.get_json()['sq_one'])
        sq_two = eval(request.get_json()['sq_two'])
        piece = b.board[sq_one]

        if type(piece) == King and (piece.castle['king_side'] == sq_two or piece.castle['queen_side'] == sq_two):
            y = sq_one[1]
            if piece.castle['king_side'] == sq_two:
                r_one = str((8, y))
                r_two = str((6, y))

            elif piece.castle['queen_side'] == sq_two:
                r_one = str((1, y))
                r_two = str((4, y))
            castle = [r_one, r_two]

        try:
            b.move(sq_one, sq_two)
            if b.game_over():
                outcome = b.outcome
            empty = b.js_remove()
        except Exception as e:
            error = str(e)
        response = {'error': error, 'castle': castle, 'empty': empty, 'outcome': outcome}

    return make_response(jsonify(response))

Solution

  • This could be achieved with the library cachelib to store your instance of Board in pickled format, using Flask's session object to store a unique key in the cookie.

    Install with pip install cachelib or add cachelib to your requirements.txt.

    Start by importing the required libs and initialising the cache:

    from flask import Flask, session
    import pickle
    from uuid import uuid4
    from cachelib.simple import SimpleCache
    
    c = SimpleCache(default_timeout=0)
    
    app.config['SECRET_KEY'] = 'somesupersecretkey'
    app = Flask(__name__)
    
    

    A quick function to return a unique ID:

    def generate_id():
        return uuid4().__str__()
    

    Instead of setting b = Board() at the global level, we will do this inside a function and return it.

    So we could define a function which loads a board. This looks to see if the key game_id exists in the session object (cookie storage). If it does, we load the board from our cache. If not, this function will just create a new board. You could also do other board initialization steps in the else clause of this block:

    def load_board():
    
        if 'game_id' in session:
            pb = c.get(session['game_id'])
            board = pickle.loads(pb)
        else:
            # Initialize new board
            board = Board()
    
        return board
    

    Now we create a function which saves the board. This immediately pickles the board we pass as an argument, then saves it in the cache. Depending on whether a game_id exists in the session object (cookie storage) it will either use that ID, or generate a new one.

    def save_board(board):
        
        pb = pickle.dumps(board)
    
        if 'game_id' in session:
            c.set(session['game_id'], pb)
        else:
            unique_id = generate_id()
            session['game_id'] = unique_id
            c.set(unique_id, pb)
    
    

    With these utility functions, you can now persist a board across requests:

    @app.route('/chess', methods=["GET", "POST"])
    def chess():
    
        b = load_board()
    
        flipped = b.flipped
        img_dict = b.board_html()
    
        save_board(b)
    
        return render_template('base.html', img_dict=img_dict, flipped=flipped)
    
    

    Then the other route:

    @app.route('/execute', methods=['GET', 'POST'])
    def execute():
        if request.method == "POST":
            b = load_board()
    
            # All of your logic
    
            # Finally save the board 
            save_board(b)
            return make_response(jsonify(response))
    
    

    There's probably different ways you could design this functionality in. SimpleCache stores everything in memory, which should be fine, assuming you only run with 1 gunicorn worker.

    Eventually if you outgrew a single worker, or found the memory footprint of your web dyno was too high, you could switch SimpleCache out for RedisCache easily without changing much logic. This would also be needed to persist the data across dyno restarts.

    The cachelib library is tiny so you can read the code to see the other available backends and methods.