11
\$\begingroup\$

Basically the tron light-cycle game as a king of the hill competition!

Picture of an online version of the tron light-cycle video game

Leaderboard

Updated as of 2022-04-05

#1) Player Avoid by Agent Biscutt

#2) Dodge West by Imanton1

#3) Space Finder by Spitemaster

#4) Blind Bot by TheSilican

Judge/Leaderboard Website

The Game

Players will write bots that compete in a light-cycle tournament. A round of light-cycle starts with bots evenly spaced out along the edge of a rectangular play area:

+--------------------+
|    @          @    |
|                    |
|                    |
|                    |
|                    |
|                    |
|                    |
|    @          @    |
+--------------------+

Each turn, each bot simultaneously must choose one of 4 directions to step (N, E, S, or W). The "head" of the bot steps in that direction, and the space that was once occupied by the head becomes part of the "body".

+--------------------+
|    #         @#    |
|    @               |
|                    |
|                    |
|                    |
|                    |
|               @    |
|   @#          #    |
+--------------------+

The round ends as soon as one or more bots crash into a wall, or into the head or body of another bot (including its own body).

         *CRASH*
+-----------@--------+
|    #      #####    |
|    ###             |
|      #             |
|   @  @             |
|   #                |
|   #            @#  |
|   #           ###  |
|   ##          #    |
+--------------------+

The bot(s) that crashed into the wall will be eliminated, and a new round will start with new starting positions. Bots are eliminated each round, until only 1 bot remains.

The leaderboard rankings are based on the order that the bots are eliminated.

Code / API

For ease of judging and creating the judge website, submissions will be restricted to javascript.

A bot is a javascript function with the following API:

(state) => {
  // state is an object with access to information about the game
  // bots can use the state to make decisions
  // bots return one of "N", "E", "S", "W"
  // e.g.:
  return "N";
}

The state parameter contains the following API calls:

  • state.getDim(): Returns a [w, h] tuple of the width and height of the arena. e.g. [20, 10]
  • state.getSelf(): Returns a [x, y] tuple of the current bot's head. e.g. [3, 7]
  • state.get(x, y): Given integers x and y, returns a string corresponding to what is currently on the map at (x, y)
    • "#" - body of a bot, or out-of bounds of the map ("wall")
    • "@" - head of a bot
    • " " - empty cell
  • state.getBoard(): Returns a 2d array of the entire map. state.getBoard()[x][y] is equivalent to state.get(x, y)

Coordinates: (0, 0) is the bottom-left corner of the playfield with x increasing to the right and y upwards.

Code Submission

To make a bot submission, add your bot code to this GitHub repository with a pull request, in addition to posting an answer.

Edit: Opening a PR in GitHub is no longer necessary, simply post your submission here.

Each user may only make one submission, however you may freely edit your submission after posting.

\$\endgroup\$
10
  • 4
    \$\begingroup\$ The challenge itself is cool, but why the requirement for a Github PR? \$\endgroup\$
    – Etheryte
    Sep 18, 2021 at 19:16
  • \$\begingroup\$ I thought it would be nice if the code submission/judging process was more transparent \$\endgroup\$
    – thesilican
    Sep 18, 2021 at 21:42
  • 2
    \$\begingroup\$ Perhaps I don't see what you mean, what's more transparent about Github? You can see submission histories on the site here as well. \$\endgroup\$
    – Etheryte
    Sep 18, 2021 at 21:44
  • 1
    \$\begingroup\$ Additionally, it would be nice if you provided an API call which returned the whole map. Would that be possible? \$\endgroup\$
    – ophact
    Sep 19, 2021 at 8:17
  • 1
    \$\begingroup\$ relevant article about optimal strategy \$\endgroup\$
    – agtoever
    Sep 19, 2021 at 16:50

4 Answers 4

4
\$\begingroup\$

Space Finder

