24
\$\begingroup\$

In this game two players compete to eat the most points worth of tokens, but there's a twist! Eating multiple tokens in a row of the same color gives an ever-growing bonus, but watch out, or your opponent will thwart your plans by eating the tokens you want before you can!

Rules:

  • 1 versus 1
  • n by n board (random size between 5x5 and 15x15)
  • You and your opponent will spawn in the same random cell
  • Throughout the board will be randomly generated numbers in some cells ranging in value from 1-3
  • 2 * (the width of the board) tokens will be generated, but there can be overrides, so there may be fewer by chance.
  • Each number will be one of 3 colors: Red, Green, or Blue, in hex RGB format
  • Each round, player 1 moves and the board is updated, then player 2 moves, and the board is updated. So each player can effectively tell what move the previous player made based on the change in the board state. This continues until the game ends, as described later.
  • You have 6 possible actions for a turn: UP, RIGHT, DOWN, LEFT, EAT, and PASS
  • The 4 move commands are self-explanatory, and you CAN pass your turn. If you return a nonsensical move, we will assume you meant pass. If you try to move off the edge of the board, you will not move. The edges don't wrap.
  • EAT consumes the number you are currently in the same space as
  • You gain as many points as the number you consume
  • If you eat 2 numbers in a row of the same color, you get +1
  • If you eat 3 numbers in a row of the same color, you get +2
  • If you eat m numbers in a row of the same color, you get +(m-1)
  • These bonuses are added cumulatively, so getting m numbers in a row leads to m*(m-1)/2 in total bonus by the time you eat a different color.
  • Game end conditions:
    • All numbers are consumed
    • 4 * (the width of the board) turns have gone by with no effective eating (just saying “EAT” with no token where you are doesn't count) occurring by either player (any token is reachable in 2 * (the width) moves, so this bound will only be surpassed if both players have no single target token in mind)
  • Your AI should take less than a second to make a move, else PASS will be assumed as your choice.

The tournament will be round robin with a large number of rounds, say 100 or 1000. A random board is generated, and each ordered pair of different players is run on that board. After the tournament completes we will rank people by their total score. So even if you are player 2 for a game, your goal is still to get as many points as possible.

AI Submission: The language my controller supports is Javascript. Multiple submissions are allowed. Everyone submits a constructor for an object like this:

function (player1) {
    this.yourMove = function (b) {
        return "MOVE";
    }
}

The input player1 is a boolean saying whether you are player 1 or not. Your constructor must have the yourMove function, but it can have any number of additional functions or values as well. Don't define any global variables, just put them as variables on your object. A new version of your object will be created at the beginning of each match, and yourMove will be called on it, with the current board as input, on each of your turns, and should return a valid move.

b, the input to yourMove, is a copy of the current board, here are the constructors, with input examples, though you can't call them yourself:

function token(color, points) {
    this.color = color; //"#FF0000"
    this.points = points; //5
}

function player(pos, score, colorBonus, lastColor) {
    this.pos = pos; //[5, 5]
    this.score = score; //9
    this.colorBonus = colorBonus; //i.e. 2 if you just ate 3 blue tokens in a row
                                  //0 if you just ate two different colors.
    this.lastColor = lastColor; //"#00FF00", is "#000000" at start
}

function board(player1, player2, tokens) {
    this.player1 = player1; //new player([5, 5], 9, 2, "#00FF00")
    this.player2 = player2; //new player([5, 5], 9, 2, "#00FF00")
    this.tokens = tokens; //[[new token("#0000FF", 5), false],
                      // [new token("#0000FF", 5), false]]
}

The token array has "false" for any empty squares, and tokens[a][b] is the token at x = a, y = b, numbered starting from the upper-left corner.

Controller: Here's a link to the controller in GitHub. It's an html file that you can run to see how the game and the round-robin works, and it comes with two AIs, a random one that moves in a random direction each turn but eats tokens at its position, and a naive algorithm that goes for the nearest token that gives the most points. I will add in each AI as it is submitted.

Below is a snippet that allows you to run the controller on the default AI. Current AIs:

  • KindaRandomAI
  • NaiveAI
  • MirrorBot
  • HungryBot

<!DOCTYPE html>
<html>

<body>

  <canvas id="myCanvas" width="700" height="600" style="border:1px solid #c3c3c3;">
    Your browser does not support the HTML5 canvas tag.
  </canvas>

  <script>
    // AI Array, put in at least two constructor functions as described in the
     // data file, and it will do round robin on them.
     // Also, you can adjust some variables for testing purposes only.
    var interval = 1; // in milliseconds per turn
    var matches = 10; // number of matches for each pairing
    var aiArray = [];
    myAI = function(player1) {
      this.player1 = player1;
      this.yourMove = function(b) {
        var me;
        if (this.player1) {
          me = b.player1;
        } else {
          me = b.player2;
        }
        var d = 0;
        var tokenP;
        while (tokenP == undefined) {
          var arr = this.findTokensAtDistance(me.pos, d)
          tokenP = this.findBestToken(arr, b.tokens, me);
          d += 1;
        }
        return this.startAndGoalToCommand(me.pos, tokenP);
      }
      this.findTokensAtDistance = function(p, d) {
        if (d == 0) {
          return [
            [p[0], p[1]]
          ];
        }
        var myArr = [];
        for (i = 0; i <= d; i++) {
          myArr[i] = [i, d - i];
        }
        var mySecArr = [];
        for (i = 0; i <= d; i++) {
          mySecArr[i] = [myArr[i][0] + p[0], myArr[i][1] + p[1]];
        }
        mySecArr[mySecArr.length] = [myArr[0][0] + p[0], -myArr[0][1] + p[1]];
        for (i = 1; i < myArr.length - 1; i++) {
          mySecArr[mySecArr.length] = [-myArr[i][0] + p[0], myArr[i][1] + p[1]]
          mySecArr[mySecArr.length] = [myArr[i][0] + p[0], -myArr[i][1] + p[1]]
          mySecArr[mySecArr.length] = [-myArr[i][0] + p[0], -myArr[i][1] + p[1]]
        }
        mySecArr[mySecArr.length] = [-myArr[myArr.length - 1][0] + p[0], myArr[myArr.length - 1][1] + p[1]];
        return mySecArr;
      }
      this.findBestToken = function(arr, t, player) {
        var tokenPos;
        for (i = 0; i < arr.length; i++) {
          if (arr[i][0] >= 0 && arr[i][0] < t.length && arr[i][1] >= 0 && arr[i][1] < t.length) {
            if (t[arr[i][0]][arr[i][1]] != false && ((tokenPos == undefined) || (this.tokenScore(player, t[arr[i][0]][arr[i][1]]) > this.tokenScore(player, t[tokenPos[0]][tokenPos[1]])))) {
              tokenPos = [arr[i][0],
                [arr[i][1]]
              ];
            }
          }
        }
        return tokenPos;
      }
      this.tokenScore = function(player, token) {
        if (player.lastColor == token.color) {
          return player.colorBonus + 1 + token.points;
        } else {
          return token.points;
        }
      }
      this.startAndGoalToCommand = function(start, goal) {
        var diff = [goal[0] - start[0], goal[1] - start[1]];
        if (diff[0] > 0) {
          return "RIGHT";
        } else if (diff[1] > 0) {
          return "DOWN";
        } else if (diff[1] < 0) {
          return "UP";
        } else if (diff[0] < 0) {
          return "LEFT";
        } else {
          return "EAT";
        }
      }
    }
    aiArray[0] = myAI;
     // This is a really stupid AI, used to test that the round-robin selected
     // it is the worst by far
    myStupidAI = function(player1) {
      this.player1 = player1;
      this.yourMove = function(b) {
        var me;
        if (this.player1) {
          me = b.player1;
        } else {
          me = b.player2;
        }
        if (b.tokens[me.pos[0]][me.pos[1]] != false) {
          return "EAT";
        } else {
          var dirs = this.getViableDirections(b, me.pos);
          var rand = Math.floor(Math.random() * dirs.length);
          return dirs[rand];
        }
      }
      this.getViableDirections = function(b, p) {
        var dirs = [];
        if (p[0] > 0) {
          dirs.push("LEFT");
        }
        if (p[1] > 0) {
          dirs.push("UP");
        }
        if (p[1] < b.tokens.length - 1) {
          dirs.push("DOWN");
        }
        if (p[0] < b.tokens.length - 1) {
          dirs.push("RIGHT");
        }
        return dirs;
      }
    }
    aiArray[1] = myStupidAI;
    mirrorBot = function(player1){
        this.hasStarted=0;
        this.player1 = player1;
        this.yourMove = function(b){
            this.op = this.player1 ? b.player2 : b.player1;
            var out = "EAT";
            if(this.hasStarted){
                if(this.opl.pos[0] < this.op.pos[0]) out = "LEFT";
                if(this.opl.pos[0] > this.op.pos[0]) out = "RIGHT";
                if(this.opl.pos[1] < this.op.pos[1]) out = "UP";
                if(this.opl.pos[1] > this.op.pos[1]) out = "DOWN";
            }
            this.opl = this.op;
            this.hasStarted = 1;
            return out;
        }
    }
    aiArray[2] = mirrorBot;
    hungryBot = function(first) {
  // Set up "self"
  var self = this;

  // Determine player order
  this.player = -(first - 2);
  this.enemy = first + 1;

  // Action associative array
  this.actions = ['EAT', 'LEFT', 'RIGHT', 'UP', 'DOWN'];

  //Logic handler
  this.yourMove = function(board) {
// Determine player object
var player = board['player' + self.player];
var enemy = board['player' + self.enemy];

// Point value action grid
var actions = [0, 0, 0, 0, 0]; // Associative with "this.actions"

// Board dimensions
var size = board.tokens.length;
var maxDist = size * 2;

// Colors remaining
var colors = {
  '#FF0000': 0,
  '#00FF00': 0,
  '#0000FF': 0
};

// Averaged value weight
var average = [0, 0];

// Total points
var points = 0;

// Token holder
var tokens = [];

// Token parser
for (var i = 0, x = 0, y = 0; i < size * size; i += 1, x = i % size, y = i / size | 0) {
  if (!board.tokens[x][y]) {
    continue;
  } else {
    var token = {};
    token.points = board.tokens[x][y].points;
    token.color = board.tokens[x][y].color;
    token.x = x - player.pos[0];
    token.y = y - player.pos[1];
    token.distX = Math.abs(token.x);
    token.distY = Math.abs(token.y);
    token.dist = token.distX + token.distY;
    token.distE = Math.abs(x - enemy.pos[0]) + Math.abs(y - enemy.pos[1]);
    token.value = -token.points - (player.colorBonus + 1) * (token.color == player.lastColor) * ((token.dist == 0) + 1) * 1.618 - (enemy.colorBonus + 1) * (token.color == enemy.lastColor);
    tokens.push(token);
    colors[token.color] += 1;
    points += token.points;
    average[0] += x * token.points;
    average[1] += y * token.points;
  }
}

// Determine actual average
average[0] = average[0] / points | 0;
average[1] = average[1] / points | 0;

// Pick best token
var best = 0;

// Calculate point values of tokens
for (i = 0; i < tokens.length; i++) {
  var token = tokens[i];
  // Add remaining numbers of tokens of color as factor
  token.value -= (colors[token.color] / tokens.length) * 1.618;
  // Subtract distance as a factor
  token.value += token.dist;
  // Add distance to average to value
  token.value += (Math.abs(average[0] - (token.x + player.pos[0])) + Math.abs(average[1] - (token.y + player.pos[1]))) / Math.sqrt(2);
  // Consider them higher value if we are closer, and lower if they are
  token.value += ((token.dist - token.distE) / (token.dist + token.distE + 0.001)) * token.dist;
  // Don't go for it if enemy is already there
  token.value += (token.distE == 0 && token.dist > 0) * 100;

  if (tokens[best].value > tokens[i].value || (tokens[best].value === tokens[i].value && Math.round(Math.random()))) {
    best = i;
  }
}

// Set token to best token
var token = tokens[best];

// What to respond with
var response = 'PASS';

// Find best action to get token
if (token.dist == 0) {
  response = 'EAT'; // We're on the token
} else if (token.distX >= token.distY) { // Token is more horizontal
  if (token.x < 0) { // Token is left
    response = 'LEFT';
  } else if (token.x > 0) { // Token is right
    response = 'RIGHT';
  }
} else if (token.distX < token.distY) { // Token is more vertical
  if (token.y < 0) { // Token is above
    response = 'UP';
  } else if (token.y > 0) { // Token is below
    response = 'DOWN';
  }
}

// Return response
return response;
  }
};
    aiArray[3]=hungryBot;
    for (i = 0; i < aiArray.length; i += 1) {
      Object.freeze(aiArray[i]);
    }
     // This is the actual code to run the program. You shouldn't mess with it,
     // or your AI might not work during the actual competition.
     // Also note that you can't access this code at all in your AI.
     // Feel free to make suggestions and we'll try to incorporate them,
     // and let us know if something isn't working correctly.
    Object.freeze(aiArray);
    setInterval((function() {
      function token(color, points) {
        this.color = color;
        this.points = points;
      }

      function player(pos, score, colorBonus, lastColor) {
        this.pos = pos;
        this.score = score;
        this.colorBonus = colorBonus;
        this.lastColor = lastColor;
      }

      function board(player1, player2, tokens) {
        this.player1 = player1;
        this.player2 = player2;
        this.tokens = tokens;
      }

      function copyBoard(b) {
        return new board(copyPlayer(b.player1), copyPlayer(b.player2), copyTokens(b.tokens));
      }

      function copyTokens(t) {
        var tokens = [];
        for (i = 0; i < t.length; i++) {
          tokens[i] = [];
          for (j = 0; j < t[i].length; j++) {
            tokens[i][j] = t[i][j];
          }
        }
        return tokens;
      }

      function copyPlayer(p) {
        return new player([p.pos[0], p.pos[1]], p.score, p.colorBonus, p.lastColor);
      }

      function endGame(b) {
        if (b.player1.score > b.player2.score) {
          var c = document.getElementById("myCanvas");
          var ctx = c.getContext("2d");
          ctx.font = 20 + "px Arial";
          ctx.textAlign = "left";
          ctx.fillStyle = "#000000";
          ctx.fillText("Player 1", 601, 160);
          ctx.fillText("wins!", 601, 180);
        } else if (b.player1.score < b.player2.score) {
          var c = document.getElementById("myCanvas");
          var ctx = c.getContext("2d");
          ctx.font = 20 + "px Arial";
          ctx.textAlign = "left";
          ctx.fillStyle = "#000000";
          ctx.fillText("Player 2", 601, 160);
          ctx.fillText("wins!", 601, 180);
        } else {
          var c = document.getElementById("myCanvas");
          var ctx = c.getContext("2d");
          ctx.font = 20 + "px Arial";
          ctx.textAlign = "left";
          ctx.fillStyle = "#000000";
          ctx.fillText("It's a", 601, 160);
          ctx.fillText("draw!", 601, 180);
        }
      }

      function genTokenArray(size) {
        var temp = [];
        for (i = 0; i < size; i++) {
          temp[i] = [];
          for (j = 0; j < size; j++) {
            temp[i][j] = false;
          }
        }
        return temp;
      }

      function genRandomTokenArray(size) {
        var temp = genTokenArray(size);
        for (i = 0; i < size * 2; i++) {
          var rand = Math.floor(Math.random() * temp.length);
          var points = Math.floor(Math.random() * 3 + 1);
          var rand2 = Math.floor(Math.random() * 3);
          var color;
          var rand3 = Math.floor(Math.random() * temp[rand].length);
          if (rand2 == 0) {
            color = "#FF0000"
          } else if (rand2 == 1) {
            color = "#00FF00"
          } else {
            color = "#0000FF"
          }
          temp[rand][rand3] = new token(color, points);
        }
        return temp;
      }

      function countTokens(tokenArr) {
        var x = 0;
        for (i = 0; i < tokenArr.length; i++) {
          for (j = 0; j < tokenArr[i].length; j++) {
            if (tokenArr[i][j] != false) {
              x += 1
            }
          }
        }
        return x;
      }

      function actPlayer(b, p, s) {
        if (s == "UP" && p.pos[1] > 0) {
          p.pos[1] -= 1;
        } else if (s == "RIGHT" && p.pos[0] < b.tokens.length - 1) {
          p.pos[0] += 1;
        } else if (s == "DOWN" && p.pos[1] < b.tokens.length - 1) {
          p.pos[1] += 1;
        } else if (s == "LEFT" && p.pos[0] > 0) {
          p.pos[0] -= 1;
        } else if (s == "EAT" && b.tokens[p.pos[0]][p.pos[1]] != false) {
          if (p.lastColor == b.tokens[p.pos[0]][p.pos[1]].color) {
            p.colorBonus += 1;
            p.score += p.colorBonus;
          } else {
            p.lastColor = b.tokens[p.pos[0]][p.pos[1]].color;
            p.colorBonus = 0;
          }
          p.score += b.tokens[p.pos[0]][p.pos[1]].points;
          b.tokens[p.pos[0]][p.pos[1]] = false;
        }
      }

      function drawEmptyRect() {
        var c = document.getElementById("myCanvas");
        var ctx = c.getContext("2d");
        ctx.clearRect(0, 0, 700, 600);
      }

      function drawToken(t, x, y, g) {
        drawEntity(x, y, t.points, t.color, "#000000", 300 / g, g);
      }

      function drawLittleEntity(x, y, label, fillColor, labelColor, size, cX, cY, g) {
        var delta = 600 / g;
        var x1 = x * delta + cX * delta / 4;
        var y1 = y * delta + cY * delta / 4;
        var c = document.getElementById("myCanvas");
        var ctx = c.getContext("2d");
        ctx.beginPath();
        ctx.arc(x1, y1, size / 2, 0, 2 * Math.PI);
        ctx.stroke();
        ctx.fillStyle = fillColor;
        ctx.fill();
        ctx.font = delta / 2 + "px Arial";
        ctx.textAlign = "center";
        ctx.fillStyle = labelColor;
        ctx.fillText(label, x1, y1 + delta * 3 / 16);
      }

      function drawEntity(x, y, label, fillColor, labelColor, size, g) {
        var delta = 600 / g;
        var x1 = x * delta + delta / 2;
        var y1 = y * delta + delta / 2;
        var c = document.getElementById("myCanvas");
        var ctx = c.getContext("2d");
        ctx.beginPath();
        ctx.arc(x1, y1, size, 0, 2 * Math.PI);
        ctx.stroke();
        ctx.fillStyle = fillColor;
        ctx.fill();
        ctx.font = delta + "px Arial";
        ctx.textAlign = "center";
        ctx.fillStyle = labelColor;
        ctx.fillText(label, x1, y1 + delta * 3 / 8);
      }

      function drawAllTokens(arrToken) {
        for (i = 0; i < arrToken.length; i++) {
          for (j = 0; j < arrToken[i].length; j++) {
            if (arrToken[i][j] != false) {
              drawToken(arrToken[i][j], i, j, arrToken.length);
            }
          }
        }
      }

      function drawGrid(g) {
        var delta = 600 / g;
        for (i = 0; i <= g; i++) {
          var canvas = document.getElementById("myCanvas");
          var ctx = canvas.getContext("2d");
          ctx.beginPath();
          ctx.moveTo(i * delta, 0);
          ctx.lineTo(i * delta, 600);
          ctx.stroke();
        }
        for (j = 0; j <= g; j++) {
          var canvas = document.getElementById("myCanvas");
          var ctx = canvas.getContext("2d");
          ctx.beginPath();
          ctx.moveTo(0, j * delta);
          ctx.lineTo(600, j * delta);
          ctx.stroke();
        }
      }

      function drawPlayer1(b, g) {
        drawLittleEntity(b.player1.pos[0], b.player1.pos[1], "F", "#000000", "#FFFFFF", 300 / g, 1, 1, g);
      }

      function drawPlayer2(b, g) {
        drawLittleEntity(b.player2.pos[0], b.player2.pos[1], "S", "#000000", "#FFFFFF", 300 / g, 3, 3, g);
      }

      function drawScore(b) {
        var canvas = document.getElementById("myCanvas");
        var ctx = canvas.getContext("2d");
        ctx.font = "20px Arial";
        ctx.fillStyle = "#000000";
        ctx.textAlign = "left";
        ctx.fillText("P" + indices[0] + " Score:", 601, 50);
        ctx.fillText(b.player1.score, 601, 70);
        ctx.fillText("P" + indices[1] + " Score:", 601, 100);
        ctx.fillText(b.player2.score, 601, 120);
      }

      function drawScoreNum(s, i) {
        var canvas = document.getElementById("myCanvas");
        var ctx = canvas.getContext("2d");
        ctx.font = "20px Arial";
        ctx.fillStyle = "#000000";
        ctx.textAlign = "left";
        ctx.fillText("P" + i + " Score:", 601, i * 50 + 50);
        ctx.fillText(s, 601, i * 50 + 70);
      }

      function redraw(b, g) {
        drawEmptyRect();
        drawGrid(g);
        drawScore(b);
        drawAllTokens(b.tokens);
        drawPlayer1(b, g);
        drawPlayer2(b, g);
        drawScoreNum()
      }

      function nextPair(pair, total) {
        var pairRes = [pair[0], (pair[1] + 1) % total];
        if (pairRes[1] == 0) {
          pairRes[0] = pairRes[0] + 1;
        }
        return pairRes;
      }

      function nextGameUnfair() {
        scores[indices[0]] = scores[indices[0]] + currBoard.player1.score;
        scores[indices[1]] = scores[indices[1]] + currBoard.player2.score;
        if (matchCount >= matches) {
          matchCount = 0;
          indices = nextPair(indices, aiArray.length);
          if (indices[0] == indices[1]) {
            indices = nextPair(indices, aiArray.length);
          }
          if (indices[0] >= aiArray.length) {
            return false;
          }
          player1Turn = true;
          timeWithoutEating = 0;
          firstAI = new aiArray[indices[0]](true);
          secondAI = new aiArray[indices[1]](false);
          currBoard = genBoard();
        } else {
          matchCount++;
          player1Turn = true;
          timeWithoutEating = 0;
          firstAI = new aiArray[indices[0]](true);
          secondAI = new aiArray[indices[1]](false);
          currBoard = genBoard();
        }
        return true;
      }

      function nextGameFair() {
        scores[indices[0]] = scores[indices[0]] + currBoard.player1.score;
        scores[indices[1]] = scores[indices[1]] + currBoard.player2.score;
        indices = nextPair(indices, aiArray.length);
        if (indices[0] == indices[1]) {
          indices = nextPair(indices, aiArray.length);
        }
        if (indices[0] >= aiArray.length) {
          indices = [0, 1];
          matchCount++;
          if (matchCount >= matches) {
            return false;
          }
          player1Turn = true;
          timeWithoutEating = 0;
          currBoard = genBoard();
        } else {
          player1Turn = true;
          timeWithoutEating = 0;
          currBoard = savedBoard;
        }
        firstAI = new aiArray[indices[0]](true);
        secondAI = new aiArray[indices[1]](false);
        savedBoard = copyBoard(currBoard);
        return true;
      }

      function genBoard() {
        var rand = Math.floor(Math.random() * 11 + 5);
        var randX = Math.floor(Math.random() * rand);
        var randY = Math.floor(Math.random() * rand);
        return new board(new player([randX, randY], 0, 0, "#000000"),
          new player([randX, randY], 0, 0, "#000000"),
          genRandomTokenArray(rand));
      }
      var running = true;
      var indices = [0, 1];
      var scores = []
      for (i = 0; i < aiArray.length; i++) {
        scores.push(0);
      }
      var matchCount = 0;
      var firstAI = new aiArray[0](true);
      var secondAI = new aiArray[1](false);
      var player1Turn = true;
      var currBoard = genBoard();
      var savedBoard = copyBoard(currBoard);
      var timeWithoutEating = 0;
      return function() {
        if (running) {
          var numTokens = countTokens(currBoard.tokens);
          if (numTokens <= 0) {} else if (player1Turn) {
            actPlayer(currBoard, currBoard.player1, firstAI.yourMove(copyBoard(currBoard)));
          } else {
            actPlayer(currBoard, currBoard.player2, secondAI.yourMove(copyBoard(currBoard)));
          }
          player1Turn = !player1Turn;
          redraw(currBoard, currBoard.tokens.length);
          if (numTokens > countTokens(currBoard.tokens)) {
            timeWithoutEating = 0;
          } else {
            timeWithoutEating += 1;
          }
          if (numTokens <= 0 || timeWithoutEating > 4 * currBoard.tokens.length) {
            running = nextGameFair();
          }
        } else {
          drawEmptyRect();
          for (i = 0; i < scores.length; i++) {
            drawScoreNum(scores[i], i);
          }
        }
      }
    })(), interval);
  </script>

</body>

</html>

\$\endgroup\$
27
  • 12
    \$\begingroup\$ Yay, a KOTH! It has been forever since the last one. \$\endgroup\$ Apr 6, 2016 at 14:51
  • 2
    \$\begingroup\$ Agreed, I love me a good KOTH and this seems to be a great premise. I'm a bit green to js, how does one persist game state between moves if we can't save results within out player object? \$\endgroup\$ Apr 6, 2016 at 15:47
  • \$\begingroup\$ Is the board width passed anywhere into the function? \$\endgroup\$ Apr 6, 2016 at 16:44
  • \$\begingroup\$ @BentNeeHumor Yes, the function that takes in the player1 boolean is the constructor for your AI, which will have a yourMove function that takes the current board as input, as b. \$\endgroup\$ Apr 6, 2016 at 17:57
  • 1
    \$\begingroup\$ @DylanSp Sometimes they aren't allowed due to collusion possibilities, but in this case, collusion would have minimal benefits, so I'll allow multiple submissions. \$\endgroup\$ Apr 6, 2016 at 20:25

8 Answers 8

4
\$\begingroup\$

HungryBot

Uses a point system to add weight to the value of pursuing each token. Uses a variety of different factors in its consideration, and reevaluates them each turn to ensure its following the best strategy.

function hungryBot(first) {
  // Set up "self"
  var self = this;
  
  // Determine player order
  this.player = -(first - 2);
  this.enemy = first + 1;
  
  // Action associative array
  this.actions = ['EAT', 'LEFT', 'RIGHT', 'UP', 'DOWN'];
  
  //Logic handler
  this.yourMove = function(board) {
    // Determine player object
    var player = board['player' + self.player];
    var enemy = board['player' + self.enemy];
    
    // Point value action grid
    var actions = [0, 0, 0, 0, 0]; // Associative with "this.actions"
    
    // Board dimensions
    var size = board.tokens.length;
    var maxDist = size * 2;
    
    // Colors remaining
    var colors = {
      '#FF0000': 0,
      '#00FF00': 0,
      '#0000FF': 0
    };
    
    // Averaged value weight
    var average = [0, 0];
    
    // Total points
    var points = 0;
    
    // Token holder
    var tokens = [];
    
    // Token parser
    for (var i = 0, x = 0, y = 0; i < size * size; i += 1, x = i % size, y = i / size | 0) {
      if (!board.tokens[x][y]) {
        continue;
      } else {
        var token = {};
        token.points = board.tokens[x][y].points;
        token.color = board.tokens[x][y].color;
        token.x = x - player.pos[0];
        token.y = y - player.pos[1];
        token.distX = Math.abs(token.x);
        token.distY = Math.abs(token.y);
        token.dist = token.distX + token.distY;
        token.distE = Math.abs(x - enemy.pos[0]) + Math.abs(y - enemy.pos[1]);
        token.value = -token.points - (player.colorBonus + 1) * (token.color == player.lastColor) * ((token.dist == 0) + 1) * 1.618 - (enemy.colorBonus + 1) * (token.color == enemy.lastColor);
        tokens.push(token);
        colors[token.color] += 1;
        points += token.points;
        average[0] += x * token.points;
        average[1] += y * token.points;
      }
    }
    
    // Determine actual average
    average[0] = average[0] / points | 0;
    average[1] = average[1] / points | 0;
    
    // Pick best token
    var best = 0;
    
    // Calculate point values of tokens
    for (i = 0; i < tokens.length; i++) {
      var token = tokens[i];
      // Add remaining numbers of tokens of color as factor
      token.value -= (colors[token.color] / tokens.length) * 1.618;
      // Subtract distance as a factor
      token.value += token.dist;
      // Add distance to average to value
      token.value += (Math.abs(average[0] - (token.x + player.pos[0])) + Math.abs(average[1] - (token.y + player.pos[1]))) / Math.sqrt(2);
      // Consider them higher value if we are closer, and lower if they are
      token.value += ((token.dist - token.distE) / (token.dist + token.distE + 0.001)) * token.dist;
      // Don't go for it if enemy is already there
      token.value += (token.distE == 0 && token.dist > 0) * 100;
      
      if (tokens[best].value > tokens[i].value || (tokens[best].value === tokens[i].value && Math.round(Math.random()))) {
        best = i;
      }
    }
    
    // Set token to best token
    var token = tokens[best];
    
    // What to respond with
    var response = 'PASS';
    
    // Find best action to get token
    if (token.dist == 0) {
      response = 'EAT'; // We're on the token
    } else if (token.distX >= token.distY) { // Token is more horizontal
      if (token.x < 0) { // Token is left
        response = 'LEFT';
      } else if (token.x > 0) { // Token is right
        response = 'RIGHT';
      }
    } else if (token.distX < token.distY) { // Token is more vertical
      if (token.y < 0) { // Token is above
        response = 'UP';
      } else if (token.y > 0) { // Token is below
        response = 'DOWN';
      }
    }
    
    // Return response
    return response;
  }
};
\$\endgroup\$
4
  • \$\begingroup\$ Are you a Python programmer? \$\endgroup\$ Apr 7, 2016 at 16:37
  • \$\begingroup\$ @CatsAreFluffy Not really...? \$\endgroup\$
    – Mwr247
    Apr 7, 2016 at 16:38
  • \$\begingroup\$ Just thought you were because self :) \$\endgroup\$ Apr 7, 2016 at 16:53
  • \$\begingroup\$ Why use self? Isn't this sufficient? \$\endgroup\$ Apr 8, 2016 at 22:03
