Skip to content

Commit

Permalink
Implement /makemove route (#22)
Browse files Browse the repository at this point in the history
* Add validation for /makemove

* Fix validation for when game is over

* Prepare test suite for /makemove

* Fix 'validate_move' comment

* Check for gameover before making move

* Finish /makemove tests
  • Loading branch information
eonu authored and notexactlyawe committed Feb 26, 2019
1 parent 0b4dee1 commit 048074f
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 26 deletions.
5 changes: 5 additions & 0 deletions server/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

import server
52 changes: 44 additions & 8 deletions server/schemas/game.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from marshmallow import Schema, fields, validates, ValidationError
from marshmallow import Schema, fields, validates, validates_schema, ValidationError
from server.game import Game

OPEN_SLOT = "OPEN"
AI = "AI"
USER_COLLECTION = "users"
GAMES_COLLECTION = "games"
GAME_COLLECTION = "games"

def assert_player_exists(player, db):
"""Helper function that checks if a given player id exists in db"""
Expand All @@ -19,11 +20,46 @@ class MakeMoveInput(Schema):
# Identifier for the game to make the move on
game_id = fields.String(required=True)

@validates('move')
def validate_move(self, value):
# TODO: Check move decodes properly
# TODO: Check move is applicable to the board
pass
def __init__(self, db):
super().__init__()
self.db = db

@validates_schema
def validate_move(self, data):
# Validate 'game_id'
game_ref = self.db.collection(GAME_COLLECTION).document(data['game_id']).get()
if not game_ref.exists:
raise ValidationError(f"Game {data['game_id']} doesn\'t exist!")

# Create a game object for validation
game = Game.from_dict(game_ref.to_dict())

# Validate 'user_id'
if data['user_id'] == OPEN_SLOT or data['user_id'] == AI:
pass
else:
user_ref = self.db.collection(USER_COLLECTION).document(data['user_id']).get()
if not user_ref.exists:
raise ValidationError(f"User {data['user_id']} doesn\'t exist!")

# Check if user is one of the players of the game
if data['user_id'] not in game.players.values():
raise ValidationError(f"User {data['user_id']} is not a player in this game.")

# Check that it's the user's turn
if data['user_id'] != game.players[game.turn]:
raise ValidationError(f"User {data['user_id']} cannot move when it is not their turn.")

# Check that game is not over
if not game.in_progress:
raise ValidationError(f"Game {data['game_id']} is over.")

# Validate 'move'
try:
# Check move is valid SAN and applicable to the board
game.move(data['move'])
except ValueError:
raise ValidationError(f"Invalid move {data['move']} in current context.")

class CreateGameInput(Schema):
# ID of the user that creates the game
Expand Down Expand Up @@ -72,7 +108,7 @@ def __init__(self, db):

@validates('game_id')
def game_exists(self, value):
game_ref = self.db.collection(GAMES_COLLECTION).document(value).get()
game_ref = self.db.collection(GAME_COLLECTION).document(value).get()
if not game_ref.exists:
raise ValidationError('Game doesn\'t exist!')

Expand Down
18 changes: 16 additions & 2 deletions server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,24 @@ def main():

@app.route('/makemove', methods=["POST"])
def make_move():
errors = MakeMoveInput().validate(request.form)
errors = MakeMoveInput(db).validate(request.form)
if errors:
abort(BAD_REQUEST, str(errors))
return REQUEST_OK

# Get the game reference and construct a Game object
game_ref = db.collection(GAMES_COLLECTION).document(request.form['game_id'])
game = Game.from_dict(game_ref.get().to_dict())

# Make the requested move on the game object
game.move(request.form['move'])

# Export the updated Game object to a dict
game_dict = game.to_dict()

# Write the updated Game dict to Firebase
game_ref.set(game_dict)

return jsonify(game_dict)

@app.route('/getgame/<game_id>')
def get_game(game_id):
Expand Down
149 changes: 133 additions & 16 deletions test/routes/test_make_move.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"""Test cases for the POST server route /makemove."""

import unittest
import pytest
from server.server import app
from unittest.mock import patch
from .mock_firebase import MockClient

OK = 200
BAD_REQUEST = 400

class MakeMoveTest(unittest.TestCase):
# Setup and helper functions
Expand All @@ -21,21 +27,132 @@ def post(self, data):
"""
return MakeMoveTest.client.post(MakeMoveTest.route, data=data)

def setUp(self):
self.params = {'game_id': None, 'user_id': None, 'move': None}
self.mock_game = {
'id': 'some_game',
'creator': 'some_creator',
'players': {'w': 'some_player_1', 'b': 'some_player_2'},
'free_slots': 2,
'time_controls': None,
'remaining_time': {'w': None, 'b': None},
'resigned': {'w': False, 'b': False},
'draw_offers': {
'w': {'made': False, 'accepted': False},
'b': {'made': False, 'accepted': False}
},
'in_progress': True,
'result': '*',
'game_over': {'game_over': False, 'reason': None},
'turn': 'w',
'ply_count': 0,
'move_count': 1,
'pgn': '',
'history': [],
'fen': 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
}

def fill_params(self, game_id=None, user_id=None, move=None):
self.params['game_id'] = game_id
self.params['user_id'] = user_id
self.params['move'] = move

def set_up_mock_db(self, mock_db):
"""Creates some entries in the mock database"""
mock_db.collection("users").add({}, document_id='some_creator')
mock_db.collection("users").add({}, document_id='some_player_1')
mock_db.collection("users").add({}, document_id='some_player_2')
mock_db.collection("games").add(self.mock_game, document_id='some_game')

# Tests

def test_missing_params(self):
# NOTE: Request with 'game_id' missing from params
response = self.post({
'user_id': 'someuser',
'move': 'd4'
})
self.assertEqual(response.status_code, 400)

def test_correct_params(self):
# NOTE: Request with all required params
response = self.post({
'user_id': 'someuser',
'move': 'Qxe6',
'game_id': 'somegame'
})
self.assertEqual(response.status_code, 200)
@patch('server.server.db', new_callable=MockClient)
def test_game_doesnt_exist(self, mock_db):
"""Make a move on a game that doesn't exist."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='game_that_doesnt_exist', user_id='some_player_1', move='e4')
response = self.post(self.params)
self.assertEqual(BAD_REQUEST, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_game_exists(self, mock_db):
"""Make a move on a game that exists."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='some_player_1', move='e4')
response = self.post(self.params)
self.assertEqual(OK, response.status_code)

@pytest.mark.skip(reason = "Need to decide how open slots will be dealt with")
@patch('server.server.db', new_callable=MockClient)
def test_user_id_open_slot(self, mock_db):
"""Make a move with the user ID set as the open slot."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='OPEN', move='e4')
response = self.post(self.params)
self.assertEqual(OK, response.status_code)

@pytest.mark.skip(reason = "Need to decide how AI slots will be dealt with")
@patch('server.server.db', new_callable=MockClient)
def test_user_id_ai_slot(self):
"""Make a move with the user ID set as the AI slot."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='AI', move='e4')
response = self.post(self.params)
self.assertEqual(OK, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_user_id_doesnt_exist(self, mock_db):
"""Make a move with the ID of a user that doesn't exist."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='user_that_doesnt_exist', move='e4')
response = self.post(self.params)
self.assertEqual(BAD_REQUEST, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_user_not_in_game(self, mock_db):
"""Make a move with the ID of a user that exists, but isn't a player in the game."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='some_creator', move='e4')
response = self.post(self.params)
self.assertEqual(BAD_REQUEST, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_user_wrong_turn(self, mock_db):
"""Make a move from the player whose side it isn't to play."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='some_player_2', move='e4')
response = self.post(self.params)
self.assertEqual(BAD_REQUEST, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_user_correct_turn(self, mock_db):
"""Make a move from the player whose side it is to play."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='some_player_1', move='e4')
response = self.post(self.params)
self.assertEqual(OK, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_game_over(self, mock_db):
"""Make a move in a game which is over."""
self.mock_game['resigned']['w'] = True
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='some_player_1', move='e4')
response = self.post(self.params)
self.assertEqual(BAD_REQUEST, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_move_invalid_san_notation(self, mock_db):
"""Make a move with invalid SAN notation."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='some_player_1', move='wrong_notation')
response = self.post(self.params)
self.assertEqual(BAD_REQUEST, response.status_code)

@patch('server.server.db', new_callable=MockClient)
def test_move_invalid_san_context(self, mock_db):
"""Make a move which is valid SAN, but invalid in the current context."""
self.set_up_mock_db(mock_db)
self.fill_params(game_id='some_game', user_id='some_player_1', move='Nc6')
response = self.post(self.params)
self.assertEqual(BAD_REQUEST, response.status_code)

0 comments on commit 048074f

Please sign in to comment.