Проф. Волкгонагалл: Лесная Трансфигурация

Материал из Абсурдопедии
Перейти к навигацииПерейти к поиску
Я не сру, я минирую реальность. Я не ем, я провожу оптимизацию демографии. Я — санитар леса, а вы — лишь флуктуации биомассы на моей координатной сетке!
~ Цитата из предвыборной программы Волка, 2026 год

Осторожно, волк-людоед! Что тебе дороже: грибы или жизнь?
~ Табличка в лесу, которую грибник вдумчиво читает 4 хода, пока его едят

«Проф. Волкгонагалл: Лесная Трансфигурация» (англ. WolfGonagall: The Bureaucratic Carnivore Simulator) — величайший текстовый симулятор выживания, социальной инженерии, коррупционных схем и гастроэнтерологического террора. Выпущенная в знойном августе 2026 года неизвестным гением-мизантропом на чистом Python, игра, первоначально маскировавшаяся под простенький roguelike, на деле оказалась пугающе точным отражением суровой реальности и бюрократического абсурда. ​С момента появления в сети этот шедевр стал причиной массовой истерии. Чиновники из Министерства лесного хозяйства пытались запретить игру за «разглашение государственной тайны о реальных методах работы егерей», а Союз Писателей подал в суд за пропаганду поедания библиотекарей. ​Аналитики Абсурдопедии провели декомпиляцию этого монументального феномена. Перед вами 15 фундаментальных столпов, на которых держится эта гротескная вселенная. Пристегните ремни, мы спускаемся в самые тёмные уголки пищевой цепи.

Историческая основа: Засекреченный инцидент «Красная Шапочка-2026»[править]

​Обыватели, не знакомые с теорией заговоров, уверены, что текстовые сетки с эмодзи — это всего лишь плод больной фантазии ленивого программиста. Но архивы ФСБ не лгут. Игра с маниакальной точностью воспроизводит засекреченный инцидент, произошедший в одном из лесничеств средней полосы аккурат в августе 2026 года. В тот роковой сезон мутировавший волк, обладающий интеллектом гроссмейстера и метаболизмом промышленного мусоросжигательного завода, начал методичную зачистку территории от двуногих паразитов.

​Разработчик не придумал ничего. Он лишь безжалостно оцифровал реальность. Лес (🌲), усеянный спорами грибов (🍄), хижинами дачников-идиотов (🏠) и пивными ларьками (🎪), стал идеальной ареной для дарвиновского отбора. В реальной жизни, как и в игре, местные власти оказались абсолютно бессильны против зверя, который научился использовать законы Российской Федерации, Конвенцию о защите животных и собственные экскременты в качестве оружия массового поражения.

Экзистенциальная философия протагониста (🐺)[править]

​Вы играете за Волка. Но это не романтизированный Акела, воющий на луну, и не глупый хищник из русских народных сказок. Местный протагонист — это уставший от жизни интеллектуал-социопат, застрявший в петле сансары между двумя всадниками апокалипсиса: всепоглощающим Голодом и неминуемой Дефекацией. ​Цель Волка — не просто набить брюхо. Его миссия глубоко урбанистична и экологична: он проводит джентрификацию леса. Лес загажен. Он кишит праздношатающимися грибниками, вытаптывающими реликтовый мох, и вооружёнными до зубов маргиналами с удостоверениями егерей. Волк выступает в роли высшего судьи, безжалостного аудитора биомассы. Каждый съеденный человек для него — это не калории, это восстановление космической справедливости, омрачённое лишь необходимостью впоследствии переварить их дешёвую синтетическую одежду.

Биомеханика гравитационной дефекации: Искусство сорванного полёта[править]

​Игровая индустрия годами приучала нас к тому, что герои могут сожрать сорок головок сыра или пять жареных кабанов прямо во время боя без всяких последствий. «ВолкГонагалл» ломает этот стереотип об колено, вводя суровый закон желудочно-кишечной инерции.

​Как только вы проглатываете грибника или егеря (а это, на минуточку, 80 килограммов костей, мяса, панамки и ведра с лисичками), статус хищника меняется на needs_to_poop = True. Вы становитесь тяжелым, как совесть коррумпированного депутата. Игроки, привыкшие к динамике, инстинктивно жмут клавиши двойного прыжка (WASD или ЦФЫВ), ожидая грациозного, стремительного полёта волка над кустами. ​Но вместо этого происходит величайший облом: переполненный желудок прибивает зверя к земле! Волк никуда не летит. Он остаётся на месте. Гравитация и биология берут своё: вся кинетическая энергия несостоявшегося прыжка конвертируется во внутрибрюшное давление. Зверь напрягает сфинктер, и мощный выброс шлаков (💩) ложится ровно на ту клетку, через которую он пытался перелететь. Это не баг геймдизайна, это глубокая философская метафора: ты не можешь возвыситься над суетой (прыгнуть), пока не сбросишь балласт прошлого.

Копро-архитектура и тактическое минирование (💩)[править]

​Обычное дерьмо в лесу — это неприятность. Дерьмо ВолкГонагалла — это монолитная архитектурная форма, способная остановить продвижение спецназа. Опорожнить кишечник можно только на пустую землю (..) или, что особенно приветствуется игрой, прямо на грибы (🍄).

​Это гениальная издёвка над собирателями. Сколько раз вы приезжали в красивый лес за грибами, лишь для того, чтобы обнаружить под самым красивым подосиновиком чью-то кучу? В игре эти био-мины остаются на карте навсегда. Искусственный интеллект игры устроен так, что ни грибники, ни егеря не могут наступить на ваши миазмы. Профессиональные киберспортсмены используют эту механику для создания целых лабиринтов из фекалий, отрезая егерей от баров или загоняя грибников в узкие «кишки», где их удобно употреблять в пищу. Великая Китайская Стена была построена из камня; Великая Лесная Стена строится из переваренных людей.

Грибники (🤠): Броуновское движение и 360-градусный идиотизм[править]

​Долгое время в фанатской среде бытовал миф, будто грибники в игре настолько косоглазы, что способны передвигаться только по диагонали. Исходный код безжалостен в своей реалистичности: грибники могут делать шаг на 1 клетку абсолютно в любом из восьми направлений, что делает их пугающе похожими на реальных дачников, ищущих дорогу к электричке.

​Поведение грибника — это идеальная симуляция человека, который увидел в лесу белку, но забыл, где оставил машину. Их траектории — чистый, первозданный хаос. Они непредсказуемы, как решения ТСЖ. Грибник может методично идти на север, затем резко прыгнуть на юго-восток, обогнуть сосну и с радостной улыбкой шагнуть прямо вам в пасть. Ими движет лишь слепой инстинкт сбора (get_direction_to_nearest(npc, '🍄')), который заглушает даже базовый инстинкт самосохранения. ​6. Проклятие предупреждающих табличек (🪧)

​Непреодолимая тяга NPC к чтению табличек — это блестящая, злая сатира на наше общество. В реальной жизни человек, видя надпись «Не влезай — убьёт!», обязательно подойдёт поближе, потрогает провод и заглянет в трансформаторную будку.

​В игре грибник, наткнувшись на деревянный щит, замирает на долгих четыре хода, превращаясь в идеальную неподвижную мишень. Каждый ход в консоль выводится процесс его мыслительной деятельности:

  • Ход 1: 🤠 читает: "Осторожно, волк-людоед!.."
  • Ход 2: 🤠 читает: "...В этом лесу водится опасный хищник,.."

