Battle Sheep 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 pile with sheep that can move and moves one of them in the first direction it sees:


EFAQ: Expected Frequently-Asked Questions

What is Battle Sheep?

Battle Sheep is a partizan combinatorial game played with stack of blue and red sheep tokens on a hexagonal board. On their turn, the current player (Blue or Red) chooses one of their stacks with at least two tokens on it. They then choose to move in any available direction, as far as they can move as possible, and move as many of their tokens to that spot as they want, leaving at least one token behind. The last player to move wins. You can play Battle Sheep here.

What is this for?

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

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?

We are currently testing boards with rows, each with a maximum of dominoes per row. I am planning games per match. Please feel free to offer any feedback you like! When this has been finalized, we'll make that clear here.

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 less than 6 seconds on our 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.
  7. In order to be sure to get your player included in the tournament, please submit by 11:59pm (Eastern US Time) Thursday, April ??, 2026. (We'll update the ?? to be the date of the Thursday before the conference once we know exactly when that'll be.) The link above will continue to work after that time but if you miss the deadline, please reach out to me via email () so we'll be notified of your late submission. If we are able to, we'll include your player! (Most years so far we are low on players and would be excited to get more.)
  8. If you create an even better player, please resubmit your code with the same (file) name. If this system works, we'll use the latest version provided before the deadline above.

Testing Two Players

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


Battle Sheep code

Here is the underlying JavaScript code for the Battle Sheep class, which uses the prototype package to define objects. It is currently a part of the (very large) combinatorialGames.js file I maintain.


/**
 * Battle Sheep
 * 
 * Grid is stored as a 2D array of [onBoard, numSheep, sheepColor] pairs.
 * - onBoard is whether the space exists on the board.
 * - numSheep is the number of sheep on that space
 * - sheepColor is the color of the sheep on that space (LEFT, RIGHT, BattleSheep.prototype.UNCOLORED)
 * odd rows are considered to be shifted one half space to the right.
 * @author Kyle Burke
 */
