Fjords Computer Tournament!

Round 0

Standings Summary



Matches


Full Standings


Make-Your-Own Player

(We recommend that you write your code in a separate text editor, then copy-paste it here.)


(This does not submit your player to the tournament. There are further directions below for that.)

Here is an example that finds the first vertex of our player's color with a neighboring empty vertex next to it:

//Don't change the function header (next line) at all.
function userGetMove(playerId, position) {
    const graph = position.getGraph(); //instance of TriangularGridSubgraph
    //search through the vertices of the graph
    for (var columnIndex = 0; columnIndex < graph.getWidth(); columnIndex++) {
        for (var rowIndex = 0; rowIndex < graph.getHeight(); rowIndex++) {
            //get the color of the vertex we're looking at
            const color = graph.getValue(columnIndex, rowIndex);
            if (color == playerId) {
                //the vertex is our color.  Let's go look at the neighbors!
                const neighborsCoordinates = graph.getNeighborCoordinates(columnIndex, rowIndex);
                for (const coordinates of neighborsCoordinates) {
                    const neighborColumn = coordinates[0];
                    const neighborRow = coordinates[1];
                    const neighborColor = graph.getValue(neighborColumn, neighborRow);
                    if (neighborColor == CombinatorialGame.prototype.UNCOLORED) {
                        //the neighboring vertex is uncolored!  Let's paint it our color and return the new position.
                        const optionGraph = graph.clone();
                        optionGraph.setValue(neighborColumn, neighborRow, playerId);
                        const option = new Fjords(0, 0, 0, true); //this starts with an empty underlying graph.  That's fine because we're about to replace it.
                        option.graph = optionGraph;
                        return option;
                    }
                }
            }
        }
    }
    //we (hopefully) shouldn't ever get here because we should always be given a graph we can move on.
    console.log("ERROR: didn't find a move to make!");
}
        

EFAQ: Expected Frequently-Asked Questions

What is Fjords?

Fjords is a partizan combinatorial game played on a subgraph of a triangular grid with some vertices colored Blue or Red. On their turn, the current player (Blue or Red) chooses an uncolored vertex next to one with their color, painting it their color. The last player to move wins. You can play Fjords here.

What is this for?

We are holding a computer player tournament as part of Sprouts 2027. People can use this to test their players. Instructions to submit a player are below.

What if I can't attend Sprouts?

You do not have to be present at Sprouts to enter your player into the tournament. It's often the case that the winning player authors are not present during the tournament.

Can I work on a team, or do I have to work independently?

You can work in a group or on your own.

How big will the boards be in the tournament? How many games will be played per match?

The initial positions should each be on an 8x8 board, but different variations of edges will be removed.

I wrote a brute-force player that always finds the optimal solution. Do I win?

We'll need players to run efficiently. For the actual conference tournament, if we run this with a bunch of contestants at the same time as we're running Zoom, it will get bogged down quickly. (And Kyle's laptop is not very powerful.) Please make sure your player takes their turn in close to 6 seconds or less on starting positions on your own machine. (If your machine isn't too overpowered, that should equate to about 15 seconds on my laptop.) If specific players are running too long, we'll have to exclude them from the tournament. (If you disagree with these rules, feel free to talk to me. We would much rather have more players than fewer!)

What if I want to add more details to my player than just what current move to make? Does it have to be stateless?

Below, you'll see the details for submitting your player class. It does not have to be "stateless"; you can definitely include fields that your player uses to make moves.

I got a player working real well! How do I submit it to your tournament?

Check out the instructions below. (After this EFAQ.)

I actually have two players and I would like to test them out against each other. Can I do that?

Yes, those directions are below!

How do I get updates about the tournament?

Keep watching this space, or watch @CGTKyle@mathstodon.xyz (Mastodon) for updates.

Kyle, this code is awful! Did you write this? I see tons of scripting code in the HTML source and inline styling. What have you done?

