Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
*.egg-info/
.eggs/
dist/
build/
.venv/
venv/
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"task.allowAutomaticTasks": "on"
}
30 changes: 30 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Snake: auto demo (terminal)",
"type": "shell",
"command": "python prototype_cli.py --demo --loop --ticks 200 --fps 8 --loop-delay 1 --random-seed",
"windows": {
"command": "python prototype_cli.py --demo --loop --ticks 200 --fps 8 --loop-delay 1 --random-seed"
},
"linux": {
"command": "python3 prototype_cli.py --demo --loop --ticks 200 --fps 8 --loop-delay 1 --random-seed"
},
"osx": {
"command": "python3 prototype_cli.py --demo --loop --ticks 200 --fps 8 --loop-delay 1 --random-seed"
},
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"reveal": "always",
"panel": "dedicated",
"focus": false,
"clear": false
},
"isBackground": true,
"problemMatcher": []
}
]
}
81 changes: 79 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,79 @@
# cli_snake
Simple game "Classic Snake" in the command line interface
# Змейка — ядро игры

Расширенная змейка с бесконечным ростом, видами змей, чувствами, процедурным миром и параллельными вселенными.

## Структура

```
game/
├── constants.py # Константы и перечисления
├── species.py # Уровни/виды змеи
├── food_types.py # Типы еды и характеристики
├── entities.py # Snake, Food
├── senses.py # Обоняние, слух, слабое зрение
├── abilities.py # Прокачка способностей
├── world.py # Генерация мира по seed
├── levels.py # Уровни и цели прохождения
├── mechanics.py # Поедание, урон, линька, смерть
└── engine.py # Игровой движок
```

## Ключевые механики

| Механика | Описание |
|----------|----------|
| Рост | Бесконечный через поедание; линька сбрасывает длину, уровень сохраняется |
| Виды | Червеобразная → Травяная → … → Космическая |
| Яд | Баланс ядовитой и неядовитой пищи определяет ядовитость |
| Поедание | Голова ≥ размер жертвы — можно съесть |
| Урон | Жертва больше головы — потеря сегментов при прохождении |
| Смерть | Потеря >50% длины, удар о препятствие, раздавливание |
| Труп | Тело змеи становится едой на том же уровне |
| Раздавливание | Хвост <50% длины — смерть; иначе только потеря длины |
| Чувства | Еда видна по запаху/звуку; движущаяся — при шаге/писке |
| Стены | Пробитие при достаточной длине и уровне (−50% длины) |
| Вселенные | После внешних стен — параллельный мир с другими правилами |
| Мир | Процедурная генерация по `seed` |

## Запуск прототипа

Авто-режим (удобно в CI и облаке):

```bash
python3 prototype_cli.py --demo --ticks 150 --fps 10 --seed 42
```

Интерактивно (локальный терминал):

```bash
python3 prototype_cli.py
```

Управление: `WASD` / стрелки, `p` — пауза, `q` — выход.

## Запуск тестов

```bash
python3 -m unittest discover -s tests -v
```

## CLI-прототип

Классическая CLI-версия: `cli_snake.py` (не интегрирована с новым ядром).

## Пример использования ядра

```python
from game import GameEngine
from game.constants import Direction

engine = GameEngine(seed=42)
engine.new_game(level_index=0)

for _ in range(100):
result = engine.tick(Direction.RIGHT)
if not result.snake_alive:
break

print(engine.status_text())
```
14 changes: 14 additions & 0 deletions game/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Ядро игры «Змейка» — растущая змея с уровнями видов, чувствами и процедурным миром."""

from game.engine import GameEngine, GameState
from game.entities import Food, Snake, Vec2
from game.world import World

__all__ = [
"Food",
"GameEngine",
"GameState",
"Snake",
"Vec2",
"World",
]
86 changes: 86 additions & 0 deletions game/abilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Прокачка способностей змеи."""

from dataclasses import dataclass, field
from enum import Enum

from game.senses import SenseProfile


class AbilityId(Enum):
SMELL = "smell"
HEARING = "hearing"
VISION = "vision"
TOUGHNESS = "toughness"
DIGESTION = "digestion"
VENOM = "venom"


@dataclass
class AbilityDef:
id: AbilityId
name: str
description: str
max_level: int
cost_per_level: int