(state) => {
  const [x, y] = state.getSelf();
  let starts = [[x, y + 1, "N"], [x + 1, y, "E"], [x, y - 1, "S"], [x - 1, y, "W"]];
  let toSearch = starts.slice();
  toSearch = toSearch.filter(d => state.get(d[0], d[1]) == " ");
  let searched = [];
  let adjacent = [[0, 1], [1, 0], [0, -1], [-1, 0]];
  let dirs = {
    N: toSearch.find(s => s[2] == "N") ? 1 : 0,
    E: toSearch.find(s => s[2] == "E") ? 1 : 0,
    S: toSearch.find(s => s[2] == "S") ? 1 : 0,
    W: toSearch.find(s => s[2] == "W") ? 1 : 0
  };
  let active = {...dirs};
  // Determine the size of the spaces adjacent to each of the possible moves
  while (toSearch.length){
    active[toSearch[0][2]]--;
    adjacent.forEach(a => {
      if (!toSearch[0]) return;
      let loc = [toSearch[0][0] + a[0], toSearch[0][1] + a[1], toSearch[0][2]];
      let tile = state.get(loc[0], loc[1]);
      if (tile == " "){
        let prev = searched.find(s => s[0] == loc[0] && s[1] == loc[1]);
        if (prev){
          if (prev[2] == toSearch[0][2]){
            return;
          }
          dirs[toSearch[0][2]] = prev[2];
          searched.forEach(s => s[2] = s[2] == toSearch[0][2] ? prev[2] : s[2]);
          toSearch = toSearch.filter(d => d[2] != toSearch[0][2]);
          if (Object.values(dirs).filter(v => !isNaN(+v)).length == 2){
            toSearch = [null];
            return;
          }
          return;
        }
        searched.push(loc);
        toSearch.push(loc);
        dirs[loc[2]]++;
        active[loc[2]]++;
      } else if (tile == "@"){
        dirs[loc[2]] /= 2;
      }
    });
    toSearch.shift();
    if ((active.N == 0) + (active.E == 0) + (active.S == 0) + (active.W == 0) == 3){
      let key = Object.keys(active).find(k => active[k]);
      if (dirs[key] > Math.max(...Object.entries(dirs).filter(v => v[0] != key && !isNaN(+v[1])).map(v => v[1]))) break;
    }
  }
  // Pick the move with the largest space
  for (let key of Object.keys(dirs)){
    if (isNaN(+dirs[key])) dirs[key] = dirs[dirs[key]];
    if (isNaN(+dirs[key])) dirs[key] = dirs[dirs[key]];
  }
  let best = Math.max(...Object.values(dirs));
  let possible = Object.keys(dirs).filter(d => dirs[d] == best);
  // Pick the move with the fewest things adjacent to it
  if (possible.length > 1){
    let scores = {" ": 0, "#": 0, "@": 1};
    possible = possible.map(p => {
      let dir = starts.find(s => s[2] == p);
      return [p, adjacent.reduce((a, c) => a + scores[state.get(c[0] + dir[0], c[1] + dir[1])], 0)];
    });
    possible = possible.filter(p => p[1] == Math.min(...possible.map(p => p[1]))).map(p => p[0]);
  }
  return possible[0];
}

Space Finder checks each adjacent space to determine which is attached to the largest empty area. If it has multiple choices, it avoids other heads.

\$\endgroup\$
3
\$\begingroup\$

Dodge West

(state) => {
        const [x, y] = state.getSelf();

        if (state.get(x - 1, y) === " " && !(state.get(x - 2, y + 0) === "@" || state.get(x - 1, y + 1) === "@" || state.get(x - 1, y - 1) === "@"))
            return "W";
        if (state.get(x, y + 1) === " " && !(state.get(x + 0, y + 2) === "@" || state.get(x + 1, y + 1) === "@" || state.get(x - 1, y + 1) === "@"))
            return "N";
        if (state.get(x, y - 1) === " " && !(state.get(x + 0, y - 2) === "@" || state.get(x + 1, y - 1) === "@" || state.get(x - 1, y - 1) === "@"))
            return "S";
        if (state.get(x + 1, y) === " " && !(state.get(x + 2, y + 0) === "@" || state.get(x + 1, y + 1) === "@" || state.get(x + 1, y - 1) === "@"))
            return "E";

        if (state.get(x - 1, y) === " ")
            return "W";
        if (state.get(x, y + 1) === " ")
            return "N";
        if (state.get(x, y - 1) === " ")
            return "S";
        if (state.get(x + 1, y) === " ")
            return "E";

        return "N";
    }

Much like 'Blind Bot', but tries to avoid head-on collisions by turning if another bot could collide with it. Tries to go West and hold on that side of the screen.

\$\endgroup\$
2
  • \$\begingroup\$ Leaderboard updated; you might want to remove the console.log from your code \$\endgroup\$
    – thesilican
    Oct 19, 2021 at 14:29
  • \$\begingroup\$ @thesilican Oops, didn't even notice I left that in. Sorry for the console spam. \$\endgroup\$ Oct 19, 2021 at 16:54