const BattleSheep = Class.create(CombinatorialGame, {

    /**
     * Constructor.
     */
    initialize: function(height, width, board) {
        this.playerNames = ["Blue", "Red"];
        if (board === undefined) {
            this.board = [];
            for (var i = 0; i < width; i++) {
                const column = [];
                for (var j = 0; j < height; j++) {
                    column.push([true, 0, BattleSheep.prototype.UNCOLORED]);
                }
                this.board.push(column);
            }
            const lowDegreeVertices = this.getVerticesWithDegreeAtMost(2); //we're going to delete these
            for (const location of lowDegreeVertices) {
                const column = location[0];
                const row = location[1];
                this.board[column][row][0] = false; //make it not exist
            }
        } else {
            this.board = omniClone(board);
        }
        const blueSheep = this.getSheepOfColor(CombinatorialGame.prototype.LEFT);
        const redSheep = this.getSheepOfColor(CombinatorialGame.prototype.RIGHT);
        const totalSheep = blueSheep + redSheep;
        if (totalSheep == 0) {
            //this is a new board, so let's kill some spaces and add some sheep.
            var numSpaces = this.getNumSpaces();
            var deletedSpaces = 0;
            while (deletedSpaces * 10 + 1 < numSpaces) {
                var randomSpace = randomChoice(randomChoice(this.board));
                while (!randomSpace[0]) {
                    randomSpace = randomChoice(randomChoice(this.board));
                }
                randomSpace[0] = false;
                if (this.hasVertexWithDegreeAtMost(2)) {
                    randomSpace[0] = true;
                }
                deletedSpaces ++; //do this even if we don't delete anything so we get some variety?
            }
            //now go through and delete any spaces with fewer than 3 neighbors
            
            
            numSpaces = this.getNumSpaces(); //update this
            const border = this.getBorderSpaces();
            const blueSpace = randomChoice(border);
            var redSpace = randomChoice(border);
            while (omniEquals(blueSpace, redSpace)) {
                redSpace = randomChoice(border);
            }
            this.board[blueSpace[0]][blueSpace[1]][1] = Number(Math.ceil(numSpaces/2));
            this.board[blueSpace[0]][blueSpace[1]][2] = CombinatorialGame.prototype.LEFT;
            this.board[redSpace[0]][redSpace[1]][1] = Number(Math.ceil(numSpaces/2));
            this.board[redSpace[0]][redSpace[1]][2] = CombinatorialGame.prototype.RIGHT;
        }
    }
    
    /**
     * Returns whether two are equal.
     */
    ,equals: function(other) {
        return omniEquals(this.board, other.board);
    }
    
    /**
     * Returns a clone of this.
     */
    ,clone: function() {
        return new BattleSheep(this.board[0].length, this.board.length, this.board);
    }
    
    /**
     * Returns whether a space exists.
     */
    ,spaceExists: function(col, row) {
        return col >= 0 && col < this.board.length && row >=0 && row < this.board[col].length && this.board[col][row][0];
    }
    
    /**
     * Returns whether a space has no sheep
     */
    ,spaceClear: function(col, row) {
        return this.spaceExists(col, row) && this.board[col][row][1] == 0;
    }
    
    /**
     * Gets the options from a location.
     */
    ,getMoveLocationsFrom: function(col, row) {
        const locations = [];
        if (this.board[col][row][1] > 1) {
            //there are at least two sheep 
            const existingNeighbors = this.getClearNeighborIndicesOf(col, row);
            for (const neighbor of existingNeighbors) {
                var here = [col, row];
                var next = neighbor;
                while (this.spaceClear(next[0], next[1])) {
                    const source = here;
                    here = next;
                    next = this.nextInLine(source, here);
                }
                locations.push(here);
            }
        }
        return locations;
    }
    
    /**
     * Gets the coordinates of the next space in line, whether or not it exists.
     */
    ,nextInLine: function(one, two) {
        const oneNeighbors = this.getNeighborIndicesOf(one[0], one[1]);
        const i = omniIndexOf(oneNeighbors, two);
        const twoNeighbors = this.getNeighborIndicesOf(two[0], two[1]);
        return twoNeighbors[i]; //at the same index 
    }
    
    /**
     * Returns an option of this game.  If the option is not legal, it returns null;
     */
    ,getOptionAt: function(startCol, startRow, endCol, endRow, numSheep) {
        const endLocations = this.getMoveLocationsFrom(startCol, startRow);
        if (arrayContains(endLocations, [endCol, endRow])) {
            const startSheep = this.board[startCol][startRow][1];
            const color = this.board[startCol][startRow][2];
            if (startSheep > numSheep) {
                const option = this.clone();
                option.board[startCol][startRow][1] -= numSheep;
                option.board[endCol][endRow][1] = numSheep;
                option.board[endCol][endRow][2] = color;
                //console.log("Sending option:");
                //console.log(option);
                return option;
            } else {
                console.log("There aren't enough sheep at the start to move " + numSheep + " sheep.  Starting sheep: " + startSheep);
                return null;
            }
        } else {
            console.log("Chosen option doesn't end at a legal location.\nendLocations: [" + endLocations + ",   endCol, endRow: " + endCol + ", " + endRow);
            return null;
        }
    }
    
    /**
     * Returns options for a player.
     */
    ,getOptionsForPlayer: function(playerId) {
        const options = [];
        for (var col = 0; col < this.board.length; col++) {
            for (var row = 0; row < this.board[col].length; row++) {
                if (this.board[col][row][2] == playerId) {
                    //there is sheep of the right color at col, row 
                    const maxSheep = this.board[col][row][1] - 1;
                    const destinations = this.getMoveLocationsFrom(col, row);
                    for (const destination of destinations) {
                        for (var sheep = 1; sheep <= maxSheep; sheep++) {
                            options.push(this.getOptionAt(col, row, destination[0], destination[1], sheep));
                        }
                    }
                }
            }
        }
        return options;
    }
    
    /**
     * Returns the number of sheep of a given color.
     */
    ,getSheepOfColor: function(color) {
        var sheep = 0;
        for (var i = 0; i < this.board.length; i++) {
            for (var j = 0; j < this.board[i].length; j++) {
                if (this.board[i][j][2] == color) {
                    sheep += this.board[i][j][1];
                }
            }
        }
        return sheep;
    }
    
    /**
     * Returns the number of spaces on the board.
     */
    ,getNumSpaces: function() {
        var numSpaces = 0;
        for (var i = 0; i < this.board.length; i++) {
            for (var j = 0; j < this.board[i].length; j++) {
                if (this.board[i][j][0]) {
                    numSpaces += 1;
                }
            }
        }
        return numSpaces;
    }
    
    /**
     * Returns locations of all vertices with a given max degree.
     */
    ,getVerticesWithDegreeAtMost: function(maxDegree) {
        const lowDegreeVertices = [];
        for (var i = 0; i < this.board.length; i++) {
            for (var j = 0; j < this.board[i].length; j++) {
                const boardSpace = this.board[i][j];
                if (boardSpace[0]) {
                    //the space exists
                    const neighbors = this.getExistingNeighborIndicesOf(i, j);
                    if (neighbors.length <= maxDegree){
                        lowDegreeVertices.push([i, j]);
                    }
                }
            }
        }
        return lowDegreeVertices;
    }
        
    
    /**
     * Returns whether any space has degree less than the given degree.
     */
    ,hasVertexWithDegreeAtMost: function(maxDegree) {
        return this.getVerticesWithDegreeAtMost(maxDegree).length > 0;
    }
    
    /**
     * Returns the indices of neighbors, but doesn't check whether they exist
     */
    ,getNeighborIndicesOf: function(col, row) {
        if (row % 2 == 0) {
            return [[col-1, row-1], [col, row-1], [col+1, row], [col, row+1], [col-1, row+1], [col-1, row]];
        } else {
            return [[col, row-1], [col+1, row-1], [col+1, row], [col+1, row+1], [col, row+1], [col-1, row]];
        }
    }
    
    /**
     * Returns the existing neighbors of a space, starting above and to the left.
     */
    ,getExistingNeighborIndicesOf: function(col, row) {
        const neighbors = [];
        const possibleNeighbors = this.getNeighborIndicesOf(col, row);
        for (var i = 0; i < possibleNeighbors.length; i++) {
            const possible = possibleNeighbors[i];
            if (this.spaceExists(possible[0], possible[1])) {
                neighbors.push(possible);
            }
        }
        return neighbors;
    }
    
    /**
     * Returns the neighbors of a space that exist and are clear, starting above and to the left.
     */
    ,getClearNeighborIndicesOf: function(col, row) {
        const neighbors = [];
        const possibleNeighbors = this.getNeighborIndicesOf(col, row);
        for (var i = 0; i < possibleNeighbors.length; i++) {
            const possible = possibleNeighbors[i];
            if (this.spaceClear(possible[0], possible[1])) {
                neighbors.push(possible);
            }
        }
        return neighbors;
    }
    
    /**
     * Returns the number of spaces on the board.
     */
    ,getBorderSpaces: function() {
        var border = [];
        //look along the left for the first playable space
        for (var col = 0; col < this.board.length; col++) {
            if (border.length > 0) break;
            for (var row = 0; row < this.board[col].length; row++) {
                if (this.board[col][row][0]) {
                    border = [[col, row]];
                    break;
                }
            }
        }
        
        const first = border[0];
        //find the second one
        var second = -1;
        const firstNeighborIndices = this.getNeighborIndicesOf(first[0], first[1]);  
        for (const neighbor of firstNeighborIndices) {
            if (this.spaceExists(neighbor[0], neighbor[1])) {
                second = neighbor;
                break;
            }
        }
        if (second == -1) {
            console.log("Issue!  Board only has one space!");
        }
        //now go through and find the rest of them
        var next = second;
        while (!omniEquals(next, first)) {
            //add the next one
            border.push(next);
            
            //get the neighbors of the one you just pushed 
            const nextNeighbors = this.getNeighborIndicesOf(next[0], next[1]);
            //get the index of the one prior
            const nIndex = omniIndexOf(nextNeighbors, border[border.length-2]);
            for (var i = (nIndex + 1) % nextNeighbors.length; i !=nIndex; i = (i + 1) % nextNeighbors.length) {
                const neighbor = nextNeighbors[i];
                
                if (this.spaceExists(neighbor[0], neighbor[1])) {
                    next = neighbor;
                    break;
                }
            }
        }
        //console.log("border size: " + border.length);
        return border;
    }
    
    
}); //end of the BattleSheep class
BattleSheep.prototype.PLAYER_NAMES = ["Blue", "Red"];