Oh yeah. I got it working, but I definitely need to clean it up. Please don't tell my software engineering students! I'll refactor it when I have time (please don't check to see if this answer is the same as it was last year).


Submitting Your Player to the Actual Tournament

If you get a player working as above, you'll need to make a few changes to get it working for the actual Sprouts tournament.

  1. Choose a name for your player. The name should (1) be something no one else will choose, (2) be in PascalCase, and (3) be appropriate to read and speak in an academic setting. We reserve the right to drop players without notice, and vulgar or inappropriate language is part of that. We recommend keeping the name length to 15 characters.
  2. Your class will need to subclass the ComputerPlayer class I've created, so you'll need to add some extra code to wrap your function in. Copy/paste the following code into a text file named <YourPlayerName>.js, with <YourPlayerName> replaced by the name you chose above.
  3. Copy paste the body of your userGetMove function into the function of the same name there. Feel free to add to your player's initialize (constructor) if you need to hold any info between moves.
  4. Modify the name of the class to be your player's name. Modify the body of getName to also return your player's name as a string. Modify the body of getAuthor to return your name or your team's name. Finally, modify the first line to include your team name and a contact email address so we can get in touch with you if necessary.
  5. If you make significant modifications, ensure again that your player makes moves within 6 seconds on the suggested board sizes.
  6. Use our Dropbox to submit your code. By submitting your player to us, you are giving us permission to run the code in a public setting. As mentioned above, we reserve the right to not include your submission in the tournament. Please use this Dropbox link to submit your player. We highly recommend that you email me () when you submit so I can test your code and make sure it's going to run with the system.
  7. In order to be sure to get your player included in the tournament, please submit by 11:59pm (Eastern US Time) on the Thursday before the conference. The link above will continue to work after that time. We do not often receive many submissions and want to include as many as possible, so if you miss the deadline, definitely email me () so we'll be notified of your late submission. If we are able to, we'll include your player!
  8. If you create an even better player, please resubmit your code with the same (file) name. If everything works, we'll use the latest version that we can get into the system.

Testing Two Players

By popular demand, we've included a way to pit two (or more) players against each other.


Fjords code

Here is the underlying JavaScript code for the Fjords class, which uses the prototype package to define objects. It is currently a part of the (very large) combinatorialGames.js file I maintain. For more details, please check out that file.


/**
 * Implements the second phase of Fjords.
 * @author Kyle Burke
 */
const Fjords = Class.create(CombinatorialGame, {

    /**
     * Constructor.
     */
    initialize: function(width, height, numFarms, skipRemovingEdges) {
        //console.log("launched init....");
        //console.log("numFarms: " + numFarms);
        if (numFarms === undefined || numFarms < 0) numFarms = (width + height) / 3;
        if (numFarms * 2 > width * height) {
            numFarms = Math.floor(width * height /2);
        }
        if (skipRemovingEdges === undefined) skipRemovingEdges = false;
        //console.log("numFarms: " + numFarms);
        this.playerNames = ["Blue", "Red"];
        this.graph = new TriangularGridSubgraph(width, height, ((col, row) => CombinatorialGame.prototype.UNCOLORED));
        //console.log("Created the graph...");

        //remove some edges
        /* */
        for (var i = 0; i < 1* height * width && !skipRemovingEdges; i++) {
            //console.log("removing " + (i+1) + "th edge...");
            const randColumn = getRandomInt(0, width);
            const randRow = getRandomInt(0, height);
            const neighbors = this.graph.getNeighborCoordinates(randColumn, randRow);
            if (neighbors.length > 0) {
                const neighborCoords = randomChoice(neighbors);
                const neighborCol = neighborCoords[0];
                const neighborRow = neighborCoords[1];
                this.graph.removeEdge(randColumn, randRow, neighborCol, neighborRow);
                if (!this.graph.isConnected()) {
                    this.graph.addEdge(randColumn, randRow, neighborCol, neighborRow);
                } else {
                    //TODO: these cases shouldn't happen anymore... can we remove them?
                    if (neighbors.length == 1) {
                        //remove the vertex
                        //console.log("removing a vertex: (" + randColumn + ", " + randRow + ")");
                        this.graph.removeVertex(randColumn, randRow);
                        //console.log(this.graph.hasVertex(randColumn, randRow));
                    }
                    //check to see whether we need to remove the neighbor.
                    const neighborNeighborCoords = this.graph.getNeighborCoordinates(neighborCol, neighborRow);
                    if (neighborNeighborCoords.length == 0) {
                        this.graph.removeVertex(neighborCol, neighborRow);
                        //console.log("removing a neighbor vertex: (" + neighborCol + ", " + neighborRow + ")");
                    }
                }
            }
        }
        /* */

        //get the center of the board
        var centerColumn = (width-1) / 2;
        var centerRow = (height-1) / 2;
        if (centerRow % 2 == 0) {
            centerColumn += .5;
        } else if (centerRow % 1 == .5) {
            centerColumn += .25;
        }

        //add some starter colors in random places
        for (var i = 0; i < numFarms; i++) {
            var blueColumn;
            var blueRow;
            var redColumn;
            var redRow;
            while (true) {
                blueColumn = getRandomInt(0, width);
                blueRow = getRandomInt(0, height);
                if ((!this.graph.hasVertex(blueColumn, blueRow)) || this.graph.getValue(blueColumn, blueRow) !== CombinatorialGame.prototype.UNCOLORED) continue; //restart the loop

                var blueColumnDistanceFromEdge = Math.min(blueColumn, width - blueColumn - 1);
                var blueRowDistanceFromEdge = Math.min(blueRow, height - 1 - blueRow);
                const blueTotalDistance = blueColumnDistanceFromEdge + blueRowDistanceFromEdge;

                //try another distance calculation
                var blueColumnShifted = blueColumn;
                if (blueRow % 2 == 0) blueColumnShifted += .5;
                const blueHexRowDistanceToCenter = Math.abs(blueRow - centerRow);
                const blueHexColumnDistanceToCenter = Math.abs(blueColumnShifted - centerColumn);
                const blueHexDistanceToCenter = blueHexRowDistanceToCenter + Math.max(0, blueHexColumnDistanceToCenter - (blueHexRowDistanceToCenter/2));

                redRow = getRandomInt(0, height);
                const redHexRowDistanceToCenter = Math.abs(redRow - centerRow);
                const redColumnPossibilities = [];
                for (var j = 0; j < width; j++) {
                    var redColumnShifted = j;
                    if (redRow % 2 == 0) redColumnShifted += .5;
                    const redHexColumnDistanceToCenter = Math.abs(redColumnShifted - centerColumn);
                    const redHexDistanceToCenter = redHexRowDistanceToCenter + Math.max(0, redHexColumnDistanceToCenter - redHexRowDistanceToCenter / 2);

                    if (redHexDistanceToCenter == blueHexDistanceToCenter) {
                        redColumnPossibilities.push(j);
                    }
                }
                shuffleArray(redColumnPossibilities);
                var foundOne = false;
                for (const redColumnPossibility of redColumnPossibilities) {
                    redColumn = redColumnPossibility;
                    if (this.graph.hasVertex(redColumn, redRow) && this.graph.getValue(redColumn, redRow) === CombinatorialGame.prototype.UNCOLORED && (blueColumn != redColumn || blueRow != redRow)) {
                        foundOne = true;
                        //console.log("blueHexDistanceToCenter: " + blueHexDistanceToCenter);
                        break;
                    }
                }

                if (foundOne) break; //found one that worked!
            }

            //okay to add the blue and red vertices.... they are somewhat fair
            this.graph.setValue(blueColumn, blueRow, CombinatorialGame.prototype.LEFT);
            this.graph.setValue(redColumn, redRow, CombinatorialGame.prototype.RIGHT);
            //console.log("Added vertices: (" + blueColumn + ", " + blueRow + ") and (" + redColumn + ", " + redRow + ")");
        }
    }

    /**
     * Returns the width of this board.
     */
    ,getWidth: function() {
        return this.graph.getWidth();
    }

    /**
     * Returns the height of this board.
     */
    ,getHeight: function() {
        return this.graph.getHeight();
    }

    /**
     * Returns a copy of the graph.
     */
    ,getGraph: function() {
        return this.graph.clone();
    }

    /**
     * Equals!
     */
    ,equals: function(other) {
        return this.graph.equals(other.graph);
    }

    /**
     * Clone.
     */
    ,clone: function() {
        const copy = new Fjords(this.getWidth(), this.getHeight(), 0, true);
        copy.graph = this.graph.clone();
        return copy;
    }

    /**
     * Gets the options.
     */
    ,getOptionsForPlayer: function(playerId) {
        const options = [];
        for (coordinates of this.graph.getVertexCoordinates()) {
            const col = coordinates[0];
            const row = coordinates[1];
            if (this.graph.getValue(col, row) == playerId) {
                const neighbors = this.graph.getNeighborCoordinates(col, row);
                for (const neighbor of neighbors) {
                    const neighborCol = neighbor[0];
                    const neighborRow = neighbor[1];
                    if (this.graph.getValue(neighborCol, neighborRow) == CombinatorialGame.prototype.UNCOLORED) {
                        const option = this.clone();
                        option.graph.setValue(neighborCol, neighborRow, playerId);
                        options.push(option);
                    }
                }
            }
        }
        return options;
    }

    /**
     * Determines whether a player can play at a position.
     */
    ,playerCanPlayAt: function(column, row, playerId) {
        if (this.graph.hasVertex(column, row) && this.graph.getValue(column, row) == CombinatorialGame.prototype.UNCOLORED) {
            const neighborCoordinates = this.graph.getNeighborCoordinates(column, row);
            for (const neighbor of neighborCoordinates) {
                const neighborColumn = neighbor[0];
                const neighborRow = neighbor[1];
                if (this.graph.getValue(neighborColumn, neighborRow) == playerId) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Gets a single option, if it exists.
     */
    ,getOption: function(column, row, playerId) {
        if (this.playerCanPlayAt(column, row, playerId)) {
            const option = this.clone();
            option.graph.setValue(column, row, playerId);
            return option;
        }
        console.log("Fjords.getOption() tried to create an illegal option!");
    }

    /**
     * Returns the value at a specific vertex.
     */
    ,getColorAt: function(column, row) {
        return this.graph.getValue(column, row);
    }

}); //end of Fjords