И так далее. Если в этот момент волк сожрёт кого-то на соседней клетке и заляпает эту самую табличку кровью, грибник прервёт чтение, сбросит таймер и философски произнесёт: "Что это было? А, показалось", после чего пойдёт дальше искать маслята. Эта механика иллюстрирует абсолютную слепоту масс к надвигающемуся пиздецу.

Трезвые егеря (👮): Правоохранительная Матрица[править]

​Если грибники олицетворяют хаос, то трезвые егеря — это безжалостный, ледяной порядок. Они напоминают сотрудников ДПС, спрятавшихся с радаром в кустах на трассе, где знак «40» забыли снять с прошлого года. Они не спят, не отвлекаются на таблички и обладают встроенным тепловизором (has_los). ​Если между трезвым егерем и Волком нет деревьев, зданий или спасительных куч дерьма, активируется алгоритм прямого аннигилирования по формуле dmg = round(180 / max(1, dist)). На дистанции в одну клетку егерь наносит 180 урона, разрывая волка на ошмётки с одного выстрела из табельного дробовика. Их точность абсолютна, как налоги. Встреча с трезвым егерем на открытом пространстве — это гарантированная путёвка в волчью Вальгаллу. Но у этой безупречной машины смерти есть одна исконно русская, фатальная уязвимость.

Бары и ларьки (🎪): Чёрные дыры морали[править]

​В лесу случайным образом генерируются бары (в игре они деликатно прикрыты эмодзи циркового шатра 🎪, что само по себе иронично). И вот тут начинается настоящая магия отечественного реализма. Как только трезвый, целеустремлённый егерь, готовый отдать жизнь за защиту природы, оказывается на соседней клетке с баром, он мгновенно забывает про долг, присягу и гигантского волка.

​В коде игры прописано жёстко: если бар рядом, егерь исчезает внутри (entered_bar = True). Эти шатры работают как чёрные дыры, засасывающие в себя остатки человеческого достоинства. Лес может гореть, волки могут доедать последних грибников, но егерь будет сидеть в баре, потому что у него, очевидно, наступил законный обеденный перерыв, плавно перетекающий в запой.

Пьяные клоуны (🤡): Оружие массового поражения[править]

​Спустя несколько ходов из бара выходит совершенно иная сущность — Пьяный Егерь, чьё агрегатное состояние обозначается эмодзи клоуна 🤡 (is_drunk = True). Это уже не страж порядка, это мобильная установка залпового огня, которой управляет слепая макака под спидами.

​Система распознавания «свой-чужой» у алкаша отключается напрочь. Водка стирает границы между видами. Пьяный начинает палить во всё, что движется. Если он не видит волка, он стреляет в кусты, где прячутся грибники, или... в своих же трезвых коллег! Наблюдать за этим — высшее эстетическое удовольствие.

Игра в этот момент выдаёт диалоги, достойные Тарантино: ​*👮 (Трезвый): "Ты куда стреляешь, придурок?" ​*🤡 (Пьяный): "Оп, маслину поймал, гы-гы-гы!"

Опытные волки никогда не трогают пьяных егерей. Зачем пачкать лапы, если можно использовать алкоголизм как бесплатную артиллерию для зачистки сектора?

Рослесхозный Коллапс: Великая Сила Стукачества[править]

​Но что происходит, если пьяный егерь стреляет в грибника, но не добивает его? В любой другой игре NPC бы просто умер от потери крови. Но в «ВолкГонагалл» реализована самая страшная и разрушительная сила современности — Бюрократическая Жалоба.

​Испуганный раненый грибник впадает в панику и с воплями "Я буду жаловаться в Рослесхоз! 😭😭😭" несётся к своему домику (🏠). Как только он переступает порог, система рушится. На весь экран загорается красная, фатальная надпись: «Грибник написал жалобу в Рослесхоз! ВСЕ БАРЫ ЗАКРЫТЫ!» ​Это конец эпохи Возрождения и начало Темных Веков. «Сухой закон» вступает в силу мгновенно. Бары перестают генерировать пьяных клоунов. Экосистема резко меняется: веселый хаос уступает место тоталитарному порядку, где по лесу бродят только злые, вынужденно протрезвевшие егеря, жаждущие волчьей крови. Это учит игрока тонкому балансу: используй пьяных, но не оставляй в живых стукачей.

Храм Знаний (🏛️) и литературные артефакты[править]

​Самый сюрреалистичный этап игры начинается тогда, когда волк съедает достаточное количество людей. С вероятностью, известной лишь богам рандома, волк не переваривает литературу. Из его пасти вываливаются «Книга Грибника 📗» и «Книга Егеря 📘». Зачем эти люди носили с собой в лес библиотечные книги? Почему егерь взял почитать роман вместо Уголовного Кодекса? Игра хранит гордое молчание.

​Собрав эти артефакты, обмазанный кровью зверь должен явиться к Библиотеке. Причём войти в неё по диагонали технически запрещено (видимо, у здания нет пандусов для диагонального перемещения четвероногих). Внутри происходит акт высшего кафкианского абсурда. Библиотекарь, не обращая внимания на клыки и запах гниющей плоти, читает вам стихи собственного сочинения.

Красная Книга (📕): Бюрократическое Бессмертие[править]

​В благодарность за возврат просроченной литературы, Библиотекарь совершает действие, ломающее саму ткань мироздания: он торжественно вписывает Волка-Людоеда в Красную книгу (📕). ​Отныне волк получает абсолютную юридическую неприкосновенность. Статус охраняемого вымирающего вида оказывается крепче бронежилета. Когда трезвый, вооруженный до зубов егерь видит волка, доедающего чью-то ногу, он со вздохом опускает ружьё и произносит сакраментальную фразу: «Бля-я, его ж нельзя убивать...».

​Законы Российской Федерации побеждают инстинкт самосохранения! Вы можете буквально жрать граждан на глазах у представителей власти, но у вас есть справка. Впрочем, разработчик оставил гениальный штрих: сразу после получения бумажки, код заботливо предлагает волку навалить кучу прямо в читальном зале и сожрать самого библиотекаря. Потому что Красная книга защищает вас от егерей, но Трудовой Кодекс никак не защищает библиотекаря от хищника.

Акустика Ужаса: Система цепной паники[править]

​Помимо стрельбы и экскрементов, в игре реализована прорывная система акустического давления. Грибники, заметившие волка, начинают истошно визжать (has_yelled_wolf = True). Над картой появляется кроваво-красная строка: «ВОЛК! ВОЛК! ВОЛК!», длина которой визуализирует уровень массовой истерии. ​И здесь вскрывается жестокая правда: крики не отпугивают врагов, они работают как маяк. Параметр total_yells накапливается, и каждая поднятая тревога математически увеличивает шанс того, что на границах экрана (rim_cells) отспавнится новый отряд егерей. Реализм зашкаливает: в нашем мире крики о помощи тоже чаще всего приводят к тому, что на место прибывает толпа суровых мужиков с дубинками, которые начинают прессовать всех подряд, не особо разбираясь, кто тут хищник, а кто просто зашёл по малой нужде в кусты.

Физиология истощения: Безжалостный таймер метаболизма[править]

