feat(game): реализация ядра игровой логики ColorLine98
- Добавлен класс Game для управления состоянием игры - Реализованы: генерация шаров, превью следующих 3 шаров - Подсчёт очков по таблице из GDD (5→10, 6→12, 7→18, 8→28, 9+→формула) - Комбо-множитель x1.5 за каждую дополнительную линию - Условие окончания игры (поле заполнено) - Исправлены type imports для verbatimModuleSyntax Refs: [GAM-8](/GAM/issues/GAM-8) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
198
src/game/Game.ts
Normal file
198
src/game/Game.ts
Normal file
@@ -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<number, number> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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 }[]
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
export * from './Board'
|
||||
export * from './PathFinder'
|
||||
export * from './LineChecker'
|
||||
export * from './Game'
|
||||
|
||||
Reference in New Issue
Block a user