ABILITY_DEFS: dict[AbilityId, AbilityDef] = {
AbilityId.SMELL: AbilityDef(
AbilityId.SMELL, "Обоняние", "Увеличивает дальность запаха", 5, 3,
),
AbilityId.HEARING: AbilityDef(
AbilityId.HEARING, "Слух", "Усиливает восприятие звуков добычи", 5, 3,
),
AbilityId.VISION: AbilityDef(
AbilityId.VISION, "Зрение", "Немного улучшает ближнее зрение", 3, 5,
),
AbilityId.TOUGHNESS: AbilityDef(
AbilityId.TOUGHNESS, "Прочность", "Снижает урон при прохождении через крупную добычу", 5, 4,
),
AbilityId.DIGESTION: AbilityDef(
AbilityId.DIGESTION, "Пищеварение", "Бонус к росту от еды", 5, 4,
),
AbilityId.VENOM: AbilityDef(
AbilityId.VENOM, "Яд", "Усиливает эффект ядовитой пищи", 5, 4,
),
}


@dataclass
class AbilityState:
levels: dict[AbilityId, int] = field(default_factory=dict)
skill_points: int = 0

def level_of(self, ability: AbilityId) -> int:
return self.levels.get(ability, 0)

def can_upgrade(self, ability: AbilityId) -> bool:
current = self.level_of(ability)
definition = ABILITY_DEFS[ability]
return current < definition.max_level and self.skill_points >= definition.cost_per_level

def upgrade(self, ability: AbilityId) -> bool:
if not self.can_upgrade(ability):
return False
cost = ABILITY_DEFS[ability].cost_per_level
self.skill_points -= cost
self.levels[ability] = self.level_of(ability) + 1
return True

def to_sense_profile(self) -> SenseProfile:
return SenseProfile(
smell_range=6 + self.level_of(AbilityId.SMELL) * 2,
hearing_range=8 + self.level_of(AbilityId.HEARING) * 2,
vision_range=2 + self.level_of(AbilityId.VISION),
smell_sensitivity=1.0 + self.level_of(AbilityId.SMELL) * 0.15,
hearing_sensitivity=1.0 + self.level_of(AbilityId.HEARING) * 0.15,
)

def damage_reduction(self) -> float:
return self.level_of(AbilityId.TOUGHNESS) * 0.1

def growth_bonus(self) -> int:
return self.level_of(AbilityId.DIGESTION)

def poison_bonus(self) -> int:
return self.level_of(AbilityId.VENOM)
64 changes: 64 additions & 0 deletions game/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Общие константы ядра игры."""

from enum import Enum, IntEnum


class Direction(IntEnum):
LEFT = 10
RIGHT = 11
UP = 12
DOWN = 13


class GamePhase(Enum):
NEW = "new"
PLAYING = "playing"
PAUSED = "paused"
ENDED = "ended"
UNIVERSE_TRANSITION = "universe_transition"


class TileType(Enum):
EMPTY = "empty"
WALL = "wall"
INNER_WALL = "inner_wall"
OUTER_WALL = "outer_wall"
BREAKABLE_WALL = "breakable_wall"
WATER = "water"


class Mobility(Enum):
STATIC = "static"
MOBILE = "mobile"


class FoodCategory(Enum):
INSECT = "insect"
RODENT = "rodent"
BIRD = "bird"
FRUIT = "fruit"
BERRY = "berry"
VEGETABLE = "vegetable"
CARCASS = "carcass"
HERBIVORE = "herbivore"
PREDATOR = "predator"
SNAKE_CARCASS = "snake_carcass"


# Порог потери длины за одно событие — смерть
DEATH_LENGTH_LOSS_RATIO = 0.5

# Доля хвоста от общей длины — выживание при раздавливании
TAIL_SURVIVAL_RATIO = 0.5

# Порог яда/антияда для определения ядовитости
POISON_THRESHOLD = 3

# Базовый размер головы новорождённой змеи
BASE_HEAD_SIZE = 1

# Начальная длина змеи
INITIAL_SNAKE_LENGTH = 3

# Доля длины, теряемая при пробитии стены / переходе вселенной
WALL_BREAK_LENGTH_LOSS_RATIO = 0.5
Loading