​Вы думали, можно просто спрятаться в лесу и переждать? Игра не позволяет вам быть пассивным наблюдателем. В левом верхнем углу неумолимо тает полоска сытости (satiety). ​Волк — это акула; если он не жрёт человечину, он умирает. Каждый ход без еды сжигает калории. Как только сытость падает до нуля, начинает сгорать само здоровье (health -= 1), сопровождаемое укоризненным системным сообщением: "🩸 Вы голодаете! Здоровье падает". Вы не можете питаться грибами — вы же не крестьянин, вы санитар леса. Вы обязаны постоянно поддерживать уровень насилия на должном уровне, иначе ваш собственный метаболизм сожрёт вас изнутри.

Эсхатология Волка: Посмертное унижение[править]

​Разработчик позаботился о том, чтобы даже поражение приносило игроку глубокие моральные страдания. В игре нет сухих надписей «Game Over». Если вы погибли, система анализирует ваш позор и выдает диагноз:

​*Смерть пацифиста/вегана: Если вы умерли от голода, так никого и не убив, игра презрительно выведет: "Ты решил стать веганом?" (Абсолютное уничтожение чести).

​*Смерть труса: Если вас застрелили до первой трапезы: "Ты не волк, а жалкий щенок!"

​*Смерть казуала: Если вы охотились только на безоружных грибников (🤠) и откинули лапы при первой же встрече с полицией: "Что, волчок, только безоружных можешь кушать?"

​*Смерть Вальгаллы: Вам выдадут скупую мужскую похвалу только в том случае, если вы успели перегрызть глотку хотя бы одному егерю. Лишь тогда экран смерти признает: "Ты храбро сражался и не обосрался (в переносном смысле)". Уточнение про переносный смысл критически важно — ведь в прямом смысле вы, согласно пункту 4, заминировали пол-леса.

Мета-игра и Диктатура: Как победить Систему[править]

Волк победа.jpg

​Как вообще выиграть в этот симулятор хтонического абсурда? Убить всех физически невозможно — вселенная процедурно генерируется, и новые люди лезут из-за края экрана бесконечно. Победа достигается только через идеальную синергию всех пороков общества: ​*Вы должны получить справку о неприкосновенности (Красную Книгу). ​*Вы должны заставить грибника-стукача добежать до дома и закрыть бары (Рослесхозный коллапс). ​*Вы должны доесть всех оставшихся пьяных егерей (поскольку новые больше не появляются). ​*Вы должны переждать 10 ходов (turns_since_drunk_seen > 10), дефилируя по очищенному от алкоголизма лесу.

​Вы побеждаете не тогда, когда всех съели. Вы побеждаете, когда система замыкается сама на себе: бары закрыты навсегда, трезвые егеря парализованы бюрократией и не смеют в вас стрелять, а немногочисленные грибники сидят по домам в животном страхе. Волк становится не просто санитаром леса — он становится его единоличным, легитимным, неприкосновенным Диктатором. ​«Проф. ВолкГонагалл: Лесная Трансфигурация» — это не просто кусок кода. Это мрачное, злое и бесконечно смешное зеркало нашей реальности, в котором отражаются бабки с корзинками, коррумпированные чиновники, бессилие перед бумажкой с печатью и вечная, неистребимая человеческая потребность нагадить, чтобы сбросить с себя тяжесть бытия.

Код игры[править]

import os
import sys
import random
import shutil
import time
import textwrap
import math
import re

if os.name == 'nt':
    os.system('chcp 65001 >nul')

def clear_screen():
    if os.name == 'nt':
        os.system('cls')
    else:
        print("\033[H\033[J", end="")

def visible_len(s):
    return len(re.sub(r'\033\[[0-9;]*m', '', s))

def get_input():
    # Маппинг движения для 1 шага
    move_map = {
        'е': (-1, -1), 't': (-1, -1),
        'н': (0, -1),  'y': (0, -1),
        'г': (1, -1),  'u': (1, -1),
        'п': (-1, 0),  'g': (-1, 0),
        'о': (1, 0),   'j': (1, 0),
        'м': (-1, 1),  'v': (-1, 1),
        'и': (0, 1),   'b': (0, 1),
        'т': (1, 1),   'n': (1, 1),
        '7': (-1, -1), '8': (0, -1), '9': (1, -1),
        '4': (-1, 0),                '6': (1, 0),
        '1': (-1, 1),  '2': (0, 1),  '3': (1, 1),
        '5': (0, 0),   ' ': (0, 0),
        'р': (0, 0),   'p': (0, 0) # Клавиша Р (и P) для пропуска хода
    }
    # Маппинг для прыжка на 2 клетки
    jump_map = {
        'w': (0, -2), 'ц': (0, -2),
        'a': (-2, 0), 'ф': (-2, 0),
        's': (0, 2),  'ы': (0, 2),
        'd': (2, 0),  'в': (2, 0)
    }
    
    if os.name == 'nt':
        import msvcrt
        while True:
            key = msvcrt.getwch()
            if key in ('\xe0', '\x00'):
                arr = msvcrt.getwch()
                if arr == 'H': return (0, -1)
                if arr == 'P': return (0, 1)
                if arr == 'K': return (-1, 0)
                if arr == 'M': return (1, 0)
                return "invalid"
            elif key == '\x03': return None
            else:
                k = key.lower()
                if k in ['q', 'й']: return None
                if k in move_map: return move_map[k]
                if k in jump_map: return jump_map[k]
                return "invalid"
    else:
        import tty, termios
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
            if ch == '\x1b':
                sys.stdin.read(1)
                arr = sys.stdin.read(1)
                if arr == 'A': return (0, -1)
                if arr == 'B': return (0, 1)
                if arr == 'D': return (-1, 0)
                if arr == 'C': return (1, 0)
                return "invalid"
            elif ch == '\x03': return None
            else:
                k = ch.lower()
                if k in ['q', 'й']: return None
                if k in move_map: return move_map[k]
                if k in jump_map: return jump_map[k]
                return "invalid"
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

