Warning: Undefined array key 0 in /var/www/tgoop/function.php on line 65

Warning: Trying to access array offset on value of type null in /var/www/tgoop/function.php on line 65
- Telegram Web
Telegram Web
Добавил в читы кнопку "idkfa" на работе. Тестить без ней - как пить борщ без сметана. Нравица!

#work
Закончил читать Extreme programming explained от Kent Beck. Вторая половина книжки сплошная вода, а вот начало было приятно читать. Трудно комментировать, потому что полноценное парное программирование я никогда не пробовал, хотя очень хотелось бы. Вообще, я прям верю словам книжки, что это реально гораздо эффективнее, чем работать поодиночке. Несмотря на то, что абсолютно никто нигде так не работает. Просто те редкие дискорд-созвоны, когда за полчаса совместного обсуждения и просмотра кода удаётся найти причину бага, которую до этого безуспешно искал целый день - приносят огромное удовольствие и облегчение)

Хочется верить, что в Valve именно экстремальное программирование, и их пример доказывает, что можно в 300-400 человек делать и ААА игры, и стим с деком.

Вообще, очень жду Code with me для Rider. Думаю, когда он выйдет, всё-таки получится уломать начальство хотя бы попробовать. Но, похоже, AI агенты всё-таки быстрее появятся)

#comment
Если бы существовал конкурс пиратов, то я взял бы гран-при

#everythingelse
Не выходит из головы история, услышанная на работе. У людей мета-сервер на nakama, и общую логику между клиентом(C#) и сервером(go) они пишут на TypeScript, который в юньку перегоняют через jint. И, вообщем-то, изврат конечно, но что ещё остаётся? Playfab уже давно не конкурент, какие-нибудь azure functions выйдут дороже, да и там шаредлогику тяжело сделать.

Я ради шаредлогики не пошёл бы на такое. Не считаю большой проблемой дублирование логики на C# и go - инфраструктура под шаред логику сама по себе немножко уродует архитектуру. Но писать общую на промежуточном TypeScript - худший вид компромисса.

И тут как раз релиз SpacetimeDB, которая на словах прям серебряная пуля.

#everythingelse
Слегка зарубился с коллегой по поводу использования ключевого слова default в C#. Он считает default более понятным/читаемым, чем null. Хотя default даже пишется длиннее!

Противоречие возникло из такого кода:
public bool HasEquip() => _equip != default;

Мне кажется верхний вариант хуже, чем нижний:
public bool HasEquip() => _equip != null;


Проблему я вижу в двусмысленности default - это и значение по умолчанию (для struct), и отсутствие значения (для class). Из-за этого приходится смотреть на тип переменной, чтобы определить точную семантику.

Потому что если не смотреть, то может возникнуть баг, когда у структуры переопределен оператор сравнения:
private Vector2 _point;
public bool HasPoint() => _point != default;

В этом случае HasPoint будет неправильно работать для Vector2.Zero. Но коллегу это не убедило. В большинстве файлов у нас #nullable enable и такие сравнения просто не скомпилятся.

А вы что предпочитаете default или null?

#codestyle
Я из тех, кто расстроился от решения Microsoft по выбору Go. В черновик написал простыню о том, что в таких решениях нужно руководствоваться не прагматизмом, а долгосрочными стратегическими планами. Спустил пар, обругал всех топ-менеджеров.

Но потом разобрался и успокоился.

#comment
Media is too big
VIEW IN TELEGRAM
Записал 15-минутный ролик о ключевом слове default и стиле кода, связанном с ним :)

С помощью такой презентации получилось убедить коллег на работе не юзать default в качестве alias для null :)

#codestyle
На работе опять прикол - уговаривали серверного программиста всей командой фичи не дублировать настройки :)

Суть в чём - у нас есть новый тип шмотки, и у неё 5 рангов прокачки. На каждом ранге - список параметров + абилок. У нас и абилки, и параметры имеют общий базовый тип ParameterEntry.

Формат настроек примерно такой:
message Antique {
required int32 id = 1;
repeated AntiqueRankSettings ranksSettings = 2;
}

message AntiqueRankSettings {
required int32 rank = 1;
repeated ParameterEntry parameters = 2; // общий список слотов и абилок
}



И на первом ранге - свои статы + только один слот с абилкой, на втором ранге - улучшенные статы + первый слот + добавляется второй, на третьем ранге - три слота, на четвёртом - четыре, а на пятом - четыре слота абилок превращаются в более крутые версии. Описанные выше настройки позволяли всё это настроить целиком для рассчётов в бою.

