7
\$\begingroup\$

The two player game four-to-one works by leaving four stones on a table. Each player may remove 2 or 1 stones from the table. The first player who cannot take away any stones loses. I created a web page that allows you to play this game against a computer that plays the game perfectly1.

Here is the code:

server.py

import hug
import json
from hug.middleware import CORSMiddleware

# Probably should never do this but lol.
api = hug.API(__name__)
api.http.add_middleware(CORSMiddleware(api))

def do_move(pos, mov):
    """
    Given a particular position perform a move in four to one.
    Note: Assumes validity of move.
    """
    return pos - mov


def gen_moves(pos):
    """
    Generate every possible move given a position in the game four to one.
    """
    if pos == 1:
        return [1]
    elif pos == 0:
        return []
    else:
        return [1, 2]

# -------------------------------------------------------------------
# Collection of functions to update state of the best position found.
# old is the previous best state data, new is the challenger.
# -------------------------------------------------------------------

def loss_loss(old, new):
    return new

def loss_win(old, new):
    return old[0], max(old[1], new[1])

def win_loss(old, new):
    return old[0], min(old[1], new[1])

def no_op(old, new):
    return old

#--------------------------------------------------------------------
#--------------------------------------------------------------------
#--------------------------------------------------------------------

STATES = {
    "LOSS" : { "LOSS" : loss_loss, "WIN" : loss_win },
    "WIN"  : { "LOSS" : win_loss,  "WIN" : no_op },
}

@hug.get('/')
def next_best_move(pos: hug.types.number):
    """
    Given a position, returns the next best move that someone should play
    Arguments:
    pos -- The current position of the board.
    """
    moves = gen_moves(pos)
    with open('fto1.json') as raw_data:
        data = json.loads(raw_data.read())
        best = moves[0]
        best_stats = [data[str(do_move(pos, moves[0]))]['value'],
                      data[str(do_move(pos, moves[0]))]['remoteness']]
        for move in moves:
            move_stats = (data[str(do_move(pos, move))]['value'],
                          data[str(do_move(pos, move))]['remoteness'])
            old_best_stats = best_stats
            best_stats = STATES[best_stats[0]][move_stats[0]](best_stats, move_stats)
            if best_stats != old_best_stats:
                best = move
    return best

logic.js

/**
 * A representation of the game four to one that 
 */
var game = new StateMachine({
    init: 'playerTurn',
    transitions: [
        { name: 'playerMove', from: 'playerTurn', to: 'computerTurn' },
        { name: 'computerMove', from: 'computerTurn', to: 'playerTurn' },
    ],

    data: {
        board: 4,
    },

    methods: {
        onPlayerMove: function(lifecycle, amount) {
            this.board -= amount;
            if (this.board <= 0) {
                $("#board span").text("Game over");
            } else {
                $("#board span").text(this.board);
            }
        },
        onComputerMove: function() {
            self = this;
            return $.ajax({
                type: "GET",
                url: "http://127.0.0.1:8000/",
                dataType: 'json',
                data: {pos: self.board },
            }).done(function (resp) {
                self.board -= resp;
                if (self.board <= 0) {
                    $("#board span").text("Game over");
                } else {
                    $("#board span").text(self.board);
                }
            });
        },
    },
});

$('document').ready(function() {
    /**
     * Remove one "stone" from the board.
     */
    $("#remove1").on('click', function() {
        game.playerMove(1);
    });

    /**
     * Remove two "stones" from the board.
     */
    $("#remove2").click(function() {
        game.playerMove(2);
    });

    /**
     * Check for updates in the state, used to determine if the computer can m-
     * ake a move.
     */
    window.setInterval(function() {
        game.computerMove();
    }, 2000);
});

index.html

<html>
  <head>
    <meta charset="UTF-8">
    <script
      src="http://code.jquery.com/jquery-3.2.1.min.js"
      integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
      crossorigin="anonymous"></script>
    <script
      type="text/javascript"
      src="state-machine.js"
    ></script>
    <script
      type="text/javascript"
      src="logic.js"
    ></script>
  </head>
  <body>
    <div id="board">
      <span>4</span>
    </div>
    <div>
      <button id="remove1">-1</button>
      <button id="remove2">-2</button>
    </div>
  </body>
</html>

There is a github project here so feel free to view/clone/download that if you find that easier to work with.

I haven't worried about styling the game yet, so it looks kind of aesthetically displeasing.

1 Well, you start off in a winning position, so you can pretty much always beat the computer.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

While you might be mostly interested in feedback on the Python code, I am still working on my Python skills. I can say that the use of storing the data in the JSON file and reading it looks good. Perhaps I can offer more feedback on the Python code in the future.

I can however offer feedback on the JavaScript.

Code duplication

It appears that the code in the onPlayerMove() and onComputerMove() has some redundancy. The redundant code could be abstracted into a separate method like below:

updateBoardByAmount: function(amount) {
    this.board -= amount;
    if (this.board <= 0) {
        $("#board span").text("Game over");
    } else {
        $("#board span").text(this.board);
    }
}

Then that method can be called when necessary:

onPlayerMove: function(lifecycle, amount) {
    this.updateBoardByAmount(amount);
},
onComputerMove: function() {
    return $.ajax({
        type: "POST",
        url: "/",
        dataType: 'json',
        data: {pos: this.board },
    }).done(this.updateboardByAmount.bind(this));
}

Notice .bind() was used to bind the callback of the AJAX call to the method (though ensure Browser compatibility doesn't become an issue) - this means the variable self is no longer needed and thus removed.

DOM lookups

While it may be negligible for a simple page like this, for pages with more complex DOM structures it would be beneficial to cache DOM lookups. For example, we can add a variable before the state machine is instantiated:

var messageContainer;

Then in the jQuery DOM loaded callback, set that to the DOM element accordingly:

$('document').ready(function() { //DOM ready callback
    messageContainer = $("#board span");

And then use that variable when necessary - for example:

messageContainer.text("Game over");

jQuery DOM-loaded callback

This is nothing to worry about, but the format of the jQuery DOM-loaded callback (i.e. $('document).ready(function() {) works but is deprecated1.

The recommended format is simply:

$(function() {

1http://api.jquery.com/ready/

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thanks for the answer! I actually was looking for help with both. I am more familiar with Python the language, but less familiar with hug than I am with jQuery. I am going to hold off on accepting for a little bit to see if there are any other suggestions. Thanks again! \$\endgroup\$
    – Dair
    Commented Nov 8, 2017 at 20:20
  • 1
    \$\begingroup\$ Sure - I feel like one should wait (perhaps 1+ days) so as to allow other users that might potentially provide an answer to do so \$\endgroup\$ Commented Nov 8, 2017 at 21:06

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.