diff --git a/src/game/Game.ts b/src/game/Game.ts new file mode 100644 index 0000000..84c0979 --- /dev/null +++ b/src/game/Game.ts @@ -0,0 +1,198 @@ +/** + * @author aevgarik@gmail.com + * @date 2026-03-22 + * + * Главный класс игры ColorLine98 + */ + +import type { Board, BallColor } from './Board' +import { + createEmptyBoard, + getRandomColor, + getEmptyCells, + INITIAL_BALLS, + BALLS_PER_TURN, +} from './Board' +import { findPath } from './PathFinder' +import { checkLines } from './LineChecker' +import type { LineInfo } from './LineChecker' + +const SCORE_TABLE: Record = { + 5: 10, + 6: 12, + 7: 18, + 8: 28, +} + +function getLineScore(length: number): number { + if (SCORE_TABLE[length] !== undefined) { + return SCORE_TABLE[length] + } + if (length >= 9) { + return (length - 8) * 14 + 42 + } + return 0 +} + +export interface GameState { + board: Board + score: number + nextBalls: BallColor[] + selectedCell: { row: number; col: number } | null + gameOver: boolean +} + +export type GameListener = (state: GameState) => void + +export class Game { + private state: GameState + private listeners: GameListener[] = [] + + constructor() { + this.state = { + board: createEmptyBoard(), + score: 0, + nextBalls: [], + selectedCell: null, + gameOver: false, + } + } + + getState(): GameState { + return { ...this.state } + } + + subscribe(listener: GameListener): () => void { + this.listeners.push(listener) + return () => { + this.listeners = this.listeners.filter((l) => l !== listener) + } + } + + private notify(): void { + const state = this.getState() + this.listeners.forEach((l) => l(state)) + } + + startNewGame(): void { + this.state = { + board: createEmptyBoard(), + score: 0, + nextBalls: [], + selectedCell: null, + gameOver: false, + } + + this.placeRandomBalls(INITIAL_BALLS) + this.generateNextBalls() + this.notify() + } + + private placeRandomBalls(count: number): boolean { + const emptyCells = getEmptyCells(this.state.board) + if (emptyCells.length < count) { + return false + } + + for (let i = 0; i < count; i++) { + const randomIndex = Math.floor(Math.random() * emptyCells.length) + const { row, col } = emptyCells.splice(randomIndex, 1)[0] + this.state.board[row][col].ball = getRandomColor() + } + + return true + } + + private generateNextBalls(): void { + this.state.nextBalls = Array.from({ length: BALLS_PER_TURN }, () => getRandomColor()) + } + + selectCell(row: number, col: number): boolean { + if (this.state.gameOver) return false + + const cell = this.state.board[row][col] + + if (cell.ball !== null) { + this.state.selectedCell = { row, col } + this.notify() + return true + } + + if (this.state.selectedCell === null) { + return false + } + + const from = this.state.selectedCell + const path = findPath(this.state.board, from, { row, col }) + + if (path === null) { + return false + } + + const ball = this.state.board[from.row][from.col].ball + this.state.board[from.row][from.col].ball = null + this.state.board[row][col].ball = ball + this.state.selectedCell = null + + const lines = checkLines(this.state.board, row, col) + + if (lines.length > 0) { + this.processLines(lines) + } else { + if (!this.placeNextBalls()) { + this.state.gameOver = true + } + } + + this.notify() + return true + } + + private processLines(lines: LineInfo[]): void { + let totalScore = 0 + let multiplier = 1 + + const sortedLines = [...lines].sort((a, b) => b.cells.length - a.cells.length) + + for (const line of sortedLines) { + const baseScore = getLineScore(line.cells.length) + totalScore += Math.floor(baseScore * multiplier) + multiplier *= 1.5 + + for (const { row, col } of line.cells) { + this.state.board[row][col].ball = null + } + } + + this.state.score += totalScore + } + + private placeNextBalls(): boolean { + const emptyCells = getEmptyCells(this.state.board) + + if (emptyCells.length < BALLS_PER_TURN) { + const ballsToPlace = Math.min(emptyCells.length, this.state.nextBalls.length) + if (ballsToPlace === 0) { + return false + } + + for (let i = 0; i < ballsToPlace; i++) { + const randomIndex = Math.floor(Math.random() * emptyCells.length) + const { row, col } = emptyCells.splice(randomIndex, 1)[0] + this.state.board[row][col].ball = this.state.nextBalls[i] + } + + this.generateNextBalls() + return emptyCells.length > 0 + } + + for (let i = 0; i < BALLS_PER_TURN; i++) { + const randomIndex = Math.floor(Math.random() * emptyCells.length) + const { row, col } = emptyCells.splice(randomIndex, 1)[0] + this.state.board[row][col].ball = this.state.nextBalls[i] + } + + this.generateNextBalls() + return true + } +} diff --git a/src/game/LineChecker.ts b/src/game/LineChecker.ts index 7594b7d..2ab3ea5 100644 --- a/src/game/LineChecker.ts +++ b/src/game/LineChecker.ts @@ -5,7 +5,8 @@ * Проверка линий для ColorLine98 */ -import { BOARD_SIZE, BallColor, MIN_LINE_LENGTH } from './Board' +import { BOARD_SIZE, MIN_LINE_LENGTH } from './Board' +import type { BallColor } from './Board' export interface LineInfo { cells: { row: number; col: number }[] diff --git a/src/game/PathFinder.ts b/src/game/PathFinder.ts index 1910cbb..393d7b8 100644 --- a/src/game/PathFinder.ts +++ b/src/game/PathFinder.ts @@ -5,7 +5,8 @@ * Поиск пути (BFS) для ColorLine98 */ -import { BOARD_SIZE, Board } from './Board' +import { BOARD_SIZE } from './Board' +import type { Board } from './Board' const DIRECTIONS = [ [-1, 0], diff --git a/src/game/index.ts b/src/game/index.ts index 74f939f..0f28bd3 100644 --- a/src/game/index.ts +++ b/src/game/index.ts @@ -8,3 +8,4 @@ export * from './Board' export * from './PathFinder' export * from './LineChecker' +export * from './Game'