How I Built a Browser Gomoku Game with AI and Multiplayer Support A developer built a browser-based Gomoku (five-in-a-row) game featuring three AI difficulty levels and real online multiplayer. The AI uses pattern-based evaluation rather than full alpha-beta search, scoring moves by connected-stone counts and open ends across four directions and restricting candidates to empty cells within two squares of existing stones. Easy, medium and hard modes share the evaluation function but differ in mistake rate, top-candidate randomization and a one-ply lookahead. Gomoku also known as "five in a row" looks simple on the surface: place stones on a grid, and the first player to line up five in a row wins. But once you try to build it properly, you run into three different problems: This post walks through how I built a browser Gomoku game with three AI difficulties and real online multiplayer. You can try the final result here: The board is a classic 15×15 grid: js export const BOARD SIZE = 15 // 0 = empty, 1 = black, 2 = white export type CellValue = 0 | 1 | 2 export type Board = CellValue Players take turns placing stones. Black moves first. A player wins by forming an unbroken line of five or more stones horizontally, vertically, or diagonally. If the board fills up with no winner, it is a draw. Checking for a win after every move only requires scanning four directions from the last placed stone, which is cheap. The hard part is the AI. A full search over a 15×15 board is computationally hopeless without heavy pruning. Even with alpha-beta pruning, a branching factor of 200+ quickly becomes unmanageable in the browser. Instead, I used a pattern-based evaluation approach. It plays fast, scales to thousands of candidate positions, and — importantly — feels human. For every candidate move, I look in four directions and count how many stones are connected and how many ends are open. That gives a score per direction: js const SCORE = { FIVE: 1 000 000, OPEN FOUR: 100 000, FOUR: 10 000, // "closed four" one end blocked OPEN THREE: 8 000, THREE: 800, OPEN TWO: 500, TWO: 100, ONE: 10, } function directionScore count, openEnds { if count = 5 return SCORE.FIVE if openEnds === 0 return 0 // blocked on both sides switch count { case 4: return openEnds === 2 ? SCORE.OPEN FOUR : SCORE.FOUR case 3: return openEnds === 2 ? SCORE.OPEN THREE : SCORE.THREE case 2: return openEnds === 2 ? SCORE.OPEN TWO : SCORE.TWO case 1: return SCORE.ONE default: return 0 } } A move is then scored by summing its four directions, with extra bonuses for compound patterns like double open threes or four-three combinations , which are near-winning threats. Instead of evaluating all 225 empty cells, the AI only considers empty cells within two squares of an existing stone . On an empty board it simply plays the center. // Only empty cells near existing stones are candidates. // This dramatically reduces the work per move. const candidates = getCandidates board Early in the game this collapses the search from ~225 cells to a few dozen, and it scales gracefully as the board fills up. A single AI is either too hard or too boring. So I split it into three personalities that share the same evaluation function but behave very differently. Before scoring anything, every difficulty follows two non-negotiable rules: js // 1. If I can win right now, win. for const p of candidates { if createsFive board, p.row, p.col, player return p } // 2. If my opponent can win next move, block it. const losingMoves = candidates.filter p = createsFive board, p.row, p.col, opponent // ... handle blocking Easy looks at offense plus a little defense, but deliberately makes mistakes: js const missChance = 0.25 // sometimes fails to block and occasionally picks a random move from a broader pool, so beginners get chances to win. Medium weights offense vs. defense and picks randomly among the top few candidates, which adds variety without being unfair: js const score = evaluatePosition board, p.row, p.col, player + evaluatePosition board, p.row, p.col, opponent 1.1 // pick randomly among the top 3 Hard adds a one-ply lookahead : for each of its top candidates, it simulates placing its stone, then asks "what is the opponent's best single-move threat now?" and penalizes moves that leave the opponent a strong reply: js board cand.p.row cand.p.col = player let oppBest = 0 for const q of candidates { const t = evaluatePosition board, q.row, q.col, opponent if t oppBest oppBest = t } board cand.p.row cand.p.col = 0 const value = cand.ai 1.15 + cand.opp 1.0 - oppBest 0.9 It only does this for the top ~16 candidates to keep the turn fast, but it is enough to avoid blunders and punish open threes. The multiplayer mode was the part I most wanted to keep frictionless: no accounts, no lobbies to sign into, just share a room code. It runs on a WebSocket server h3's defineWebSocketHandler with an in-memory room store. Each room holds the board, whose turn it is, the status, and up to two players: interface Room { roomCode: string players: RoomPlayer board: number currentPlayer: Player gameStatus: 'waiting' | 'playing' | 'gameover' winner: GameResult rematchRequests: Set