Created
October 20, 2022 14:25
-
-
Save tkshill/a93d88266505531f4126ee089d753fd2 to your computer and use it in GitHub Desktop.
connect four game logic implementation using js generators
This file contains hidden or 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
// ------------ 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