REST API для классической игры Крестики-нолики.
Проект реализован с использованием NestJS, архитектуры с разделением на слои (domain, datasource, web) и покрыт тестами (Jest).
- Создание новой игры
- Совершение хода игрока
- Ходы компьютера (AI)
- Проверка состояния игры (активна, победа, ничья)
- Возврат игрового поля в удобном для чтения виде
- NestJS — каркас приложения
- TypeScript — строгая типизация
- Jest — тестирование
- Используются паттерны: ООП, MVC, Domain-driven design (DDD), Clean Architecture, DRY, SOLID, KISS;
Три слоя, зависимости направлены внутрь — к домену.
src/modules/
domain/ # бизнес-логика
model/ # Game, Board
port/ # GameRepositoryPort — интерфейс хранилища
service/ # GameService — правила игры и ИИ
datasource/ # хранение
repository/ # GameRepository implements GameRepositoryPort
storage/ # InMemoryGameStorage
mapper/ model/ # GameEntity ↔ Game
web/ # HTTP
controller/ # GameController
model/ # DTO с class-validator
mapper/
Домен объявляет интерфейс хранилища и токен для внедрения:
// domain/port/game.repository.port.ts
export const GAME_REPOSITORY_PORT = 'GAME_REPOSITORY_PORT';
export interface GameRepositoryPort {
save(game: Game): void;
findById(id: string): Game | null;
findAll(): Game[];
}GameService работает только с этим интерфейсом и никогда не видит конкретную реализацию:
constructor(
@Inject(GAME_REPOSITORY_PORT)
private readonly repository: GameRepositoryPort,
) {}Реализация подставляется в инфраструктурном слое:
// datasource/datasource.module.ts
{ provide: GAME_REPOSITORY_PORT, useClass: GameRepository }Поэтому InMemoryGameStorage заменяется на реальную БД без единой правки в домене.
Доменная Game и персистентная GameEntity — разные типы, преобразование идёт через
GameDatasourceMapper (toDomain / toEntity). Схема хранения может меняться независимо
от бизнес-модели.
- Ход компьютера — минимакс с полным перебором и штрафом за глубину (
10 - depth), поэтому ИИ играет оптимально и выбирает наиболее быструю победу. makePlayerMoveне мутирует состояние: доска копируется, возвращается новыйGame.validateGameBoardпроверяет, что за ход изменилась ровно одна пустая клетка, а уже занятые остались нетронутыми.
# Клонировать репозиторий
git clone https://github.com/YanaKris/tic-tac-toe-api.git
cd tic-tac-toe-api
# Установить зависимости
npm install
# Запустить приложение
npm run start:dev
# Запустить unit-тесты
npm run testPOST /game
Пример ответа:
{
"uuid": "123e4567-e89b-12d3-a456-426614174000",
"board": [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
],
"status": "active"
}POST /game/:uuid/move
Тело запроса:
{
"x": 1,
"y": 2
}Пример ответа:
{
"uuid": "123e4567-e89b-12d3-a456-426614174000",
"board": [
[2, 0, 0],
[0, 1, 0],
[0, 0, 0]
],
"status": "active"
}