1
\$\begingroup\$

Blind Bot

Bot that simply checks each of the 4 adjacent directions, and goes in the first direction that is empty. Just to get the ball rolling for now, I may or may not edit this submission in the future.

(state) => {
    const [x, y] = state.getSelf();
    if (state.get(x, y + 1) === " ") {
      return "N";
    } else if (state.get(x + 1, y) === " ") {
      return "E";
    } else if (state.get(x, y - 1) === " ") {
      return "S";
    } else if (state.get(x - 1, y) === " ") {
      return "W";
    }
    return "N";
  }
\$\endgroup\$
0
\$\begingroup\$

Player Avoid

This bott simply tries to avoid collision with other bots. Also, runs east instead of north as default, just to spice things up.

(state) => {
    const [x, y] = state.getSelf();
    if (state.get(x + 1, y) === " " &&
  (state.get(x + 1, y+1) ===" " || state.get(x + 1, y-1) ===" ")
  && (state.get(x+1, y + 1) != "@" && state.get(x+1, y-1)!=  "@")
  && ((state.get(x-1, y + 1) != "@" && state.get(x-1, y-1)!=  "@") || state.get(x+2, y)=="#")
  && ((state.get(x+2, y + 2) != "@" && state.get(x+2, y-2)!=  "@"))
  && ((state.get(x-2, y + 2) != "@" && state.get(x-2, y-2)!=  "@"))
  && ((state.get(x+2, y + 2) != "@" && state.get(x+2, y-1)!=  "@"))
  && ((state.get(x-2, y + 2) != "@" && state.get(x-2, y-1)!=  "@"))
        && state.get(x+2, y)!="@"
       ) {
      return "E";
    } else if (state.get(x - 1, y) === " " 
  && (state.get(x - 1, y+1) ===" " || state.get(x - 1, y-1) ===" ")
  && (state.get(x-1, y + 1) != "@" && state.get(x-1, y-1)!=  "@")
  && ((state.get(x+1, y + 1) != "@" && state.get(x+1, y-1)!=  "@") || state.get(x+2, y)=="#")
  && ((state.get(x+2, y + 2) != "@" && state.get(x+2, y-2)!=  "@"))
  && ((state.get(x-2, y + 2) != "@" && state.get(x-2, y-2)!=  "@"))
  && ((state.get(x+2, y + 2) != "@" && state.get(x+2, y-1)!=  "@"))
  && ((state.get(x-2, y + 2) != "@" && state.get(x-2, y-1)!=  "@"))
 
               && state.get(x-2, y)!="@"
              ) {
      return "W";
    }else if (state.get(x, y + 1) === " " 
  && (state.get(x+1, y + 1) != "@" && state.get(x-1, y + 1)!=  "@")
  && state.get(x, y+2)!="@"
             ) {
  
                return "N";
    } else  if (state.get(x, y - 1) === " " 
  && (state.get(x+1, y - 1) != "@" && state.get(x-1, y - 1)!=  "@")
  && state.get(x, y+2)!="@"
               ) {
               return "S";
    }
    
    if (state.get(x, y + 1) === " " && state.get(x, y + 2)!="@") {
      return "N";
    } else if (state.get(x + 1, y) === " " && state.get(x+2, y)!="@") {
      return "E";
    } else if (state.get(x, y - 1) === " " && state.get(x, y - 2)!="@") {
      return "S";
    } else if (state.get(x - 1, y) === " " && state.get(x-2, y)!="@") {
      return "W";
    }
    if (state.get(x, y + 1) === " ") {
      return "N";
    } else if (state.get(x + 1, y) === " ") {
      return "E";
    } else if (state.get(x, y - 1) === " ") {
      return "S";
    } else if (state.get(x - 1, y) === " ") {
      return "W";
    }
    return "S";
  }
\$\endgroup\$
3
  • \$\begingroup\$ Your code doesn't seem to be properly formatted (it should just be a single es6 arrow function), could you check if you copied it over properly? \$\endgroup\$
    – thesilican
    Apr 5, 2022 at 1:03
  • \$\begingroup\$ @thesilican woops, I accidentally copied a second bot with it. Sorry \$\endgroup\$ Apr 5, 2022 at 2:04
  • \$\begingroup\$ Leaderboard updated! \$\endgroup\$
    – thesilican
    Apr 5, 2022 at 6:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.