(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:
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.
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.
You can work in a group or on your own.
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.
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!)
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.
Check out the instructions below. (After this EFAQ.)
Yes, those directions are below!
Keep watching this space, or watch @CGTKyle@mathstodon.xyz (Mastodon) for updates.
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).
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.
By popular demand, we've included a way to pit two (or more) players against each other.
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"];