2
\$\begingroup\$

PATH bot

Acronym stands for Pathfinding And Tree Heuristics Bot

EDIT: As of now, here are the rankings for the AIs, with the points

  1. HungryBot (6422)
  2. PATH bot (4591)
  3. NaiveAI (3811)
  4. KindaRandomAI (618)
  5. MirrorBot (193)
  6. LazyBot (25)

Link to complete controller on github

Description: Like NaiveAI, this bot finds the nearest token that will give it the most points. However, it also simulates the outcomes of each of its moves, up to 6 times.

Rationale: Because NaiveAI is already pretty good, I though I would make it better. Without looking at the code first (big mistake).

Beats: All except HungryBot
Looses to: None except HungryBot

Problems:

  • Cannot simulate increased streak
  • Hangs while calculating best token
  • Can teleport

I still don't know why it was teleporting, but I fixed it. Old video here: https://youtu.be/BIhSKycF9iA

Full code:

pathBot = function (player1)
{
    this.pathNode = function(pos,ppt,parents,par)
    {
        this.pos = pos;this.ppt = ppt;this.parents = parents;this.par=par;
        this.childs=[];
    }
    this.addChildren = function (pn,children)
    {
        pn.childs=[];
        for(var i=0; i<children.length; i=i+1)
        {
            if(pn.parents.indexOf(children[i].pos)==-1&&pn.pos!=children[i].pos)
                pn.childs.push(
                    new this.pathNode(
                        children[i].pos,
                        children[i].ppt*pn.ppt,
                        pn.parents.concat([pn.pos]),
                        pn
                    )
                );
        }
    }
    this.orderTokensByPPT = function(b,pos){
        var tokens = [];
        for(var y=0; y<b.tokens.length; y=y+1)
        {
            for(var x=0; x<b.tokens[y].length; x=x+1)
            {
                var tok = b.tokens[y][x];
                if(tok)
                {
                    tokens.push(
                        new this.pathNode(
                            [y,x],
                            (tok.points+(tok.color==this.color ? this.streak : 0)) / this.lenOfMovesTo(pos,[y,x]),
                            [],
                            undefined
                        )
                    );
                }
            }
        }
        tokens.sort(function(a,b){
            return b.ppt - a.ppt;
        });
        return tokens;
    }
    this.lenOfMovesTo = function(cur,pos)
    {
        return Math.abs(cur[0]-pos[0])+Math.abs(cur[1]-pos[1])+1;
    }
    this.startAndGoalToCommand = function (start, goal) {
        var diff = [goal[0] - start[0], goal[1] - start[1]];
        if (diff[0] > 0) { return "RIGHT"; }
        else if (diff[1] > 0) { return "DOWN"; }
        else if (diff[1] < 0) { return "UP"; }
        else if (diff[0] < 0) { return "LEFT"; }
        else { return "EAT"; }
    }
    this.color = 0;
    this.streak = 0;
    this.eatTok = function(b)
    {
        if(b.tokens[this.me.pos[0]][this.me.pos[1]].color==this.color)
        {
            this.streak++;
        }
        else{
            this.streak = 0;
            this.color = b.tokens[this.me.pos[0]][this.me.pos[1]].color;
        }
        this.bestToken = false;
        return "EAT";
    }

    this.recurLen = 6;
    this.include = 4;
    this.recurDown = function(b,pn,level)
    {
        if(level==0) return pn;
        this.addChildren(pn,this.orderTokensByPPT(b,pn.pos));
        var newChilds = [];
        for(var i=0; i<pn.childs.length&&i<this.include; i=i+1)
        {
            newChilds.push(this.recurDown(b,pn.childs[i],level-1));
        }
        pn.childs = newChilds;
        return pn;
    }
    this.findMax = function(pn)
    {
        if(pn.childs)
        {
            var maxList = [];
            for(var i=0; i<pn.childs.length; i=i+1)
                maxList.push(this.findMax(pn.childs[i]));
            maxList.sort(
                function(a,b)
                {
                    return b.ppt-a.ppt;
                }
            );
            return maxList[0];
        }
        return pn;
    }
    this.findMaxList = function(pnList)
    {
        for(var i=0; i<pnList.lenght; i=i+1)
        {
            pnList[i] = this.findMax(pnList[i]);
        }
        pnList.sort(function(a,b){return b.ppt-a.ppt;});
        return pnList[0];
    }
    this.bestToken=false;
    this.yourMove = function(b){
        this.op = player1 ? b.player2 : b.player1;
        this.me = player1 ? b.player1 : b.player2;
        if(this.bestToken)
        {
            if(b.tokens[this.bestToken.pos[0]][this.bestToken.pos[1]]==undefined)
                this.bestToken = false;
        }
        if(!this.bestToken)
        {
            var paths = this.orderTokensByPPT(b,this.me.pos);
            for(var i=0; i<paths.length; i++)
            {
                paths[i] = this.recurDown(b,paths[i],this.recurLen);
            }
            var max = this.findMaxList(paths);
            while(max.par)
            {
                max = max.par;
            }
            this.bestToken = max;
        }
        var move = this.startAndGoalToCommand(this.me.pos,this.bestToken.pos);
        if(move=="EAT") return this.eatTok(b);
        else return move;
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ SLaNTbot is slowing the turn speed down, and eating up 15% of my CPU... D: EDIT: And also not eating anything? \$\endgroup\$
    – Mwr247
    Apr 8, 2016 at 22:19
  • \$\begingroup\$ @Mwr247 the speed, yeah, it models ~2500 possibilities every tick. But for the eating thing, I don't exactly know why. Like I said in the question, it just teleports (aka moves multiple spaces in 1 turn), and sits there doing nothing. I put an alert just before the return and it seems to be giving the right instructions every time. \$\endgroup\$
    – Blue
    Apr 8, 2016 at 22:27
  • \$\begingroup\$ Maybe this: "Your AI should take less than a second to make a move, else PASS will be assumed as your choice.". I haven't read the controller, but if it's taking over a second, is it assuming PASS? \$\endgroup\$
    – Mwr247
    Apr 8, 2016 at 22:29
  • \$\begingroup\$ @Mwr247 I'll look into that, but it seems unlikely given that it was taking <1 second (or so I though) on my machine. Still, never hurts to look. Thank you! \$\endgroup\$
    – Blue
    Apr 8, 2016 at 22:32
  • \$\begingroup\$ @Mwr247 After some more testing, that is not it. It is making decisions almost as fast (at least for me) as NaiveAi. Also, you are more likely to experience teleportation on big maps \$\endgroup\$
    – Blue
    Apr 8, 2016 at 22:40
1
\$\begingroup\$

NaiveAI

Starting with r=0, look at all tokens with taxicab distance r away from your position. If there are any, pick one that would give you the highest score if you got it right now. Otherwise, increase r by 1 and try again.

naiveAI = function(player1) {
  this.player1 = player1;
  this.yourMove = function(b) {
    var me;
    if (this.player1) {
      me = b.player1;
    } else {
      me = b.player2;
    }
    var d = 0;
    var tokenP;
    while (tokenP == undefined) {
      var arr = this.findTokensAtDistance(me.pos, d)
      tokenP = this.findBestToken(arr, b.tokens, me);
      d += 1;
    }
    return this.startAndGoalToCommand(me.pos, tokenP);
  }
  this.findTokensAtDistance = function(p, d) {
    if (d == 0) {
      return [
        [p[0], p[1]]
      ];
    }
    var myArr = [];
    for (i = 0; i <= d; i++) {
      myArr[i] = [i, d - i];
    }
    var mySecArr = [];
    for (i = 0; i <= d; i++) {
      mySecArr[i] = [myArr[i][0] + p[0], myArr[i][1] + p[1]];
    }
    mySecArr[mySecArr.length] = [myArr[0][0] + p[0], -myArr[0][1] + p[1]];
    for (i = 1; i < myArr.length - 1; i++) {
      mySecArr[mySecArr.length] = [-myArr[i][0] + p[0], myArr[i][1] + p[1]]
      mySecArr[mySecArr.length] = [myArr[i][0] + p[0], -myArr[i][1] + p[1]]
      mySecArr[mySecArr.length] = [-myArr[i][0] + p[0], -myArr[i][1] + p[1]]
    }
    mySecArr[mySecArr.length] = [-myArr[myArr.length - 1][0] + p[0], myArr[myArr.length - 1][1] + p[1]];
    return mySecArr;
  }
  this.findBestToken = function(arr, t, player) {
    var tokenPos;
    for (i = 0; i < arr.length; i++) {
      if (arr[i][0] >= 0 && arr[i][0] < t.length && arr[i][1] >= 0 && arr[i][1] < t.length) {
        if (t[arr[i][0]][arr[i][1]] != false && ((tokenPos == undefined) || (this.tokenScore(player, t[arr[i][0]][arr[i][1]]) > this.tokenScore(player, t[tokenPos[0]][tokenPos[1]])))) {
          tokenPos = [arr[i][0],
            [arr[i][1]]
          ];
        }
      }
    }
    return tokenPos;
  }
  this.tokenScore = function(player, token) {
    if (player.lastColor == token.color) {
      return player.colorBonus + 1 + token.points;
    } else {
      return token.points;
    }
  }
  this.startAndGoalToCommand = function(start, goal) {
    var diff = [goal[0] - start[0], goal[1] - start[1]];
    if (diff[0] > 0) {
      return "RIGHT";
    } else if (diff[1] > 0) {
      return "DOWN";
    } else if (diff[1] < 0) {
      return "UP";
    } else if (diff[0] < 0) {
      return "LEFT";
    } else {
      return "EAT";
    }
  }
}
\$\endgroup\$
1
\$\begingroup\$

KindaRandomAI

Every turn, do the following: If there is a token at your position, "EAT". Otherwise, move in a random viable direction, i.e. if you're at the left edge don't say "LEFT".

kindaRandomAI = function(player1) {
    this.player1 = player1;
    this.yourMove = function(b) {
        var me;
        if (this.player1) {
            me = b.player1;
        } else {
            me = b.player2;
        }
        if (b.tokens[me.pos[0]][me.pos[1]] != false) {
            return "EAT";
        } else {
            var dirs = this.getViableDirections(b, me.pos);
            var rand = Math.floor(Math.random() * dirs.length);
            return dirs[rand];
        }
    }
    this.getViableDirections = function(b, p) {
        var dirs = [];
        if (p[0] > 0) {
            dirs.push("LEFT");
        }
        if (p[1] > 0) {
            dirs.push("UP");
        }
        if (p[1] < b.tokens.length - 1) {
            dirs.push("DOWN");
        }
        if (p[0] < b.tokens.length - 1) {
            dirs.push("RIGHT");
        }
        return dirs;
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ -1 not completely random \$\endgroup\$ Apr 7, 2016 at 16:53
  • \$\begingroup\$ That's better!. \$\endgroup\$ Apr 7, 2016 at 16:59
1
\$\begingroup\$

LazyBot

Only eats something if he spawns on it. This has no chance to win, but the challenge didn't have one of these, so why not.

lazyBot = function (player1) {
    this.yourMove = function(b) {
        return "EAT";
    }
}
\$\endgroup\$
3
1
\$\begingroup\$

MirrorBot

Should be called "cannon fodder"

Description: Moves the exact opposite of what the other player did

Rationale: I wanted to get comfortable programming in JS again. This should not win

Will beat: No one

Will lose to: Everyone

function mirror(player1) {
    this.hasStarted=false;
    this.player1 = player1;
    this.opl=[0,0];
    this.yourMove = function(b){
        this.op = this.player1 ? b.player2.pos : b.player1.pos;
        out = "EAT";
        console.log(this.op);
        console.log(this.opl);
        if(this.hasStarted){
            if(this.opl[0] < this.op[0]) out = "RIGHT";
            if(this.opl[0] > this.op[0]) out = "LEFT";
            if(this.opl[1] < this.op[1]) out = "UP";
            if(this.opl[1] > this.op[1]) out = "DOWN";
        }
        this.opl = [this.op[0],this.op[1]];
        this.hasStarted = true;
        return out;
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ There are a few problems with your code. Right and Left aren't opposite, and your function definition for yourMove isn't valid syntax. My code was broken before as well, so in the process of finding and fixing the problem in my code I fixed your code as well. You can look at the fixed code in my script. \$\endgroup\$ Apr 7, 2016 at 15:38
  • \$\begingroup\$ @FricativeMelon I fixed the broken function definition. I have to counter the claim that right aren't opposite. \$\endgroup\$
    – Blue
    Apr 7, 2016 at 16:20
  • \$\begingroup\$ 0,0 is the top-left corner, so positive x is right and negative x is left. if the new x-pos is greater in value than the old x-pos, the other player moved right, so you should move left, and vice-versa. Also, you should use var out = "EAT"; instead of out = "EAT";, since the later defines a global variable. Nitpicking a bit, the third and fourth lines don't do anything and can be removed as well, and op can be a local variable like out instead of a property. \$\endgroup\$ Apr 7, 2016 at 16:22
  • \$\begingroup\$ @FricativeMelon ah, I get what you are saying. I have updated the code. Thank you! \$\endgroup\$
    – Blue
    Apr 7, 2016 at 16:32
  • \$\begingroup\$ I put your new code in the script, and it's working now. Doesn't beat RandomAI though :( \$\endgroup\$ Apr 7, 2016 at 16:39
0
\$\begingroup\$

OneTarget

Finds the token that will give the most points in the least time and will go for that one. Ranks same color tokens a little higher because of the cumulative effect.

function (player1) {
    this.yourMove = function (b) {
        var me = player1? b.player1: b.player2;
        var him= player1? b.player2: b.player1;
        var x = me.pos[0];
        var y = me.pos[1];
        var maxVal = -1;
        var maxX = 0;
        var maxY = 0;
        for(var i = 0;i < b.tokens.length;i++){
            for(var j = 0;j < b.tokens.length;j++){
                if(b.tokens[i][j]){
                    var dist = Math.abs(x-i) + Math.abs(y-j);
                    var val = this.valueOf(b.tokens[i][j]);
                    val /= (dist + 1);
                    if(val > maxVal){
                        maxVal = val;
                        maxX = i;
                        maxY = j;
                    }
                }
            }
        }
        if(maxY < y)
            return "UP";
        if(maxX < x)
            return "LEFT";
        if(maxY > y)
            return "DOWN";
        if(maxX > x)
            return "RIGHT";
        return "EAT";
    }
    this.valueOf = function(t){
        //how many points would it give you?
        return t.points + (this.lastColor == t.color? 2 * this.colorBonus + 1 : 0);
    }
}
\$\endgroup\$
0
\$\begingroup\$

QuantityPlayer

All QuantityPlayer cares about is the amout of dots he eats, not the value or color of the dots. He know that even though all dots are different, they should be treated the same.

QuantityBot = function(playernum) {

this.dist = function(token) {
    return (Math.abs(token[0])+Math.abs(token[1]))
}

this.yourMove = function(game_board) {

    board_size = game_board.tokens.length
    board_area = board_size * board_size
    fete = board_size = size * 2

    token_list = []
    count = curr_x = curr_y = 0
    while(count < board_area) {
        if(game_board.tokens[x][y]) {
        token_list.push([x-player.pos[0],y-player.pos[1]])
        }
        count++; x = count % board_size; y = Math.floor(count / size)
    }

    closest_token = token_list[0]
    count = 1
    while(count < token_list.length) {
        curr_token = token_list[count]
        if(dist(curr_token) < dist(closest_token)){closest_token = curr_token}

        count++
    }

    if(dist(closest_token)==0){return 'EAT'}
    else{
    if(closest_token[0] >= closest_token[1]) {if(closest_token[0]<0) {return 'LEFT'} {return 'RIGHT'}}
    else{if(closest_token[1]<0) {return 'UP'} {return 'DOWN'}}
    }

}

}
\$\endgroup\$

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.