def main():
    player_x, player_y = 0, 0
    health = 100
    satiety = 50
    dead = False
    won = False
    bars_closed = False
    death_msg = ""
    
    eaten_pickers = 0
    eaten_hunters = 0
    total_yells = 0
    needs_to_poop = False 
    
    has_hunter_book = False
    has_picker_book = False
    library_poem_heard = False
    library_entered = False
    is_red_book_protected = False
    
    turns_since_drunk_seen = 0
    
    grid = {}
    npcs = [] 
    bar_hunters = [] 
    turn_events = []
    error_msg = ""
    
    def play_library_scene():
        clear_screen()
        print("\033[93m" + "="*52)
        print("                Б И Б Л И О Т Е К А")
        print("="*52 + "\033[0m\n")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————\n")
        print("                       🧓")
        print("                     / | \\\n")
        print("                       🐺")
        print("\033[93m" + "="*52 + "\033[0m\n")
        print("  🧓 Библиотекарь: «О, благородный спаситель литературы!")
        print("  Ты вернул наши бесценные тома!")
        print("  В знак бесконечной благодарности я торжественно")
        print("  заношу тебя в Красную книгу (📕)!")
        print("  Отныне ты под охраной закона, и ни один")
        print("  егерь не посмеет поднять на тебя ствол!»")
        time.sleep(10)
        
        clear_screen()
        print("\033[93m" + "="*52)
        print("                Б И Б Л И О Т Е К А")
        print("="*52 + "\033[0m\n")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————\n")
        print("                       🐺🩸")
        print("                 М-м-м, вкусно, как Орбит ")
        print("                   со вкусом библиотекаря!\n")
        print("\033[93m" + "="*52 + "\033[0m\n")
        time.sleep(3)
        
        clear_screen()
        print("\033[93m" + "="*52)
        print("                Б И Б Л И О Т Е К А")
        print("="*52 + "\033[0m\n")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————")
        print(" |📚📚📚📚📚|    |📚📚📚📚📚|    |📚📚📚📚📚|")
        print("  ——————————       ——————————       ——————————\n")
        print("                       🐺")
        print("                       💩\n\n")
        print("\033[93m" + "="*52 + "\033[0m\n")
        time.sleep(2)
        clear_screen()

    def generate_cell(x, y):
        if (x, y) not in grid:
            if x == 0 and y == 0:
                grid[(x, y)] = '..'
                return
            r = random.random()
            if r < 0.25: grid[(x, y)] = '🌲'
            elif r < 0.4: grid[(x, y)] = '🌱'
            elif r < 0.45: grid[(x, y)] = '🍄'
            elif r < 0.47: grid[(x, y)] = '🏠'
            elif r < 0.475: grid[(x, y)] = '🏛️'
            elif r < 0.485: grid[(x, y)] = '🪧'
            elif r < 0.51: grid[(x, y)] = '🎪'
            elif r < 0.55: grid[(x, y)] = '🚩'
            else: grid[(x, y)] = '..'

    def generate_viewport():
        R = 12 
        for y in range(player_y - R, player_y + R + 1):
            for x in range(player_x - R, player_x + R + 1):
                generate_cell(x, y)

    def is_visible(dx, dy):
        return abs(dx) <= 7 and abs(dy) <= 7

    def has_los(x1, y1, x2, y2, limit_to_viewport=True):
        if limit_to_viewport:
            if abs(x1 - player_x) > 7 or abs(y1 - player_y) > 7: return False
            if abs(x2 - player_x) > 7 or abs(y2 - player_y) > 7: return False
            
        if abs(x1 - x2) > 7 or abs(y1 - y2) > 7: return False
        
        dx = x2 - x1
        dy = y2 - y1
        if dx == 0 and dy == 0: return True

        min_x, max_x = min(x1, x2), max(x1, x2)
        min_y, max_y = min(y1, y2), max(y1, y2)
        eps = 1e-7

        for cx in range(min_x, max_x + 1):
            for cy in range(min_y, max_y + 1):
                if (cx, cy) == (x1, y1) or (cx, cy) == (x2, y2):
                    continue

                if grid.get((cx, cy)) in ['🌲', '🏠', '🌱', '💩', '🏛️', '🎪', '🪧'] or any(n['x'] == cx and n['y'] == cy for n in npcs):
                    t_min = 0.0
                    t_max = 1.0

                    if dx == 0:
                        if x1 <= cx - 0.5 or x1 >= cx + 0.5:
                            continue
                    else:
                        tx1 = (cx - 0.5 - x1) / dx
                        tx2 = (cx + 0.5 - x1) / dx
                        t_min = max(t_min, min(tx1, tx2))
                        t_max = min(t_max, max(tx1, tx2))

                    if dy == 0:
                        if y1 <= cy - 0.5 or y1 >= cy + 0.5:
                            continue
                    else:
                        ty1 = (cy - 0.5 - y1) / dy
                        ty2 = (cy + 0.5 - y1) / dy
                        t_min = max(t_min, min(ty1, ty2))
                        t_max = min(t_max, max(ty1, ty2))

                    if t_min < t_max - eps:
                        return False

        return True

    def eat(entity_type):
        nonlocal health, satiety, eaten_pickers, eaten_hunters
        if entity_type == 'picker': eaten_pickers += 1
        elif entity_type == 'hunter': eaten_hunters += 1
        
        gain = 20
        if health < 100:
            need = 100 - health
            if gain <= need:
                health += gain
                gain = 0
            else:
                health = 100
                gain -= need
        satiety += gain

    def get_direction_to_nearest(npc, target_type):
        def is_valid_step(x, y):
            if abs(x - player_x) > 12 or abs(y - player_y) > 12: return False
            if grid.get((x, y)) in ['🌲', '🏠', '💩', '🏛️', '🎪', '🪧']: return False
            if any(n != npc and n['x'] == x and n['y'] == y for n in npcs): return False
            if x == player_x and y == player_y and target_type != 'wolf': return False
            
            cell = grid.get((x, y), '..')
            if cell == '🚩':
                if npc['type'] == 'picker' and not npc.get('aware'): return False
                if npc['type'] == 'hunter' and not npc.get('is_drunk'): return False
                
            if npc['type'] == 'picker' and not npc['aware']:
                if cell == '🌱': return False
            return True

        def is_target(x, y):
            if target_type == '🍄': return grid.get((x, y)) == '🍄'
            elif target_type == '🏠':
                return any(grid.get((x+dx, y+dy)) == '🏠' for dx, dy in [(0,1), (0,-1), (1,0), (-1,0)])
            elif target_type == 'wolf': return x == player_x and y == player_y
            return False

        if is_target(npc['x'], npc['y']):
            return (0, 0)

        queue = []
        visited = {}
        
        for dx, dy in [(0, -1), (-1, 0), (1, 0), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]:
            nx, ny = npc['x'] + dx, npc['y'] + dy
            if is_valid_step(nx, ny):
                queue.append((nx, ny, (dx, dy), 1))
                visited[(nx, ny)] = {(dx, dy): 1}
                
        min_target_depth = 999
        best_first_steps = set()
        
        while queue:
            cx, cy, first_step, depth = queue.pop(0)
            
            if depth > min_target_depth:
                continue
                
            if is_target(cx, cy):
                min_target_depth = depth
                best_first_steps.add(first_step)
                continue
                
            if depth >= 10:
                continue
                
            for dx, dy in [(0, -1), (-1, 0), (1, 0), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]:
                nx, ny = cx + dx, cy + dy
                if is_valid_step(nx, ny):
                    if (nx, ny) not in visited:
                        visited[(nx, ny)] = {}
                    
                    if first_step not in visited[(nx, ny)] or visited[(nx, ny)][first_step] > depth + 1:
                        visited[(nx, ny)][first_step] = depth + 1
                        queue.append((nx, ny, first_step, depth + 1))
                        
        if best_first_steps:
            best_list = list(best_first_steps)
            if target_type == 'wolf':
                old_x, old_y = npc['x'], npc['y']
                npc['x'], npc['y'] = -999, -999
                currently_has_los = has_los(old_x, old_y, player_x, player_y)
                
                if currently_has_los:
                    los_steps = []
                    for step in best_list:
                        nx, ny = old_x + step[0], old_y + step[1]
                        if has_los(nx, ny, player_x, player_y):
                            los_steps.append(step)
                    npc['x'], npc['y'] = old_x, old_y
                    
                    if los_steps:
                        return random.choice(los_steps)
                    else:
                        return (0, 0)
                
                npc['x'], npc['y'] = old_x, old_y
                return random.choice(best_list)
                
            return random.choice(best_list)
            
        return (0, 0)

    def find_drunk_target(drunk_npc):
        candidates = []
        
        if has_los(drunk_npc['x'], drunk_npc['y'], player_x, player_y, limit_to_viewport=False):
            dist = math.hypot(player_x - drunk_npc['x'], player_y - drunk_npc['y'])
            candidates.append({'prio': 1, 'dist': dist, 'type': 'wolf', 'obj': None})
            
        for target in npcs:
            if target is drunk_npc or target.get('dead') or target.get('safe') or target.get('just_spawned'):
                continue
                
            if has_los(drunk_npc['x'], drunk_npc['y'], target['x'], target['y'], limit_to_viewport=False):
                dist = math.hypot(target['x'] - drunk_npc['x'], target['y'] - drunk_npc['y'])
                prio = None
                if target['type'] == 'picker':
                    prio = 2 if target.get('aware') else 3 
                elif target['type'] == 'hunter':
                    prio = 4 if not target.get('is_drunk') else 5 
                    
                if prio is not None:
                    candidates.append({'prio': prio, 'dist': dist, 'type': target['type'], 'obj': target})
                    
        if not candidates:
            return None
            
        min_prio = min(c['prio'] for c in candidates)
        best_prio_candidates = [c for c in candidates if c['prio'] == min_prio]
        
        min_dist = min(c['dist'] for c in best_prio_candidates)
        best_dist_candidates = [c for c in best_prio_candidates if abs(c['dist'] - min_dist) < 1e-6]
        
        return random.choice(best_dist_candidates)

    def render(err="", animations=None):
        clear_screen()
        term_size = shutil.get_terminal_size((80, 24))
        term_width = term_size.columns
        term_height = term_size.lines
        
        display_lines = [
            f"❤️ Здоровье: {health}/100  |  🍖 Сытость: {satiety} {'(ГОЛОД!)' if satiety == 0 else ''}"
        ]
        
        if needs_to_poop:
            display_lines.append("\033[93m💩 НУЖНО НАВАЛИТЬ КУЧУ (WASD)!\033[0m")
            
        display_lines.extend([
            f"🤠 Съедено грибников: {eaten_pickers}  |  👮 Съедено егерей: {eaten_hunters}",
            f"📢 Всего поднято тревог: {total_yells}",
            f"🐾 Ход 1 шаг: Е Н Г / П О / М И Т / Numpad / Стрелки",
            f"⚡ Прыжок 2х: WASD / ЦФЫВ  |  ⏳ Пропуск: Р / Пробел / 5"
        ])
        
        if not library_entered and (library_poem_heard or has_hunter_book or has_picker_book):
            book_status = []
            if has_hunter_book: book_status.append("📘 Егеря")
            if has_picker_book: book_status.append("📗 Грибника")
            if not book_status: book_status.append("(пока нет)")
            display_lines.insert(2, f"📚 Книги: {', '.join(book_status)}")
        
        panic_count = sum(1 for n in npcs if n['type'] == 'picker' and n.get('aware') and not n.get('scared_by_hunter_only'))
        panic_line = ""
        if panic_count > 0:
            panic_line = "\033[91m" + " ".join(["ВОЛК!"] * panic_count) + "\033[0m"
            
        wrapped_feedback = []
        if err:
            wrapped_feedback = textwrap.wrap(err, width=min(term_width, 70))
        else:
            for event in turn_events:
                for line in str(event).split('\n'):
                    wrapped_feedback.extend(textwrap.wrap(line, width=min(term_width, 70)))
            
        R = 7
        grid_width_visual = (2 * R + 1) * 3 - 1
        
        header_height = len(display_lines) + 1 + 1 + len(wrapped_feedback) + 1
        total_height = header_height + (2 * R + 1) + (3 if dead or won else 0)
        v_pad = max(0, (term_height - total_height) // 2)
        h_pad_grid = max(0, (term_width - grid_width_visual) // 2)
        
        print("\n" * v_pad, end="")
        
        for line in display_lines:
            print(" " * max(0, (term_width - visible_len(line)) // 2) + line)
        
        print() 
        
        if panic_count > 0:
            print(" " * max(0, (term_width - visible_len(panic_line)) // 2) + panic_line)
        else:
            print() 
            
        if wrapped_feedback:
            for line in wrapped_feedback:
                print(" " * max(0, (term_width - visible_len(line)) // 2) + line)
        print() 
            
        for y in range(player_y - R, player_y + R + 1):
            row_cells = []
            for x in range(player_x - R, player_x + R + 1):
                if not is_visible(x - player_x, y - player_y):
                    row_cells.append("  ")
                    continue
                
                anim_char = None
                if animations:
                    for anim in animations:
                        if anim['x'] == x and anim['y'] == y: anim_char = anim['char']
                
                if anim_char:
                    row_cells.append(anim_char)
                elif dead and x == player_x and y == player_y:
                    row_cells.append("💀")
                elif not dead and x == player_x and y == player_y:
                    row_cells.append("🐺")
                else:
                    npc_here = next((n for n in npcs if n['x'] == x and n['y'] == y), None)
                    if npc_here:
                        if npc_here['type'] == 'picker':
                            row_cells.append('😱' if npc_here.get('aware') else '🤠')
                        else:
                            row_cells.append('🤡' if npc_here.get('is_drunk') else '👮')
                    else:
                        row_cells.append(grid.get((x, y), '..'))
            row_str = " ".join(row_cells)
            print(" " * h_pad_grid + row_str)
            
        if won:
            msg1 = "🎉🎉🎉 Теперь ты в полной безопасности!"
            msg2 = "Ты — хозяин леса! ПОБЕДА! 🎉🎉🎉"
            print()
            print(" " * max(0, (term_width - visible_len(msg1)) // 2) + msg1)
            print(" " * max(0, (term_width - visible_len(msg2)) // 2) + msg2)
            msg3 = "Нажмите любую клавишу для выхода..."
            print(" " * max(0, (term_width - visible_len(msg3)) // 2) + msg3)
        elif dead:
            msg1 = f"💀 ИГРА ОКОНЧЕНА! {death_msg}"
            wrapped_death = textwrap.wrap(msg1, width=min(term_width, 60))
            print()
            for line in wrapped_death:
                print(" " * max(0, (term_width - visible_len(line)) // 2) + line)
            msg2 = "Нажмите любую клавишу для выхода..."
            print(" " * max(0, (term_width - visible_len(msg2)) // 2) + msg2)

    generate_viewport()
    
    while True:
        animations = []
        render(error_msg)
        
        if dead or won:
            get_input()
            break
            
        inp = get_input()
        
        if inp is None: break
        if inp == "invalid": continue
        
        dx, dy = inp
        pooped_this_turn = False
        
        if dx != 0 or dy != 0:
            target_x = player_x + dx
            target_y = player_y + dy
            
            is_jump = (abs(dx) == 2 or abs(dy) == 2)
            
            if is_jump and needs_to_poop:
                px = player_x + dx // 2
                py = player_y + dy // 2
                cell = grid.get((px, py), '..')
                
                if cell in ['..', '🍄'] and not any(n['x'] == px and n['y'] == py for n in npcs):
                    grid[(px, py)] = '💩'
                    needs_to_poop = False
                    pooped_this_turn = True
                    dx, dy = 0, 0
                else:
                    error_msg = "❌ Туда насрать нельзя! Выберите пустую клетку или гриб."
                    continue
                    
            elif is_jump and not needs_to_poop:
                jump_blocked = False
                mid_x = player_x + dx // 2
                mid_y = player_y + dy // 2
                
                if grid.get((mid_x, mid_y)) in ['🌲', '🏠', '🏛️', '🎪', '🚩', '🪧']:
                    jump_blocked = True
                if any(n['x'] == mid_x and n['y'] == mid_y for n in npcs):
                    jump_blocked = True
                
                if jump_blocked:
                    error_msg = "❌ Прыжок заблокирован препятствием!"
                    continue
                    
                dest_cell = grid.get((target_x, target_y), '..')
                if dest_cell in ['🌲', '🏠', '💩', '🏛️', '🎪', '🚩', '🪧']:
                    error_msg = "❌ Туда нельзя! Препятствие."
                    continue
                    
            elif not is_jump:
                dest_cell = grid.get((target_x, target_y), '..')
                
                if dest_cell == '🏛️':
                    if has_hunter_book and has_picker_book and not library_entered:
                        if abs(dx) + abs(dy) == 1:
                            pass 
                        else:
                            error_msg = "❌ В библиотеку можно войти только по прямой (не по диагонали)!"
                            continue
                    else:
                        error_msg = "❌ Туда нельзя! Препятствие (или библиотека закрыта)."
                        continue
                        
                elif dest_cell in ['🌲', '🏠', '💩', '🎪', '🚩', '🪧']:
                    error_msg = "❌ Туда нельзя! Препятствие."
                    continue
                    
                if needs_to_poop and any(n['x'] == target_x and n['y'] == target_y for n in npcs):
                    error_msg = "❌ Сначала нужно навалить кучу от предыдущего съеденного! (WASD)"
                    continue
                    
        error_msg = ""
        turn_events = []

        new_bar_hunters = []
        for bh in bar_hunters:
            ex, ey = bh['entry_x'], bh['entry_y']
            is_occupied = (player_x == ex and player_y == ey) or any(n['x'] == ex and n['y'] == ey for n in npcs)
            if not is_occupied:
                npcs.append({
                    'type': 'hunter', 'is_drunk': True,
                    'x': ex, 'y': ey, 'hp': bh['hp'],
                    'just_spawned': True, 'age': bh['age']
                })
                turn_events.append("🤡 Пьяный егерь вышел из бара на ту же клетку!")
            else:
                new_bar_hunters.append(bh)
        bar_hunters = new_bar_hunters

        if pooped_this_turn:
            turn_events.append("\033[93mВы навалили огромную кучу!\033[0m")

        if dx != 0 or dy != 0:
            target_x = player_x + dx
            target_y = player_y + dy
            dest_cell = grid.get((target_x, target_y), '..')
            
            if dest_cell == '🏛️':
                play_library_scene()
                library_entered = True
                is_red_book_protected = True
                needs_to_poop = False 
                has_hunter_book = False
                has_picker_book = False
                
                gain = 20
                if health < 100:
                    need = 100 - health
                    if gain <= need:
                        health += gain
                        gain = 0
                    else:
                        health = 100
                        gain -= need
                satiety += gain
                
                turn_events.append("\033[92mВы вернулись из библиотеки под защитой Красной книги!\nОсталось только разобраться с алкашами!\033[0m")
                target_x, target_y = player_x, player_y 
                
            player_x, player_y = target_x, target_y
            generate_viewport()
            
            if grid.get((player_x, player_y)) in ['🍄', '🌱']:
                grid[(player_x, player_y)] = '..'
                
            eaten = [n for n in npcs if n['x'] == player_x and n['y'] == player_y]
            sign_bloodied = False
            for n in eaten:
                eat(n['type'])
                if n['type'] == 'picker':
                    turn_events.append("\033[92mВы съели грибника! Приятного аппетита!\033[0m")
                    if has_hunter_book and eaten_pickers >= 5 and not has_picker_book and not library_entered:
                        has_picker_book = True
                        turn_events.append("\033[96mВы нашли книгу грибника (📗)!\033[0m")
                        
                elif n['type'] == 'hunter':
                    if n.get('is_drunk'):
                        turn_events.append("\033[92mВы съели пьяного егеря! Приятного аппетита!\033[0m")
                    else:
                        turn_events.append("\033[92mВы съели егеря! Приятного аппетита!\033[0m")
                    if eaten_hunters >= 1 and not has_hunter_book and not library_entered:
                        has_hunter_book = True
                        turn_events.append("\033[96mВы нашли книгу егеря (📘)!\033[0m")
                        
                npcs.remove(n)
                needs_to_poop = True
                
                for ax, ay in [(0,1), (0,-1), (1,0), (-1,0)]:
                    bx, by = player_x + ax, player_y + ay
                    if grid.get((bx, by)) == '🪧':
                        grid[(bx, by)] = '🪧'
                        sign_bloodied = True
                        
            if sign_bloodied:
                turn_events.append("\033[91mВолк заляпал табличку кровью!\033[0m")
                for npc in npcs:
                    if npc.get('reading_sign_timer', 0) > 0:
                        rx, ry = npc['reading_sign_pos']
                        if grid.get((rx, ry)) == '🪧':
                            npc['reading_sign_timer'] = 0
                            npc['reading_sign_pos'] = None
                            npc['interrupted_this_turn'] = True
                            if not has_los(npc['x'], npc['y'], player_x, player_y):
                                turn_events.append("🤠: Что это было? А, показалось.")

            adj_library = False
            for ax, ay in [(0,1), (0,-1), (1,0), (-1,0)]:
                if grid.get((player_x + ax, player_y + ay)) == '🏛️':
                    adj_library = True
                    break
            
            if adj_library and not library_entered:
                if has_hunter_book and has_picker_book:
                    turn_events.append("\033[95m«Вот спасибо, умный волк!\033[0m\n\033[95mВижу, знаешь в книгах толк.\033[0m\n\033[95mТы теперь друг человека.\033[0m\n\033[95mЗаходи в библиотеку!»\033[0m")
                else:
                    if not library_poem_heard:
                        library_poem_heard = True
                        turn_events.append("\033[35m«Мудаки нас обманули:\033[0m\n\033[35mНам 2 книги не вернули.\033[0m\n\033[35mЧто ты воешь, глупый волк?\033[0m\n\033[35mТы хоть в книгах знаешь толк?»\033[0m")

        for npc in npcs:
            if npc.get('just_spawned'):
                npc['just_spawned'] = False
                npc['age'] += 1
                npc['skip_turn'] = True
            else:
                npc['skip_turn'] = False

        def process_npc_group(group_npcs):
            intended_moves = {}
            for npc in group_npcs:
                if npc.get('skip_turn'):
                    continue
                
                if npc.get('interrupted_this_turn'):
                    npc['interrupted_this_turn'] = False
                    intended_moves[id(npc)] = (npc['x'], npc['y'])
                    continue
                
                ndx, ndy = 0, 0
                if npc['type'] == 'picker':
                    if not npc['aware']:
                        if not npc.get('has_read_sign') and not npc.get('reading_sign_timer'):
                            adj_signs = []
                            for ax, ay in [(0,1), (0,-1), (1,0), (-1,0)]:
                                if grid.get((npc['x']+ax, npc['y']+ay)) == '🪧':
                                    adj_signs.append((npc['x']+ax, npc['y']+ay))
                            if adj_signs:
                                npc['reading_sign_timer'] = 4
                                npc['reading_sign_pos'] = random.choice(adj_signs)
                                npc['has_read_sign'] = True
                        
                        if npc.get('reading_sign_timer', 0) > 0:
                            intended_moves[id(npc)] = (npc['x'], npc['y'])
                            continue
                            
                        if npc['age'] % 2 == 0:
                            ndx, ndy = get_direction_to_nearest(npc, '🍄')
                    else:
                        if any(grid.get((npc['x']+ax, npc['y']+ay)) == '🏠' for ax, ay in [(0,1),(0,-1),(1,0),(-1,0)]):
                            npc['entering_house'] = True
                        else:
                            ndx, ndy = get_direction_to_nearest(npc, '🏠')
                elif npc['type'] == 'hunter':
                    if npc.get('waiting_at_bar'):
                        ndx, ndy = 0, 0
                    elif max(abs(npc['x'] - player_x), abs(npc['y'] - player_y)) <= 1:
                        ndx, ndy = 0, 0
                    else:
                        ndx, ndy = get_direction_to_nearest(npc, 'wolf')
                        if npc['x'] + ndx == player_x and npc['y'] + ndy == player_y:
                            ndx, ndy = 0, 0
                            
                if not npc.get('entering_house'):
                    intended_moves[id(npc)] = (npc['x'] + ndx, npc['y'] + ndy)

            for i, pos_i in list(intended_moves.items()):
                for j, pos_j in list(intended_moves.items()):
                    if i < j:
                        npc_i = next((n for n in group_npcs if id(n) == i), None)
                        npc_j = next((n for n in group_npcs if id(n) == j), None)
                        if npc_i and npc_j and pos_i == (npc_j['x'], npc_j['y']) and pos_j == (npc_i['x'], npc_i['y']):
                            intended_moves[i] = (npc_i['x'], npc_i['y'])
                            intended_moves[j] = (npc_j['x'], npc_j['y'])
                            
            changed = True
            while changed:
                changed = False
                counts = {}
                for i, pos in intended_moves.items(): counts.setdefault(pos, []).append(i)
                for pos, claimants in counts.items():
                    if len(claimants) > 1:
                        changed = True
                        winner = random.choice(claimants)
                        for c in claimants:
                            if c != winner:
                                npc_c = next(n for n in group_npcs if id(n) == c)
                                intended_moves[c] = (npc_c['x'], npc_c['y'])

            for i in intended_moves:
                npc = next(n for n in group_npcs if id(n) == i)
                npc['x'], npc['y'] = intended_moves[i]
                
                cell = grid.get((npc['x'], npc['y']))
                if cell == '🍄': grid[(npc['x'], npc['y'])] = '..'
                if (npc['type'] == 'hunter' or npc['aware']) and cell == '🌱':
                    grid[(npc['x'], npc['y'])] = '..'
                if cell == '🚩':
                    if (npc['type'] == 'picker' and npc.get('aware')) or (npc['type'] == 'hunter' and npc.get('is_drunk')):
                        grid[(npc['x'], npc['y'])] = '..'

        scared_pickers = [n for n in npcs if n['type'] == 'picker' and n.get('aware')]
        process_npc_group(scared_pickers)
        
        unscared_pickers = [n for n in npcs if n['type'] == 'picker' and not n.get('aware')]
        process_npc_group(unscared_pickers)
        
        drunk_hunters_group = [n for n in npcs if n['type'] == 'hunter' and n.get('is_drunk')]
        process_npc_group(drunk_hunters_group)
        
        sober_hunters_group = [n for n in npcs if n['type'] == 'hunter' and not n.get('is_drunk')]
        sober_not_seeing = []
        sober_seeing = []
        for n in sober_hunters_group:
            if n.get('skip_turn'):
                sober_not_seeing.append(n)
                continue
                
            ox, oy = n['x'], n['y']
            n['x'], n['y'] = -999, -999
            if has_los(ox, oy, player_x, player_y):
                sober_seeing.append(n)
            else:
                sober_not_seeing.append(n)
            n['x'], n['y'] = ox, oy
            
        process_npc_group(sober_not_seeing)
        process_npc_group(sober_seeing)
                
        for npc in npcs:
            if npc.get('entering_house'): 
                npc['safe'] = True
                turn_events.append("\033[93mГрибник в домике!\033[0m")
                
                if npc.get('will_complain') and npc.get('scared_by_hunter_only'):
                    if not bars_closed:
                        bars_closed = True
                        turn_events.append("\033[91mГрибник написал жалобу в Рослесхоз! ВСЕ БАРЫ ЗАКРЫТЫ!\033[0m")

        for npc in npcs:
            if npc.get('safe') or npc.get('dead'): continue
            if npc['type'] == 'picker':
                if npc.get('reading_sign_timer', 0) > 0:
                    continue
                if not npc.get('aware') or npc.get('scared_by_hunter_only'):
                    if has_los(npc['x'], npc['y'], player_x, player_y):
                        npc['aware'] = True
                        npc['scared_by_hunter_only'] = False
                        if not npc.get('has_yelled_wolf'):
                            npc['has_yelled_wolf'] = True
                            turn_events.append("📢 Грибник заметил вас и поднял тревогу!")

        sober_hunters = [n for n in npcs if n['type'] == 'hunter' and not n.get('is_drunk') and not n.get('dead') and not n.get('safe')]
        for npc in sober_hunters:
            if npc.get('dead'): continue
            if has_los(npc['x'], npc['y'], player_x, player_y):
                npc['waiting_at_bar'] = False
                if is_red_book_protected:
                    turn_events.append("👮 Егерь: «Бля-я, его ж нельзя убивать...»")
                else:
                    dist = math.hypot(npc['x'] - player_x, npc['y'] - player_y)
                    dmg = round(180 / max(1, dist))
                    health -= dmg
                    ax1 = npc['x'] + round((player_x - npc['x']) / max(1, dist))
                    ay1 = npc['y'] + round((player_y - npc['y']) / max(1, dist))
                    ax2 = player_x + round((npc['x'] - player_x) / max(1, dist))
                    ay2 = player_y + round((npc['y'] - player_y) / max(1, dist))
                    animations.extend([{'x': ax1, 'y': ay1, 'char': '💥'}, {'x': ax2, 'y': ay2, 'char': '🩸'}])
                    turn_events.append(f"💥 Егерь выстрелил в вас! Урон: {dmg}")
            else:
                is_adj_bar = any(grid.get((npc['x'] + ax, npc['y'] + ay)) == '🎪' for ax, ay in [(0,1), (0,-1), (1,0), (-1,0)])
                if is_adj_bar and not bars_closed:
                    if npc.get('waiting_at_bar'):
                        npc['entered_bar'] = True
                        bar_hunters.append({'entry_x': npc['x'], 'entry_y': npc['y'], 'hp': npc.get('hp', 100), 'age': npc['age']})
                        turn_events.append("👮 Егерь зашел в бар!")
                    else:
                        npc['waiting_at_bar'] = True
                else:
                    npc['waiting_at_bar'] = False

        drunk_hunters = [n for n in npcs if n['type'] == 'hunter' and n.get('is_drunk') and not n.get('dead') and not n.get('safe')]
        random.shuffle(drunk_hunters)

        for npc in drunk_hunters:
            if npc.get('dead'): continue
            target_info = find_drunk_target(npc)
            if target_info:
                t_obj = target_info['obj']
                dist = target_info['dist']

                if target_info['type'] == 'wolf':
                    dmg = round(180 / max(1, dist))
                    health -= dmg
                    ax1 = npc['x'] + round((player_x - npc['x']) / max(1, dist))
                    ay1 = npc['y'] + round((player_y - npc['y']) / max(1, dist))
                    ax2 = player_x + round((npc['x'] - player_x) / max(1, dist))
                    ay2 = player_y + round((npc['y'] - player_y) / max(1, dist))
                    animations.extend([{'x': ax1, 'y': ay1, 'char': '💥'}, {'x': ax2, 'y': ay2, 'char': '🩸'}])
                    turn_events.append(f"💥 Пьяный егерь выстрелил в вас! Урон: {dmg}")
                else:
                    dmg = round(180 / max(1, dist))
                    t_obj['hp'] -= dmg
                    ax1 = npc['x'] + round((t_obj['x'] - npc['x']) / max(1, dist))
                    ay1 = npc['y'] + round((t_obj['y'] - npc['y']) / max(1, dist))
                    ax2 = t_obj['x'] + round((npc['x'] - t_obj['x']) / max(1, dist))
                    ay2 = t_obj['y'] + round((npc['y'] - t_obj['y']) / max(1, dist))
                    animations.extend([{'x': ax1, 'y': ay1, 'char': '💥'}, {'x': ax2, 'y': ay2, 'char': '🩸'}])
                    if t_obj['hp'] <= 0:
                        t_obj['dead'] = True
                        if t_obj['type'] == 'picker':
                            turn_events.append("💥 Пьяный егерь убил грибника!")
                        else:
                            turn_events.append("💥 Пьяный егерь убил другого егеря!")
                    else:
                        if t_obj['type'] == 'picker':
                            if t_obj.get('reading_sign_timer', 0) > 0:
                                t_obj['reading_sign_timer'] = 0
                                t_obj['reading_sign_pos'] = None
                                
                            turn_events.append(f"💥 Пьяный егерь выстрелил в грибника! Урон: {dmg}")
                            if not bars_closed:
                                turn_events.append("😱: Я буду жаловаться в Рослесхоз! 😭😭😭")
                            else:
                                turn_events.append("😱: Бары закрыли, а алкаши остались! 😭😭😭")
                                
                            if not t_obj.get('aware'):
                                t_obj['aware'] = True
                                t_obj['scared_by_hunter_only'] = True
                                t_obj['will_complain'] = True
                            elif t_obj.get('scared_by_hunter_only'):
                                t_obj['will_complain'] = True
                                
                            if t_obj['aware'] and t_obj.get('scared_by_hunter_only'):
                                if has_los(t_obj['x'], t_obj['y'], player_x, player_y):
                                    t_obj['scared_by_hunter_only'] = False
                                    if not t_obj.get('has_yelled_wolf'):
                                        t_obj['has_yelled_wolf'] = True
                                        turn_events.append("📢 Грибник заметил вас и поднял тревогу!")
                        else:
                            turn_events.append(f"💥 Пьяный егерь выстрелил в другого егеря! Урон: {dmg}")
                            if t_obj.get('is_drunk'):
                                turn_events.append("🤡: Оп, маслину поймал, гы-гы-гы!")
                            else:
                                turn_events.append("👮: Ты куда стреляешь, придурок?")

        for npc in npcs:
            if npc.get('safe') or npc.get('dead'): continue
            if npc['type'] == 'picker' and npc.get('reading_sign_timer', 0) > 0:
                timer = npc['reading_sign_timer']
                if timer == 4:
                    turn_events.append('*🤠 читает: "Осторожно, волк-людоед!.."')
                elif timer == 3:
                    turn_events.append('*🤠 читает: "...В этом лесу водится опасный хищник,.."')
                elif timer == 2:
                    turn_events.append('*🤠 читает: "...который съел не одну сотню грибников..."')
                elif timer == 1:
                    turn_events.append('*🤠 читает: "...Что тебе дороже: грибы или жизнь?"')
                
                npc['reading_sign_timer'] -= 1
                if npc['reading_sign_timer'] <= 0:
                    npc['reading_sign_pos'] = None

        fleeing_pickers = 0
        for npc in npcs:
            if npc.get('safe') or npc.get('dead'): continue
            if npc['type'] == 'picker' and npc['aware'] and not npc.get('scared_by_hunter_only'):
                total_yells += 1
                fleeing_pickers += 1

        if fleeing_pickers == 1:
            turn_events.append("📢 Испуганный грибник бежит к домику или не знает, куда бежать!")
        elif fleeing_pickers > 1:
            turn_events.append("📢 Испуганные грибники бегут к домику или не знают, куда бежать!")

        npcs = [n for n in npcs if not n.get('safe') and not n.get('dead') and not n.get('entered_bar')]
        for n in npcs:
            n['age'] += 1

        if animations:
            render(error_msg, animations)
            time.sleep(0.5)

        rim_cells = set()
        for i in range(-7, 8):
            rim_cells.add((player_x + i, player_y - 7))
            rim_cells.add((player_x + i, player_y + 7))
            rim_cells.add((player_x - 7, player_y + i))
            rim_cells.add((player_x + 7, player_y + i))
            
        for rx, ry in rim_cells:
            if grid.get((rx, ry)) == '..' and not any(n['x'] == rx and n['y'] == ry for n in npcs):
                r = random.random()
                if r < 0.005:
                    npcs.append({'type': 'picker', 'x': rx, 'y': ry, 'hp': 100, 'aware': False, 'just_spawned': True, 'age': 0})
                elif r < 0.005 + (0.001 * total_yells):
                    npcs.append({'type': 'hunter', 'x': rx, 'y': ry, 'hp': 100, 'just_spawned': True, 'age': 0})

        if health <= 0 and not dead:
            dead = True
            
        died_of_starvation = False
        if not dead:
            if satiety > 0:
                satiety -= 1
            else:
                health -= 1
                turn_events.append("🩸 Вы голодаете! Здоровье падает.")
                if health <= 0:
                    dead = True
                    died_of_starvation = True
                    
        # Отслеживание появления пьяных егерей в зоне видимости (R=7) для условия победы
        drunk_in_viewport = any(
            n['type'] == 'hunter' and n.get('is_drunk') and not n.get('dead') and not n.get('safe')
            and abs(n['x'] - player_x) <= 7 and abs(n['y'] - player_y) <= 7
            for n in npcs
        )
        if drunk_in_viewport:
            turns_since_drunk_seen = 0
        else:
            turns_since_drunk_seen += 1
                    
        if dead and not death_msg:
            if died_of_starvation:
                death_msg = "Ты решил стать веганом?"
            elif eaten_pickers == 0 and eaten_hunters == 0:
                death_msg = "Ты не волк, а жалкий щенок!"
            elif eaten_pickers > 0 and eaten_hunters == 0:
                death_msg = "Что, волчок, только безоружных можешь кушать?"
            elif eaten_hunters > 0:
                death_msg = "Ты храбро сражался и не обосрался (в переносном смысле)"

        if not dead and library_entered and bars_closed:
            has_drunks = any(n['type'] == 'hunter' and n.get('is_drunk') and not n.get('dead') and not n.get('safe') for n in npcs)
            if not has_drunks and turns_since_drunk_seen > 10:
                won = True

if __name__ == "__main__":
    main()