Skip to content

Instantly share code, notes, and snippets.

@nojvek
Created March 17, 2021 00:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nojvek/9bd116136358a3f7e4a461f8588b92f1 to your computer and use it in GitHub Desktop.
Save nojvek/9bd116136358a3f7e4a461f8588b92f1 to your computer and use it in GitHub Desktop.
Tron Coding Challenge
enum GameState {
Undecided = 'undecided',
Draw = 'draw',
Player1Win = 'player1win',
Player2Win = 'player2win',
}
enum PlayerMark {
Blank = 0,
Player1 = 1,
Player2 = 2,
}
enum PlayerMove {
Up = 'u',
Down = 'd',
Left = 'l',
Right = 'r',
}
interface PlayerPos {
x: number;
y: number;
}
type Grid = PlayerMark[][];
const NUM_ROWS = 10;
const NUM_COLS = 10;
function printGrid(grid: Grid) {
console.log('-'.repeat(20));
for (const row of grid) {
console.log(row.join(`|`));
}
console.log('-'.repeat(20));
}
function makePlayerMove(
grid: Grid,
playerMark: PlayerMark,
playerPos: PlayerPos,
playerMove: PlayerMove,
): GameState {
// move position
if (playerMove === PlayerMove.Down) {
playerPos.y += 1;
} else if (playerMove === PlayerMove.Up) {
playerPos.y -= 1;
} else if (playerMove === PlayerMove.Left) {
playerPos.x -= 1;
} else if (playerMove === PlayerMove.Right) {
playerPos.x += 1;
} else {
throw new Error(`Invalid move:'${playerMove}'`);
}
// validate position is in board
const isValidPos =
playerPos.x >= 0 &&
playerPos.x < NUM_COLS &&
playerPos.y >= 0 &&
playerPos.y < NUM_ROWS;
if (!isValidPos) {
return playerMark === PlayerMark.Player1
? GameState.Player2Win
: GameState.Player1Win;
}
// valdate new position is blank
if (grid[playerPos.y][playerPos.x] !== PlayerMark.Blank) {
return playerMark === PlayerMark.Player1
? GameState.Player2Win
: GameState.Player1Win;
}
// mark position
grid[playerPos.y][playerPos.x] = playerMark;
return GameState.Undecided;
}
function computeTronGameState(
player1Moves: PlayerMove[],
player2Moves: PlayerMove[],
): GameState {
const grid = new Array(NUM_ROWS)
.fill(null)
.map((row) => new Array(NUM_COLS).fill(PlayerMark.Blank));
const p1Pos: PlayerPos = {x: 0, y: 0};
const p2Pos: PlayerPos = {x: NUM_COLS - 1, y: NUM_ROWS - 1};
// add initial player marks
grid[p1Pos.y][p1Pos.x] = PlayerMark.Player1;
grid[p2Pos.y][p2Pos.x] = PlayerMark.Player2;
// ensure player moves are same length
if (player1Moves.length !== player2Moves.length) {
throw new Error(`player1Moves.length !== player2Moves.length`);
}
// make moves and end game if draw or any player wins
for (let i = 0; i < player1Moves.length; ++i) {
const gameStateAfterP1 = makePlayerMove(
grid,
PlayerMark.Player1,
p1Pos,
player1Moves[i],
);
const gameStateAfterP2 = makePlayerMove(
grid,
PlayerMark.Player2,
p2Pos,
player2Moves[i],
);
if (gameStateAfterP1 !== gameStateAfterP2) {
if (
gameStateAfterP1 === GameState.Player2Win &&
gameStateAfterP2 === GameState.Player1Win
) {
printGrid(grid);
return GameState.Draw;
} else if (
gameStateAfterP1 === GameState.Player2Win &&
gameStateAfterP2 !== GameState.Player1Win
) {
printGrid(grid);
return GameState.Player2Win;
} else if (
gameStateAfterP1 !== GameState.Player2Win &&
gameStateAfterP2 === GameState.Player1Win
) {
printGrid(grid);
return GameState.Player1Win;
}
}
}
return GameState.Draw;
}
interface TestCase {
name: string;
input: {
player1: string[];
player2: string[];
};
output?: GameState;
error?: string;
}
const testCases: TestCase[] = [
{
name: `error: moves aren't consistent`,
input: {
player1: ['l', 'r'],
player2: ['u'],
},
error: `player1Moves.length !== player2Moves.length`,
},
{
name: `error: invalid move character`,
input: {
player1: ['a'],
player2: ['u'],
},
error: `Invalid move:'a'`,
},
{
name: 'player 2 should win (player 1 moved off board)',
// prettier-ignore
input: {
player1: ['l'],
player2: ['u'],
},
output: GameState.Player2Win,
},
{
name: 'player 1 should win (player 2 moved off board)',
// prettier-ignore
input: {
player1: ['r'],
player2: ['d'],
},
output: GameState.Player1Win,
},
{
name: 'draw (both moved off board)',
// prettier-ignore
input: {
player1: ['l'],
player2: ['d'],
},
output: GameState.Draw,
},
{
name: 'player 2 should win (player 1 runs into his/her own path)',
// prettier-ignore
input: {
player1: ['r', 'd', 'd', 'r', 'r', 'r', 'l', 'l', 'l', 'd', 'd', 'd', 'l', 'd', 'd', 'd', 'd', 'r'],
player2: ['u', 'l', 'l', 'u', 'l', 'l', 'u', 'l', 'l', 'd', 'd', 'l', 'l', 'u', 'u', 'r', 'u', 'l'],
},
output: GameState.Player2Win,
},
{
name: 'draw (both eliminated on same turn)',
// prettier-ignore
input: {
player1: ['d', 'd', 'r', 'r', 'r', 'u', 'r', 'd', 'd', 'd', 'd', 'l', 'd', 'r', 'r', 'r', 'u', 'u'],
player2: ['l', 'l', 'l', 'u', 'u', 'l', 'u', 'u', 'u', 'r', 'r', 'u', 'l', 'l', 'l', 'l', 'u', 'r'],
},
output: GameState.Draw,
},
{
name: 'draw (both alive)',
// prettier-ignore
input: {
player1: ['d', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'r', 'r', 'r', 'r', 'r', 'u', 'u', 'u', 'u', 'u'],
player2: ['u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'l', 'l', 'l', 'l', 'l', 'd', 'd', 'd', 'd', 'd'],
},
output: GameState.Draw,
},
{
name: 'draw (same space on same turn)',
// prettier-ignore
input: {
player1: ['d', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'r', 'r', 'r', 'r', 'r', 'u', 'u', 'u', 'u', 'u'],
player2: ['u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'l', 'l', 'l', 'l', 'l', 'd', 'd', 'd', 'r', 'd'],
},
output: GameState.Draw,
},
{
name: 'draw (eliminated on 5th round)',
// prettier-ignore
input: {
player1: ['d', 'd', 'd', 'd', 'u', 'u', 'r', 'd', 'd', 'd', 'd', 'l', 'd', 'r', 'r', 'r', 'u', 'u'],
player2: ['l', 'l', 'l', 'l', 'r', 'l', 'u', 'u', 'u', 'r', 'r', 'u', 'l', 'l', 'l', 'l', 'u', 'r'],
},
output: GameState.Draw,
},
{
name: 'player 2 should win',
// prettier-ignore
input: {
player1: ['r', 'd', 'd', 'r', 'r', 'r', 'd', 'r', 'r', 'd', 'd', 'd', 'l', 'd', 'd', 'd', 'd', 'r'],
player2: ['u', 'l', 'l', 'u', 'l', 'l', 'u', 'l', 'l', 'd', 'd', 'l', 'l', 'u', 'u', 'r', 'u', 'l'],
},
output: GameState.Player2Win,
},
{
name: 'player 1 should win',
// prettier-ignore
input: {
player1: ['r', 'd', 'd', 'r', 'r', 'r', 'd', 'r', 'r', 'd', 'd', 'd', 'r', 'u', 'u', 'u', 'd', 'r'],
player2: ['u', 'l', 'l', 'u', 'l', 'l', 'u', 'u', 'u', 'u', 'u', 'l', 'l', 'u', 'u', 'r', 'u', 'l'],
},
output: GameState.Player1Win,
},
{
name: 'player 2 should win (player 1 goes out of bounds)',
// prettier-ignore
input: {
player1: ['r', 'd', 'r', 'r', 'u', 'r', 'u', 'u', 'u', 'd', 'd', 'd', 'r', 'u', 'u', 'u', 'd', 'r'],
player2: ['u', 'l', 'l', 'u', 'l', 'l', 'u', 'l', 'l', 'd', 'd', 'l', 'l', 'u', 'u', 'r', 'u', 'l'],
},
output: GameState.Player2Win,
},
];
/**
* Simple harness for table test runner
*/
export function runTests() {
console.log(`Running Tests ....`);
const filteredTestCases = testCases; //.slice(3, 4);
let numFailedTests = 0;
for (let i = 0; i < filteredTestCases.length; ++i) {
const {
name,
input,
output: expectedOutput,
error: expectedErrMsg = ``,
} = filteredTestCases[i];
let isSuccess = false;
try {
const actualOutput = computeTronGameState(input.player1, input.player2);
isSuccess = expectedOutput === actualOutput;
console.log(
i + 1,
isSuccess ? `✅` : `❌`,
name,
`expected: ${expectedOutput}`,
`actual: ${actualOutput}`,
);
} catch (actualErr) {
isSuccess = actualErr.message === expectedErrMsg;
console.log(
i + 1,
isSuccess ? `✅` : `❌`,
name,
`expectedErr: '${expectedErrMsg}'`,
`actualErr: '${actualErr.message}'`,
);
}
if (!isSuccess) {
numFailedTests += 1;
}
}
console.log(
`Done running tests. ${numFailedTests}/${filteredTestCases.length} Failed`,
);
}
// ------ main -------
runTests();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment