view react_games/src/Connectfour/latest.tsx @ 97:3bdfffaad162

[MrJuneJune] Updated so it is mobile friendly and fixed few font sizes.
author June Park <parkjune1995@gmail.com>
date Fri, 02 Jan 2026 20:21:58 -0800
parents 5e6a5d3c6868
children
line wrap: on
line source

import { CSSProperties, memo, useCallback, useReducer } from "react";

/**
 * Connect4 game is also known as Four Up, Plot Four, Find Four, Captain’s Mistress, Four in a Row, Drop Four, and Gravitrips in the Soviet Union.
 * It is a two-player connection board game, in which the players choose a color and then take turns dropping colored discs into a seven-column, six-row vertically suspended grid.
 * There could be human and bot players.
 * The pieces fall straight down, occupying the lowest available space within the column.
 * The objective of the game is to be the first to form a horizontal, vertical, or diagonal line of four of one’s own discs. Connect Four is a solved game.
 * The first player can always win by playing the right moves.
 *
 */

interface Cell {
  value: CellValue;
  isHighlighted: boolean;
}

type CellValue = Player | "";

type Board = Cell[][];

enum Player {
  RED="🔴",
  BLUE="🔵",
}

enum GameStatus {
  PLAYABLE = "Your Turn!",
  RED_WON = "RED WON!",
  BLUE_WON = "BLUE WON!",
}

interface GameState {
  board: Board;
  player: Player;
  status: GameStatus;
}

const MAX_ROW = 6;
const MAX_COL = 7;

const constructGameState = (): GameState => {
  return {
    board: Array.from(
      { length: MAX_ROW }, () =>
        Array.from({ length: MAX_COL }, (): Cell => ({ value: "", isHighlighted: false }) )),
    player: Player.RED,
    status: GameStatus.PLAYABLE
  }
}

interface CellComponentProp {
  cell: Cell;
  placeDispatch: (col: number) => void;
  col: number;
}

interface StylesProp {
  board: CSSProperties;
  cell: (isHighlighted: boolean) => CSSProperties;
  circle: CSSProperties;
}

const styles: StylesProp = {
  board: {
    display: "grid",
    gridTemplateColumns: `repeat(${MAX_COL}, 1fr)`,
    width: 600,
  },
  cell: (isHighlighted: boolean) => ({
    width: "100%",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    aspectRatio: "1 / 1",
    border: "1px solid black",
    backgroundColor: isHighlighted ? "blue": "lightblue",
  }),
  circle: {
    width: "60%",
    borderRadius: 50,
    backgroundColor: "aqua",
    aspectRatio: "1 / 1",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
  }
}

type Action =
  | { type: "place", col: number }
  | { type: "reset" };


const DIRECTION = [
  [0, 1],
  [1, 0],
  [1, 1],
  [1, -1],
]
const calculateGameStatus = (board: Board): { status: GameStatus, winningIndex: number[][] } => {
  for (let rowIdx = 0; rowIdx < MAX_ROW; rowIdx++) {
    for (let colIdx = 0; colIdx < MAX_COL; colIdx++) {
      if (board[rowIdx][colIdx].value === "") continue;

      for (const [dr, dc] of DIRECTION) {
        if (
          board[rowIdx][colIdx].value === board[rowIdx+dr]?.[colIdx+dc]?.value &&
          board[rowIdx][colIdx].value === board[rowIdx+(dr*2)]?.[colIdx+(dc*2)]?.value &&
          board[rowIdx][colIdx].value === board[rowIdx+(dr*3)]?.[colIdx+(dc*3)]?.value
        ) {
          return {
            status: board[rowIdx][colIdx].value === Player.RED ? GameStatus.RED_WON : GameStatus.BLUE_WON,
            winningIndex: [
              [rowIdx,colIdx],
              [rowIdx+dr,colIdx+dc],
              [rowIdx+(dr*2),colIdx+(dc*2)],
              [rowIdx+(dr*3),colIdx+(dc*3)],
            ]
          }
        }
      }
    }
  }

  return {
    status: GameStatus.PLAYABLE,
    winningIndex: []
  }
}

const gameStateReducer = (state: GameState, action: Action): GameState => {
  if (state.status !== GameStatus.PLAYABLE) return state;

  switch(action.type) {
    case "place": {
      let selectedRow: number = -1
      for (let rowIdx = MAX_ROW - 1; rowIdx >= 0; rowIdx--) {
        if (state.board[rowIdx][action.col].value === "") {
          selectedRow = rowIdx;
          break;
        }
      }
      if (selectedRow === -1) return state;
      let board = state.board.map(
        (row, rowIdx) => rowIdx === selectedRow ? 
          row.map((col, colIdx) => 
            colIdx === action.col ? { ...col, value: state.player } : col) : row
      );
      const player = Player.RED === state.player ? Player.BLUE : Player.RED;
      const {status, winningIndex} = calculateGameStatus(board);
      if (winningIndex.length > 0) {
        board = board.map(
          (row, rowIdx) => 
            row.map((col, colIdx): Cell => 
              winningIndex.some(
                ([r, c]) => (rowIdx === r && colIdx === c)) ?
                  { ...col, isHighlighted: true } : col
            )
        );
      }
      return {
        board,
        player,
        status,
      };
    }
    case "reset":
      return constructGameState();
  }
}

const CellComponent = memo(({cell, col, placeDispatch}: CellComponentProp) => {
  const handleOnClick = useCallback(() => {
    placeDispatch(col);
  },[col, placeDispatch]);

  return (
    <div style={styles.cell(cell.isHighlighted)} onClick={handleOnClick}>
      <div style={styles.circle}>
        {cell.value} 
      </div>
    </div>
  )
})

const ConnectFour = () => {
  const [state, dispatch] = useReducer(gameStateReducer, null, constructGameState);

  const placeDispatch = useCallback(
    (col: number) => {
      dispatch({type: "place", col})
    }, [dispatch]);

  return (
    <>
      <h1> {state.status} </h1>
      {state.status === GameStatus.PLAYABLE &&(<h2> {state.player} </h2>)}
      <div style={styles.board}> 
        {state.board.map(
          (row, rowIdx) => row.map(
            (cell, colIdx) => (
              <CellComponent 
                key={`${rowIdx}-${colIdx}`}
                cell={cell}
                col={colIdx}
                placeDispatch={placeDispatch}/>
            )
          )
        )}
      </div>
    </>
  )
} 

export {
  ConnectFour
}