Skip to content

Instantly share code, notes, and snippets.

@tkshill
Created March 6, 2023 18:16
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/cf4c3f4827970cd44a00e69b84008db7 to your computer and use it in GitHub Desktop.
Save tkshill/cf4c3f4827970cd44a00e69b84008db7 to your computer and use it in GitHub Desktop.
Minimal version of connect 4 using Generators
// ------------ GAME LOGIC ----------
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.from({length:COLUMNS}, (_) => Array.from({length: ROWS}, () => 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