Skip to content

Instantly share code, notes, and snippets.

@tkshill
Created October 20, 2022 14:25
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 tkshill/a93d88266505531f4126ee089d753fd2 to your computer and use it in GitHub Desktop.
Save tkshill/a93d88266505531f4126ee089d753fd2 to your computer and use it in GitHub Desktop.
connect four game logic implementation using js generators
// ------------ GAME LOGIC ----------
/*
You can go Here to run it
https://www.typescriptlang.org/play?target=99&jsx=0&pretty=true&useUnknownInCatchVariables=true&ssl=1&ssc=1&pln=161&pc=54#code/PTAEFpK7NBxAggWQKKgDIHk4EkDCEM0AUMQC4CeADgKagAKANgIYU0BOoAvKAEROsOmAHY1eoAD58BbdgBUA7gHte5anSSsARjQA8cgHzdQcyaGEBXRozW1QAISXN2AE2OaKO3TI4GA2gC6gaSUdggAxmQAlgBuNADKZMxkdDwA3qBkFuzCAFwMLLIANKAA5swAtjSOzi75Na6gAL62dHhKFVSMNCmJyamgGQpRwqLs+R5ePuwGJeVVDXUOTo0toXR9KcYR0XGbdFLtnd29SSkh6gxKAM5R0UrCxn6WFTrsJS9vAa2gR1pKT3oNzuUQeJSBt3uwnBwKhMMhoOE31I4Qe1zIv0w6AAqkgAHLxYwAdmIqOE6NAACVMAB1Qk8ABspFJaIxIxBzEYi3qKzcXGIoEFoD8ADoxQh2OxWAAKPBY3EEgCUARFFWYVGlAH1uEYJVKKNLqXTFSKAGZRazSyzWRWKlGs0Ds6Kc-b5HaxBJnVICoUZLI5fL8QpCUS8OaVaq8-JOqKcxbNZkgUCcxigKiwxHXR2PZigGKcqJuVGvAEVCwUgAWzDioFRjAsFXJoB0ZAUNBojwADMnhG4GT23OwlAosy22x3QN3mL3QABWFnktnXABqBZcEJBD2M0r8dYb0NAQ4UAXyG6hip1PsFe8boF0mJx+MJADJn7WlPXbwYeN3X4fh6A36TqAf5HneVK0vEzJkhS6YIg8chKECKTCM6jB-Dc267h++4lEeJ5XPBwgXlwBhXqAO7kUKwrYZ+B74SUtG4f+CgQKAACMASMTe9EAeAoAAExccKPF4XxoAAMwBMJ1GybJSZxOw0ThJyVFCn4TGNmJx7cTht78ex2nCZpjz8QJRm6XRbESUZMlyfZSYVko7BRAAXg8SSpuxamChpokscZPFsYZAWWfubHmaFIl6Y8ADUHG2SU9kOWATkue5qGcoJoA+TR-kMdFVkGRZhXhfFkUFSZoDxSF+F2cl1GOc5bkeVlEm5X5MUlVV8U2VFPWCd1QU1YlDVyU16WtamAAsHUmUNMXBdpwWBYtZnLWZq1FZJG2SdJSWgEmLixqUDxZd5yWdXRC3bbV4mcWF+mDSxEVbWVCUvTV+1CkdJ1namAlzflw5vU9d2sQZoNxc9YHlVD1UfbDHHfYKv3MKdwhtUDXX9cNO2fXtj3QxVAFw0TCPgxTKOHWAx3o-9oCzZd8244tI0vZD5PlbtQlc-jYH8VJdlJuwNCKdcdB0xj53Y9drNWezAvI3zJMQ4J8PFQTnHC2Aovi5Lf2YwDsvMZVeOU5zpVParCO81bpmI6TyM64eYscBLoBSwz7XM8DOn29Zy29RrMNO3bVWa0j2sHSLbvsB7XtG4zpDUQEyZZhhgR2slZoWik7DStK6YoWhGEkUYxcdqXHT-CKccGlEK5rmeiK2tBDpkqIkQAGJKNkximhYwiRIiABUFEXmkKc-WAMZZfMdDov05EnKAS9bDwc+MPs0+CgoFYWnQ0pkOwFg0JPu-yWAZAVnQDyMBQybhOENDXFm6YjBiSimqAfecLwCB6A4F4CKUAFAog0EYC4LMN86ALzXl6AcZQpRaBgbfWsTkJaPCCqaIcFRMjoNEAADwxF0QQ7BQG5RghicImCOztDovkT4HBjDgMgW4deNBL6NTAHiAEohyi7Ffh8JQN8RilGbFEUopQWE32nAQug-xagiM9jQcIUQ1SMGuConiaDkjJlFsmRgotmAuEfoPawIpcpRB-tKWhNx6GLXvN2CQUh7FYIYeFICcpHwEkkFIAAhHiBsbwRSNxwKhGgMiC7uMcXRC8riEH9BFAvRYu46HCE8Y2IIRpCSQ2zmNd8qERhnwANzcJnqAc0vYsxgW-lUqI8cMQ0E6JQWskDGCgJpHQWhaiADWNA3CmmclUqwqZdFGJMWYteAIxFZmGNYIxChWC1J6NkHMeY1zZhcDQIhVCHTELIAgfMFpmBaG6JSAC-JClJJSCkiMaTYmZJiiqapLgIk7KIXYjpq56ypCMAEl+1hflnztLlJMFgqAuH6AosoDzeS5U4fchYvJ0kOOeXRIIhzjnMFOecmglzjzcFytRJF-phAVOvA6RuNItzXMKUmGRGIUxplEVXWMM12mLOGDmSU04ZFVFQtcElQo4KbmEIhZC7LOQYWuDuJ5WSDzYpOSwfFhKAgFJuajMAaoqBVLwVy8ZShnLHUxikGBAJ8x-OFVqoUqp1R2JrgCUi74Sz2o1PKnG+Fy63JoMiyMtQTJBG9ZqrVSZenhD6VUkZ+jujMApA8HpNcRjJERAQ-RVY0E9OyKLVCaZgycBGEY1MdwsxAq0SKuSIprgdBoI6ksOpXW13rt84FnIz6NvLSC1IXAeBkvWW3cFYAI1RpsbCsgSg9VgUbqMxZ+zFyOmuN3MZxKbl6lYGaPB0p10Gh8QqeIJoBkUDlW3W1go66KQNEFF1SLUmop4jkyCK0wXJSTGO2BosADkWZcw8t-pwWBzZeSLtnYwEo6JJ2wpkWMZIMaZyi3JbCnlYxrG2JpVuRJjdl02kra7RDQwRhjGjNcWljwAD8vqRSIaYWM8MKLaj5FvfC2ozR06-A6F0Honp+jlKHRg-ldBAOIcRV6Kj6zV2FP7TkbgvbpAFpEGIUAFGgzkMUCoUAgZpgKdUNRFoLRSBJiIDAKkKA8QABEUCUhwHiOAhAjPgGZKvHieIIxZh4DuzdHRt18t3fKJ8h6aDHulIO4g4aHiKQxGBCd746I-pnKaRgUQ9UtOjZwGg8aIGcDVMIJLVhU1bmGZwY61wyEUAXBSE+04SsOO3GqE+UQiE8lqIqJrjRSLkQ87grzO7DSQQC0F099kRR63dnW0N1F3XSiPB83Zja6suSIZN69RgeJ+Gm72XZGrFS8fK5F4ciFEicB4FN4cExtB6GmP4DVl5qK8CkDpoU8Ujy5SWx0ztb3e08BU7IbTSm+BEPEPkctMmvvTDU+IZT6nAyal4ONu1AArJQIxpTiHu6G+KqPVAGevswAZP7MhSnJHBQZQGWPTjcOSrMdxMgAlzLcYQpRugIJcgznK1DSeuAOyfbclXyTcmWM1m7QpefXEWHxqLAJ0Qs4kcMG+L1mGcHJ+YGgChEuiByjnXVhcjwlELEQn1AADAAJGkXJbE9fBSaIKA3CMjxc4LkeC8GOAA6whYd8dMW4f4ZAJ34ILb-H+QUFc2qG2SFSZAUe3YRs51zIpEfI-EIKWHcPtWgDVAMte4imdS-ES9+PwgUfu-tAu9e5Z7fbmuPkfY5dyKfvJZ+7Ma9coUYN+S-IJvrhiZyE0A3uVGMimQxwXDLe4ARlAJgRSXTCMcHb2kTvg-2A99w4GUfVRx+T9AGZqUCgRRY92768vx2F6bHLFXr0NfqIG6Ua4XIrvXcm+vy4e3x9CfVYltKY-Zxyz+sWLaJod-hAO8v9rhn9P9khyxFQl9sc7N7NQAkAEBrMGBqQ4BKRkAMBMBMB6AYCiB24F14EeBO41EyBe5shgtEwwAmVswORUxOEA84UqhiBV5aCeAF4RRDkyDSB95D4KIAkkUXBE0L5yIYIPw-VGAlBShpROFn8kUrVQVQ12cRgqALAMQeAeV+Cd8qA8EqAI8pClBDtJDRNZDz4X0hQx1pQAlFDlCLwtATE+leNSVEEWCIw2DdkI9glXgOARQqBnAJYIkI9LCyA259MzC+CBCika1ugRQxCJDdD9CZD21jCdsgA
*/
type Player = "PlayerOne" | "PlayerTwo"
type Maybe<T> = T | null
type Board = Maybe<Player>[][]
type ActiveState = { turn: Player, gameBoard: Board }
type CompleteState = { winner: Maybe<Player>, gameBoard: Board }
type State = ActiveState | CompleteState
type Position = [number, number]
type Combo = [Position, Position, Position, Position]
const COLUMNS = 7
const ROWS = 6
const initialBoard: Board =
[...Array(COLUMNS)].map(_ => Array(ROWS).fill(null))
const initialState: ActiveState =
{ turn: "PlayerOne", gameBoard: initialBoard }
// all positions in a valid combo must have columns between 0 and 6 and rows between 0 and 5
const isValidPosition = ([column, row]: Position) =>
column < COLUMNS && column >= 0 && row >= 0 && row < ROWS
const positionToPotentialCombos = ([column, row]: Position) =>
([
[[column, row], [column, row - 1], [column, row - 2], [column, row - 3]], // vertical
[[column, row], [column - 1, row], [column - 2, row], [column - 3, row]], // horizontal 1
[[column, row], [column - 1, row], [column - 2, row], [column + 1, row]], // horizontal 2
[[column, row], [column - 1, row], [column + 2, row], [column + 1, row]], // horizontal 3
[[column, row], [column + 3, row], [column + 2, row], [column + 1, row]], // horizontal 4
[[column, row], [column - 1, row - 1], [column - 2, row - 2], [column - 3, row - 3]], // diagonal 1
[[column, row], [column - 1, row - 1], [column - 2, row - 2], [column + 1, row + 1]], // diagonal 2
[[column, row], [column - 1, row - 1], [column + 2, row + 2], [column + 1, row + 1]], // diagonal 3
[[column, row], [column + 3, row + 3], [column + 2, row + 2], [column + 1, row + 1]], // diagonal 4
[[column, row], [column + 1, row - 1], [column + 2, row - 2], [column + 3, row - 3]], // reverse diagonal 1
[[column, row], [column + 1, row - 1], [column + 2, row - 2], [column - 1, row + 1]], // reverse diagonal 2
[[column, row], [column + 1, row - 1], [column - 2, row + 2], [column - 1, row + 1]], // reverse diagonal 3
[[column, row], [column - 3, row + 3], [column - 2, row + 2], [column - 1, row + 1]], // reverse diagonal 4
] as Combo[])
.filter((potentialCombo) => potentialCombo.every(isValidPosition))
const connectFour = function* () {
// initial game state
let state = initialState
while (true) {
// the only access point of our "API". yields the game state and grabs the chosen column from the next player.
const chosenColumn: number = yield state
// No negatives, nothing bigger than the board, no decimals, no columns that are already full.
if (chosenColumn < 0 || chosenColumn >= COLUMNS || !Number.isInteger(chosenColumn) || state.gameBoard[chosenColumn][ROWS - 1])
continue;
// finds row of first empty cell. We checked for full columns already so this will always return a valid index
const nextAvailableRow =
state.gameBoard[chosenColumn].findIndex(cellValue => !cellValue)
// update the gameBoard
state.gameBoard[chosenColumn][nextAvailableRow] =
state.turn
const isWon =
// get all potential 4 cell win arrangements
positionToPotentialCombos([chosenColumn, nextAvailableRow])
// map from cell coordinates to values
.map(combo => combo.map(([column, row]) => state.gameBoard[column][row]))
// check for at least one combination that has the current player in all its cells
.some(combo => combo.every(cellValue => cellValue === state.turn))
// check if the top row is full
const isFull =
Array.from(Array(COLUMNS).keys())
.every(column => state.gameBoard[column][ROWS - 1])
// if there's a win or the board is full, stop the generator and return the winner
if (isWon || isFull)
return { winner: isWon ? state.turn : null, gameBoard: state.gameBoard } as CompleteState;
// change the turn
state.turn =
state.turn === "PlayerOne" ? "PlayerTwo" : "PlayerOne"
}
}
// ----------- RENDERING ----------
let columnNames = Array.from(Array(COLUMNS).keys())
// convert row to columns and flip em for easier manipulation for display
const transpose = (matrix: Board): Board =>
Array.from(Array(ROWS).keys())
.reverse()
.map(rowIndex => matrix.map(column => column[rowIndex]));
const rowToStr = (row: Maybe<Player>[]) =>
"| "
+ row
.map(cell => cell === "PlayerOne" ? "x" : cell === "PlayerTwo" ? "o" : "_")
.join(" | ")
+ " |"
// takes a transposed board and turns it to a single string
const boardToStr = (transBoard: Board) =>
transBoard
// row to string with row number and newline
.map((row, idx) => `${ROWS - idx - 1} ` + rowToStr(row) + "\n")
// add bottom layer of column numbers
.concat(" " + columnNames.join(" "))
// make single string
.join("")
const statusToStr = (s: State) =>
'turn' in s
? `turn: ${s.turn}`
: s.winner
? `Game Over. Winner: ${s.winner}`
: "Game Over. Draw."
const stateToStr = (gameStatus: State) =>
`board:\n\n${boardToStr(transpose(gameStatus.gameBoard))}\n\n${statusToStr(gameStatus)}`
// ----------- MAIN PROGRAM LOOP ----------
const game = connectFour()
// get initial state of game
let state = game.next()
while (!state.done) {
console.log(stateToStr(state.value))
const input = window.prompt(stateToStr(state.value))
if (!input) break;
state = game.next(Number.parseInt(input))
}
if (state.done) console.log(stateToStr(state.value));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment