# Тестовое задание — Unity / C# Middle

> Короткое тестовое задание на **примерно 1,5 часа**.  
> Нужно выбрать **один из двух вариантов** и сделать его с нуля.

---

## Содержание

- [Введение](#введение)
- [Технические требования](#технические-требования)
- [Архитектура](#архитектура)
- [Что нужно сдать](#что-нужно-сдать)
- [Чего не ждём](#чего-не-ждём)
- [Вариант A — Boot Flow](#вариант-a--boot-flow)
- [Вариант B — Energy--Regen](#вариант-b--energy--regen)

---

## Введение

Привет! Спасибо, что нашёл время.

Ниже — короткое тестовое задание. Внутри есть два варианта. Нужно выбрать **один**.

Оба варианта равнозначны по сложности, поэтому можно выбрать тот, который ближе по интересу.  
Задание нужно делать **с нуля**:

- Unity-проект создаётся самостоятельно;
- базовые абстракции пишутся самостоятельно;
- архитектурные решения являются частью оценки.

Результат нужно отправить **до завтра 19:00 по МСК** в Telegram: [@mattnastya](https://t.me/mattnastya).

Формат домашний, поэтому не требуется идеально попадать во время или доказывать, сколько часов было потрачено.  
Если ушло больше или меньше 1,5 часов — это нормально. Просто укажи это честно.

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

---

## Технические требования

### Общие требования

- **Unity 2022 LTS+**
- **C#**
- Разрешено подключать:
  - `VContainer`
  - `UniTask`
  - `UniRx` / `R3`
  - свою реактивную реализацию
- Опционально:
  - `Odin Inspector`
  - `DOTween`

---

## Архитектура

Ниже перечислены обязательные архитектурные требования, в которые нужно попасть.

### Reactive

Можно использовать `UniRx`, `R3` или собственный `ReactiveValue<T>`.

Минимальная сигнатура:

```csharp
IDisposable Subscribe(Action<T> cb, bool invokeImmediately = true);
```

Важно:

- каждая подписка должна корректно диспозиться;
- не должно быть голых `event Action` без отписки;
- жизненный цикл подписок должен быть понятным и безопасным.

---

### Async

Все асинхронные операции должны быть сделаны через:

- `UniTask`
- `CancellationToken`

`async void` запрещён везде, кроме Unity-колбэков.

---

### Dependency Injection

Использовать **VContainer**.

Все сервисы нужно регистрировать через интерфейсы:

```csharp
builder.Register<Impl>(Lifetime.Singleton).As<IInterface>();
```

Запрещено использовать:

- `FindObjectOfType`
- `Singleton.Instance`
- `static`-хранилища состояния

---

### UI

Нужно сделать свою базовую пару классов:

```csharp
public class UIView : MonoBehaviour
{
    public void Initialize();
    public void Release();
}
```

```csharp
public class UIView<TVm> : UIView
    where TVm : IUIViewModel
{
}
```

Требования к UI:

- `ViewModel` — обычный C#-класс;
- `ViewModel` не должен наследоваться от `MonoBehaviour`;
- логика должна быть во `ViewModel`;
- `View` хранит только Unity-ссылки и биндинги.

---

### Service

Сервисы должны быть оформлены через `Service : IService` с асинхронным жизненным циклом:

```csharp
UniTask InitializeAsync(CancellationToken ct);
UniTask ReleaseAsync(CancellationToken ct);
```

Требования:

- фоновые циклы стартуют в `InitializeAsync`;
- фоновые циклы останавливаются через `CancellationTokenSource.Cancel()` в `ReleaseAsync`;
- отмена должна быть реальной, а не формальной.

---

### Config

Конфиги должны быть сделаны через `ScriptableObject` с суффиксом `*Settings`.

Пример:

```csharp
[field: SerializeField]
public EnergySettings EnergySettings { get; private set; }
```

Регистрация в `LifetimeScope`:

```csharp
builder.RegisterInstance(_settings);
```

---

## Что нужно сдать

- GitHub-репозиторий  
  или `.zip`-архив без папок:
  - `Library/`
  - `Temp/`
  - `obj/`

- `README.md`:
  - как запустить проект;
  - 5–10 строк о том, что бы ты доделал, если бы было ещё 2 часа.

- `SELF_NOTES.md` — обязательный документ про собственные решения.

В `SELF_NOTES.md` нужно написать своими словами:

- какие идеи ты рассмотрел;
- почему выбрал именно эту реализацию, а не альтернативы;
- какие места в коде ты придумал и написал сам, без AI;
- почему именно эти места писал сам;
- что в коде ты понимаешь до последней строки;
- что осталось «магией», если такое есть;
- как бы ты объяснил 2–3 ключевых решения, если бы завтра тебя спросили:  
  «Почему здесь сделано именно так?»

- `AI_LOG.md`:
  - какие промпты использовал;
  - где AI ошибся;
  - что переписал руками;
  - если AI не использовал — напиши, почему.

> Важно: нужно показать, что ты понимаешь собственные решения, а не просто копируешь готовый ответ из AI.

---

## Чего не ждём

Не нужно тратить время на:

- красивый арт;
- анимации;
- звук;
- мобильную сборку.

Главное:

- слои;
- стиль кода;
- архитектура;
- понимание собственного решения.

---

# Варианты задания

Нужно выбрать **один** вариант.

| Вариант | Название | Суть |
|---|---|---|
| A | Boot Flow | State machine из трёх стейтов |
| B | Energy & Regen | Реактивный сервис энергии и UI |

---

# Вариант A — Boot Flow

> Задача: сделать загрузочный поток приложения через свою стейт-машину.

## 1. База стейт-машины

Нужно реализовать свои абстракции:

- `IState`
- `IStatesController<TEnum>`
- `StatesController<TEnum>`

У контроллера должен быть метод:

```csharp
UniTask EnterStateAsync(TEnum code, CancellationToken ct);
```

Контракт перехода между стейтами:

```csharp
await currentState.ExitAsync(ct);
await newState.EnterAsync(ct);
```

Требования:

- сначала вызывается `ExitAsync` текущего стейта;
- потом вызывается `EnterAsync` нового стейта;
- `CancellationToken` пробрасывается насквозь;
- внутренние ожидания должны реально отменяться через `CancellationToken`.

---

## 2. Три стейта

### SplashState

Должен:

- показать лого;
- подождать 1 секунду через `UniTask.Delay(..., ct)`;
- перейти дальше.

---

### LoadState

Должен имитировать загрузку:

- 5 шагов;
- каждый шаг длится 200 мс;
- на стейте лежит реактивное значение прогресса:

```csharp
ReactiveValue<float> Progress;
```

Прогресс:

- диапазон от `0` до `1`;
- обновляется после каждого шага загрузки.

---

### MenuState

Должен:

- показать `MenuUIView`;
- содержать одну кнопку `Restart`;
- по клику возвращать приложение в `LoadState`.

---

## 3. UI

`LoadingUIView` должен быть подписан на:

```csharp
LoadState.Progress
```

Он должен двигать прогресс-бар:

- через `DOTween`;
- или через ручной `Lerp`;
- способ не принципиален.

Требования к жизненному циклу:

- при `ExitAsync` все подписки должны корректно диспозиться;
- при повторном входе не должно быть `NullReferenceException`;
- повторный вход в стейт должен работать стабильно.

---

# Вариант B — Energy & Regen

> Задача: сделать систему энергии для мобильной игры.

## 1. EnergySettings

Нужно создать `ScriptableObject`:

```csharp
public class EnergySettings : ScriptableObject
{
    public int MaxEnergy;
    public float RegenSeconds;
}
```

Поля:

- `MaxEnergy` — максимальное количество энергии;
- `RegenSeconds` — количество секунд на восстановление одной единицы энергии.

Требования:

- создать asset с настройками;
- заинжектить его в `LifetimeScope`.

---

## 2. IEnergyService / EnergyService

Нужно реализовать:

```csharp
public interface IEnergyService
{
    IReadOnlyReactiveValue<int> Current { get; }
    IReadOnlyReactiveValue<float> SecondsToNext { get; }

    bool TrySpend(int amount);
}
```

`EnergyService` должен наследоваться от `Service`.

### Поля

```csharp
IReadOnlyReactiveValue<int> Current;
IReadOnlyReactiveValue<float> SecondsToNext;
```

`SecondsToNext` — это доля прогресса до следующей единицы энергии:

- `0` — восстановление только началось;
- `1` — следующая единица почти восстановлена.

### Метод

```csharp
bool TrySpend(int amount);
```

Метод должен:

- проверять, хватает ли энергии;
- если хватает — списывать энергию и возвращать `true`;
- если не хватает — ничего не менять и возвращать `false`.

### Регенерация

Регенерация должна быть фоновой `UniTask`-петлёй внутри сервиса.

Требования:

- петля стартует в `InitializeAsync`;
- петля корректно останавливается в `ReleaseAsync`;
- остановка делается через собственный `CancellationTokenSource`;
- если `Current == MaxEnergy`, петля должна спать эффективно;
- нельзя крутить `Delay(0)` внутри `while`.

---

## 3. UI

Нужно реализовать:

```csharp
EnergyBarUIView<EnergyBarUIViewModel>
```

UI должен показывать:

- текст `current / max`;
- прогресс-бар:

```csharp
Image.fillAmount = SecondsToNext;
```

- кнопку `Потратить 10`.

Требования:

- биндинги делаются через `ReactiveValue.Subscribe(...)`;
- отписки выполняются в `Release()`;
- запрещено обновлять UI через `Update()`.

---

## Критерии, на которые будут смотреть

- Понятная архитектура.
- Корректная работа с жизненным циклом.
- Отсутствие скрытых синглтонов и статического состояния.
- Корректная отмена `UniTask`-операций.
- Чистые подписки и отписки.
- Разделение логики между `View`, `ViewModel` и сервисами.
- Понимание решений, описанное в `SELF_NOTES.md`.