Но фишка в том, что слот с абилкой - на самом деле не единичная абилка, а группа абилок. Т.е. у нас в слоте может быть что-то такое, например: раз в 5 секунд наносит всем урон огнём, раз в 3 секунды накладывает на себя щит, после уклонения с шансом 20% может начать атаковать по две цели за раз.

Соответственно на клиенте чтобы правильно рисовать описание этих слотов нужно знать к какому слоту какие абилки относятся. И протокол настроек нужен скорее такой:
m
essage AntiqueRankSettings {  
required int32 rank = 1;
repeated ParameterEntry parameters = 2; // по содержимому возник спор - нужно ли тут оставлять абилки?
repeated int32 abilitySlotIds = 3; // тут сгруппированные абилки. это ClientOnly или общее?
}

message AbilitySlot
{
required int32 id = 1;
repeated ParameterEntry abilities = 2; // тут только абилки
}

Соответственно в AntiqueRankSettings.parameters останутся только статы, а все абилки переедут в слоты. Но сервер против того, чтобы убирать абилки из parameters, потому что технически - неважно сколько у тебя слотов и как по ним сгруппированы абилки- важен только сам финальный список абилок. Т.е. это изменение по мнению сервера - чисто для визуала, поэтому сервер предлагает сделать abilitySlotIds чисто ClientOnly полем, и соответственно продублировать все абилки в двух местах(parameters и abilities), а не перенести их. Потому что это по его мнению бизнес-логика не меняется от того, как мы отображаем на клиенте этот список - группами внутри слотов или единым списком.

И вот на решение этого философского вопроса делали собрание: эти слоты с абилками - чисто визуальная вещь, или это всё-таки бизнес-логика? В итоге о счётом 2 (клиент + ГД) : 1 (сервер) получилось продавить на отсутствие дублирования. Самый прикол в том, что для него технически было проще сделать SelectMany, чем писать валидацию на проверку дублирования и всего-такого, но он крепко стоял именно за свою идеологическую позицию. Так что философский вопрос остался не закрытым.

#work
На работе пару дней увлечённо вайб-кодил рослин анализатор для поиска неиспользуемых публичных пропертей. Клауд, конечно, крут, но ежедневный лимит слишком мал для реального использования, chatGPT в последнее время вообще перестал генерить что-то выше джуна, deepseek всегда был слаб, а вот grok 3 прям порадовал - в режиме "обоснуй" хоть и тупит по 5 минут, но даже без этого режима код правильный!

Grok смог объяснить мне, что анализатор тут не подходит, потому что анализ необходим на уровне солюшена, а не внутри проекта, так что тут только внешняя утилита, которую он написал практически с первой попытки. При этом я общался с ним прям очень долго - и он ни разу не забывал контекст в отличии клауда и chatGPT. Мой новый основной AI, походу.

#work #tools
Ух ты, а я и не знал, что Стив Балмер был настолько харизматичным мужиком.

https://www.youtube.com/watch?v=_WW2JWIv6G8

https://www.youtube.com/watch?v=8fcSviC7cRM

https://www.youtube.com/watch?v=CYKFcwrHmi0

Может это у меня какое-то искажение, но я всегда как-то свысока смотрел на разработчиков TypeScript, Go, Java, C++, Python и других - вы, конечно, очень популярные и востребованные, но C# - это рок-н-ролл. И теперь у меня есть доказательства :)

#everythingelse
[1/2]

Всю неделю вайбкодил - в windsurf, jetbrains ai assistant и copilot. Cursor не использовал, потому что он у меня как-то сломался и даже переустановка не помогает) Но я перепробовал абсолютно все остальные инструменты в комбинации со всеми моделями, потратив все триальные токены вот на такую задачу:

"Я поменял кодогенерацию в своём проекте: раньше у меня генерились пары полей bool HasField {get;} и T Field {get;}, а я заменил их на единичное nullable поле T? Field {get;}. Соответственно мне нужно заменить все использования HasField на сравнение Field с null. При этом иногда T - структура или примитивный тип, а иногда - класс. Давай исправим все ошибки компиляции, которые возникли из-за этого изменения кодогена.

Has заменяй на сравнение с null и вставляй .Value для примитивных типов и структур.
Типы bool никогда не nullable, так что у них Has заменяй просто на саму переменную

