Created
March 17, 2021 00:41
-
-
Save nojvek/9bd116136358a3f7e4a461f8588b92f1 to your computer and use it in GitHub Desktop.
Tron Coding Challenge
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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