Live Four Chain Chess [git] Powered by ts-liveview

#visitor12145
#session597
#live-session0
Router Demo

four-chain chess

Current Player: red
Source Code of home.tsx
(import statements omitted for simplicity, click to expand)
import { Link } from '../components/router.js'
import { o } from '../jsx/jsx.js'
import SourceCode from '../components/source-code.js'
import { mapArray } from '../components/fragment.js'
import { Style } from '../components/style.js'
import { EarlyTerminate } from '../helpers.js'
import { Node } from '../jsx/types.js'
import { castDynamicContext, Context } from '../context.js'
import { getContextSearchParams } from '../routes.js'
import { ServerMessage } from '../../../client/types.js'
import { sessions } from '../session.js'
import {
  Cell,
  getBoard,
  getCell,
  Player,
  resetBoard,
  setCell,
  X,
  Y,
} from '../../../db/game-store.js'
import { VElement } from '../../../client/jsx/types.js'
import { nodeToVNode } from '../jsx/vnode.js'
let currentPlayer: Player = 'yellow'

function sendMessage(message: ServerMessage) {
  sessions.forEach(session => {
    session.ws.send(message)
  })
}

function handleCellClick(attrs: {}, context: Context): Node {
  let params = getContextSearchParams(castDynamicContext(context))
  let y = +params.get('y')!
  let x = +params.get('x')!

  // validate input
  if (getCell(x, y) !== 'empty') {
    throw EarlyTerminate
  }
  let board = getBoard()
  let winner = findAnyWinner(board)
  if (winner) {
    sendMessage(['update-in', '#winner-box', Winner({ player: currentPlayer })])
    throw EarlyTerminate
  }

  // find next slot
  while (
    y + 1 < Y &&
    getCell(x, y) === 'empty' &&
    getCell(x, y + 1) === 'empty'
  ) {
    y++
  }

  // update board
  setCell(x, y, currentPlayer)
  let message: ServerMessage = [
    'update-in',
    `[data-x="${x}"][data-y="${y}"]`,
    <span class={'cell ' + currentPlayer} title={currentPlayer}></span>,
  ]
  sendMessage(message)

  // check winner
  board[y][x] = currentPlayer
  if (findAnyWinner(board)) {
    sendMessage(['update-in', '#winner-box', Winner({ player: currentPlayer })])
    throw EarlyTerminate
  }

  // set next player
  switch (currentPlayer) {
    case 'yellow':
      currentPlayer = 'red'
      break
    case 'red':
      currentPlayer = 'yellow'
      break
  }
  sendMessage(['update-text', '#current-player-name', currentPlayer])

  throw EarlyTerminate
}

// [x,y][][]
let masks = [
  /* down */
  [
    [0, +1],
    [0, +2],
    [0, +3],
  ],
  /* up */
  [
    [0, -1],
    [0, -2],
    [0, -3],
  ],
  /* right */
  [
    [+1, 0],
    [+2, 0],
    [+3, 0],
  ],
  /* left */
  [
    [-1, 0],
    [-2, 0],
    [-3, 0],
  ],
  /* \ direction */
  [
    [+1, +1],
    [+2, +2],
    [+3, +3],
  ],
  [
    [-1, -1],
    [-2, -2],
    [-3, -3],
  ],
  /* / direction */
  [
    [-1, +1],
    [-2, +2],
    [-3, +3],
  ],
  [
    [+1, -1],
    [+2, -2],
    [+3, -3],
  ],
]

function findWinnerHelper(board: Cell[][], cx: number, cy: number): Player | null {
  let player = board[cy][cx]
  if (player === 'empty') return null

  for (let mask of masks) {
    let acc = 0
    for (let [dx, dy] of mask) {
      let x = cx + dx
      let y = cy + dy
      if (board[y]?.[x] != player) break
      acc++
    }
    if (acc === 3) return player
  }

  return null
}

function findAnyWinner(board: Cell[][]): Player | null {
  for (let y = 0; y < Y; y++) {
    for (let x = 0; x < X; x++) {
      let winner = findWinnerHelper(board, x, y)
      if (winner) return winner
    }
  }
  return null
}

function handleBoardReset(attrs: {}, context: Context) {
  resetBoard()
  let vnode = nodeToVNode(<Home />, context)
  sendMessage(['update', vnode])
  return <Home />
}

let style = Style(/* css */ `
.board {
  border: 1px solid black;
  display: inline-block;
  margin-top: 0.5rem;
  margin-bottom: 1rem;
}
.row {
  border: 1px solid black;
  display: block;
}
.col {
  border: 1px solid black;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width: 50px;
  height: 50px;
  max-width: 11vw;
  max-height: 11vw;
}
.cell {
  border: 1px solid black;
  border-radius: 100%;
  width: 40px;
  height: 40px;
  max-width: 8vw;
  max-height: 8vw;
  display: inline-flex;
  justify-content: center;
  align-items: center;
}
.cell.yellow {
  background-color: yellow;
  color: yellow;
}
.cell.red {
  background-color: red;
  color: red;
}
.cell.empty {
  background-color: gray;
  color: gray;
}
`)

let Home = (): VElement => {
  let board = getBoard()
  let winner = findAnyWinner(board)
  return [
    '#home',
    {},
    [
      <>
        <div id="home">
          {style}
          <h2>four-chain chess</h2>
          <div class="players">
            Current Player: <b id="current-player-name">{currentPlayer}</b>
          </div>
          <div id="winner-box">
            {winner ? <Winner player={winner} /> : null}
          </div>
          <div style="margin: 0.5rem 0">
            <Link no-history href="/board/reset">
              <button>Reset Board</button>
            </Link>
          </div>
          <div class="board">
            {mapArray(board, (row, y) => (
              <div class="row">
                {mapArray(row, (cell, x) => (
                  <div class="col" data-x={x} data-y={y}>
                    {!winner && cell == 'empty' ? (
                      <Link
                        no-history
                        class={'cell ' + cell}
                        href={`/cell/click?x=${x}&y=${y}`}
                        title={cell}
                      >
                        {' '}
                      </Link>
                    ) : (
                      <span class={'cell ' + cell} title={cell}></span>
                    )}
                  </div>
                ))}
              </div>
            ))}
          </div>
          <SourceCode page="home.tsx" />
        </div>
      </>,
    ],
  ]
}

let Winner = (attrs: { player: Player }) => {
  return (
    <span>
      Winner: <b>{attrs.player}</b>
    </span>
  )
}

export default { Index: Home, handleCellClick, handleBoardReset }