Вызов AbilityExtensions.GetValueAsNullable заменяй на использование самой переменной (она Nullable). Например var x = AbilityExtensions.GetValueAsNullable(q.HasQQ, q. QQ); меняется на var x = q.QQ;

Также я убрал поля FieldNullOrEmpty, вместо него обращайся напрямую к полю FieldList и сравнивай Count с 0. Не нужно сравнивать FieldList с null, потому что он никогда не null. Например, if (!x.QNullOrEmpty) меняется на if (x.QList.Count != 0)

Если ошибка в использовании поля, которое nullable, то заменяй на field ?? 0.

Делай правки строго в одну строчку, только саму ошибку - не нужно менять имена переменных, типы и так далее. Не добавляй никаких комментариев в код, только исправления.

Вот текущий список ошибок компиляции: (список ошибок из юнити консоли)
".

Результат оказался разочаровывающим.

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

Из удивительного: Windsurf оказался лучше jetbrains и microsoft. Только он реально понимает строчку в которой ошибка, и берёт не весь файл в контекст, а только пять строчек до ошибки и пять строчек после - соответственно он генерит реально только исправления в нужном месте, а не весь файл заново. Но и он не понимает место в строчке, в которой ошибка. И поэтому такие места, как

int x = _x1 + _x2 +_x3 + _x4;

с ошибкой (100,9): error CS0266: Cannot implicitly convert type 'int?' to 'int'. An explicit conversion exists (are you missing a cast?) никто из агентов не способен исправить на

int x = _x1 + _x2 + (_x3 ?? 0) + _x4;

И это не только потому, что он у них трудности со счётом символов, но ещё и потому, что у них нет синтаксического дерева в контексте, а также возможностей узнавать тип переменных, указанных в другом файле.

Ещё меня максимально удивило, что лучшей моделью оказалась бесплатная моделька cascade из windsurf. Только она реально делала только нужные исправления - не внося случайных улучшений или комментариев в код, не добавляя комментариев, не переименовывая переменные и т.д. как все остальные.

#work #tools
[2/2]

И вот ещё: делать такие AI инструменты поверх редактора кода - неудобный подход. На текущим этапе вайбкодинг должен быть надстройкой не над редактором, а над крутым инструментом просмотра диффов.

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

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

#work #tools
Разрекламировал на работе переход на LitMotion с PrimeTween, потому что там "лучше" api + есть инспектор, с которым будет возможно верстальщикам делать разные стейты юая(ну типа твинами с 0 duration). И при демонстрации тут же столкнулся с багом либы. И есть такое подозрение, что его и не пофиксят, и пул реквест не примут, хотя одним из пунктов моей агитации было наличие более активного разраба и нормальной лицензии. Мдя :(

#work
This media is not supported in your browser
VIEW IN TELEGRAM
[1/2]

30 апреля на работе устроили хакатон. Задача была такая: есть чёрно-белая картинка (массив размером width × height из 0 и 1), и нужно угадать её за минимальное число попыток. После каждой попытки говорят, сколько точек угадано правильно, но не их позиции.

Задача крутая, я таких в интернете не нашёл. Даже ИИ с первого раза решение не выдаёт, а подходов к решению много. Время было с 11 утра до 17 вечера. Я сначала попробовал сложный метод с теоремой Байеса (пересчитывал вероятности для каждой клетки после попытки), но к 15 понял, что это не работает. В итоге написал простое решение с перебором и парой оптимизаций. К дедлайну только я сдал рабочее решение (ещё одно было, но оно крашилось). Остальные не успели отладить свои сложные алгоритмы. Но организаторы решили, что присудить мне победу "несправедливо" из-за малого числа решений, и дали ещё выходные на доработку.

#work
[2/2]

В итоге появилось 10 решений, и я занял третье место с новым алгоритмом, получив 10к призовых. Бюджет оказался больше, чем я ожидал, так что я доволен, но обидно что не получил доплату за то, что единственный предложил рабочий алгоритм в первоначальный срок. Мой итоговый алгоритм был самым сложным, но эта сложность только ухудшила результат.

Первые два места заняли решения с бинарным поиском. Я думал, что моё "адаптивное деление" (делю сегменты не пополам, а на n/(max(k, 2)), где k — число единиц в области) будет лучше, но оно оказалось хуже. Без этой "оптимизации" я всё равно был бы третьим, потому что решал задачу на одномерном массиве со "спиральной" конвертацией, а победители делили прямоугольники пополам, что лучше подходило под тесты.

Кстати, код я почти не писал — всё делал через Grok. Но его рекурсивное решение не подходило под API проверки, и он не мог без ошибок переписать рекурсию в цикл. Пришлось оставить рекурсию, но запускать её асинхронно в фоне, а для проверки результата использовать TaskCompletionSource, такой вот хак.

#work
Сегодня полдня потратил на настройку ci сборки проекта с PrimeTween. Мне он нужен с дефайном PRIME_TWEEN_EXPERIMENTAL, но проблема в том, что юнити сначала подключает и компилирует пакеты, а потом уже с дефайнами компилирует саму игру. В результате были ошибки компиляции из-за ненайденных методов.

А чтобы пакеты перекомпилировались с дефайнами проекта нужно или переоткрыть юнити, или сделать SetDirty для ProjectSettings.asset. Оба эти варианты не подходят для сборки скриптом.

В итоге нашёл выход - засунул этот PRIME_TWEEN_EXPERIMENTAL в сам asmdef праймтвина. А этот баг с порядком применения дефайнов, как подсказывает chatGPT, не поправлен даже в последних версиях юнити.

#work
Всю прошлую неделю (или даже две) на работе дебажил боёвку после этого рефакторинга. Благодаря этому наконец-то хоть чуть-чуть начал разбираться в её коде.

Что круто - сам расчёт боёвки по сути просто метод с сигнатурой

IReadOnlyList<BattleTurn> ProcessBattle(BattleInitialState state);

И это чистая функция, т.е. внутри state уже есть random generation seed, так что на один стейт всегда один и тот же результат. Благодаря этому можно легко искать расхождения при апдейтах - просто сравнивать сериализацию списка BattleTurn от старой версии и от новой.

В моём случае расхождения появлялись, например, на каком-нибудь семитысячном элементе BattleTurn на десятой секунде боя. Как искать в таком случае баг?

Тут здорово помогло то, что у нас свой самописный генератор случайных чисел (потому что сервер на Java, и надо чтобы с одинаковыми сидами был одинаковый результат). Вижу, например, что в мастере у меня рандом выдаёт значение 0.87, а в ветке 0.76 - при этом всё остальное одинаковое. Как тут быть? Завёл внутри генератора случайных чисел List<(int Index, string StackTrace, object Parameters)> _generations; в который сохранял все его использования. Благодаря этому я вижу, что 0.87 сгенерилось при Index == 100500, а 0.76 - с Index 100503, значит в ветке было где-то три лишних вызова генератора. Сравниваю последние значения _generations и нахожу первое расхождение. Потом ставлю conditional брекпоинт с индексом первого различающегося вызова генератора и начинаю искать ошибку.

Допустим вижу, что расхождение в вызове из какого-нибудь класса AbsorbShieldAbility - в мастере вызывается генерация рандома с ренджа 100-200, а в ветке - 150-250. Но вот проблема, эти параметры ренджа задаются в конструкторе AbsorbShieldAbility, который был вызван неизвестно когда, и вообще у нас этих AbsorbShieldAbility тысячи создаются в процессе боёвки да ещё и в рекурсии. Как отлаживать дальше?

Создаём в конструкторе AbsorbShieldAbility айдишник - _guid = new Guid();

После этого в момент вызова "неправильного" рандома смотрим айдишник класса и перезапускаем дебаг с conditional брекпоинтом внутри конструктора AbsorbShieldAbility с нужным айдишником. И смотрим дальше - почему у нас неправильные параметры.

И вот так вот в конце дня наконец находишь баг в какой-нибудь строчке if (Value == 0)

Проблема в том, что раньше было bool HasValue и int Value, но HasValue не проверялся, использовалась проверка на 0 как на отсутствие значение по умолчанию. А теперь просто int? Value, и null эту проверку не проходит.

И соответственно теперь должно быть if (Value is null or 0).

Upd. Стандартный Гуид юзать нельзя, потому что значения между сессиями могут отличаться. Я юзал id=staticId++; но у этого есть минус, что нужно перезапускать плеймод после каждого прогона боя.

#work
Когда код был написан раньше, чем научились программировать на Unity =)

#work
Супер-клёвый способ писать музыку кодом, да ещё и в браузере - Штрудель.

#tools #games
2025/06/26 06:20:01
Back to Top
HTML Embed Code: