Закончил читать Extreme programming explained от Kent Beck. Вторая половина книжки сплошная вода, а вот начало было приятно читать. Трудно комментировать, потому что полноценное парное программирование я никогда не пробовал, хотя очень хотелось бы. Вообще, я прям верю словам книжки, что это реально гораздо эффективнее, чем работать поодиночке. Несмотря на то, что абсолютно никто нигде так не работает. Просто те редкие дискорд-созвоны, когда за полчаса совместного обсуждения и просмотра кода удаётся найти причину бага, которую до этого безуспешно искал целый день - приносят огромное удовольствие и облегчение)
Хочется верить, что в Valve именно экстремальное программирование, и их пример доказывает, что можно в 300-400 человек делать и ААА игры, и стим с деком.
Вообще, очень жду Code with me для Rider. Думаю, когда он выйдет, всё-таки получится уломать начальство хотя бы попробовать. Но, похоже, AI агенты всё-таки быстрее появятся)
#comment
Хочется верить, что в Valve именно экстремальное программирование, и их пример доказывает, что можно в 300-400 человек делать и ААА игры, и стим с деком.
Вообще, очень жду Code with me для Rider. Думаю, когда он выйдет, всё-таки получится уломать начальство хотя бы попробовать. Но, похоже, AI агенты всё-таки быстрее появятся)
#comment
www.twirpx.org
Скачать Kent Beck. Extreme Programming Explained [PDF]
Прежде считалось, что чем позднее мы вводим в программный продукт новый функционал, тем дороже это обходится. Книга от создателя экстремального программирования XP , одного из направлений гибкой методологии разработки agile methodology , полностью опровергает…
Не выходит из головы история, услышанная на работе. У людей мета-сервер на nakama, и общую логику между клиентом(C#) и сервером(go) они пишут на TypeScript, который в юньку перегоняют через jint. И, вообщем-то, изврат конечно, но что ещё остаётся? Playfab уже давно не конкурент, какие-нибудь azure functions выйдут дороже, да и там шаредлогику тяжело сделать.
Я ради шаредлогики не пошёл бы на такое. Не считаю большой проблемой дублирование логики на C# и go - инфраструктура под шаред логику сама по себе немножко уродует архитектуру. Но писать общую на промежуточном TypeScript - худший вид компромисса.
И тут как раз релиз SpacetimeDB, которая на словах прям серебряная пуля.
#everythingelse
Я ради шаредлогики не пошёл бы на такое. Не считаю большой проблемой дублирование логики на C# и go - инфраструктура под шаред логику сама по себе немножко уродует архитектуру. Но писать общую на промежуточном TypeScript - худший вид компромисса.
И тут как раз релиз SpacetimeDB, которая на словах прям серебряная пуля.
#everythingelse
Heroic Labs
Nakama: The leading open source game server for studios and publishers
Heroic Labs | Heroic Game Stack and Cloud Platform
Слегка зарубился с коллегой по поводу использования ключевого слова default в C#. Он считает default более понятным/читаемым, чем null. Хотя default даже пишется длиннее!
Противоречие возникло из такого кода:
Мне кажется верхний вариант хуже, чем нижний:
Проблему я вижу в двусмысленности default - это и значение по умолчанию (для struct), и отсутствие значения (для class). Из-за этого приходится смотреть на тип переменной, чтобы определить точную семантику.
Потому что если не смотреть, то может возникнуть баг, когда у структуры переопределен оператор сравнения:
В этом случае HasPoint будет неправильно работать для Vector2.Zero. Но коллегу это не убедило. В большинстве файлов у нас #nullable enable и такие сравнения просто не скомпилятся.
А вы что предпочитаете default или null?
#codestyle
Противоречие возникло из такого кода:
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
Но потом разобрался и успокоился.
#comment
Telegram
yet another dev
Уже вторую неделю встречаю комментарии в духе: «Если C# такой быстрый и хороший, то почему Microsoft переписала компилятор TypeScript на Go, а не на C#?»
Среди множества спекуляций на эту тему мне попался один комментарий на Хабре, который, как мне кажется…
Среди множества спекуляций на эту тему мне попался один комментарий на Хабре, который, как мне кажется…
Media is too big
VIEW IN TELEGRAM
Записал 15-минутный ролик о ключевом слове default и стиле кода, связанном с ним :)
С помощью такой презентации получилось убедить коллег на работе не юзать default в качестве alias для null :)
#codestyle
С помощью такой презентации получилось убедить коллег на работе не юзать default в качестве alias для null :)
#codestyle
На работе опять прикол - уговаривали серверного программиста всей командой фичи не дублировать настройки :)
Суть в чём - у нас есть новый тип шмотки, и у неё 5 рангов прокачки. На каждом ранге - список параметров + абилок. У нас и абилки, и параметры имеют общий базовый тип ParameterEntry.
Формат настроек примерно такой:
И на первом ранге - свои статы + только один слот с абилкой, на втором ранге - улучшенные статы + первый слот + добавляется второй, на третьем ранге - три слота, на четвёртом - четыре, а на пятом - четыре слота абилок превращаются в более крутые версии. Описанные выше настройки позволяли всё это настроить целиком для рассчётов в бою.
Но фишка в том, что слот с абилкой - на самом деле не единичная абилка, а группа абилок. Т.е. у нас в слоте может быть что-то такое, например: раз в 5 секунд наносит всем урон огнём, раз в 3 секунды накладывает на себя щит, после уклонения с шансом 20% может начать атаковать по две цели за раз.
Соответственно на клиенте чтобы правильно рисовать описание этих слотов нужно знать к какому слоту какие абилки относятся. И протокол настроек нужен скорее такой:
m
Соответственно в AntiqueRankSettings.parameters останутся только статы, а все абилки переедут в слоты. Но сервер против того, чтобы убирать абилки из parameters, потому что технически - неважно сколько у тебя слотов и как по ним сгруппированы абилки- важен только сам финальный список абилок. Т.е. это изменение по мнению сервера - чисто для визуала, поэтому сервер предлагает сделать abilitySlotIds чисто ClientOnly полем, и соответственно продублировать все абилки в двух местах(parameters и abilities), а не перенести их. Потому что это по его мнению бизнес-логика не меняется от того, как мы отображаем на клиенте этот список - группами внутри слотов или единым списком.
И вот на решение этого философского вопроса делали собрание: эти слоты с абилками - чисто визуальная вещь, или это всё-таки бизнес-логика? В итоге о счётом 2 (клиент + ГД) : 1 (сервер) получилось продавить на отсутствие дублирования. Самый прикол в том, что для него технически было проще сделать SelectMany, чем писать валидацию на проверку дублирования и всего-такого, но он крепко стоял именно за свою идеологическую позицию. Так что философский вопрос остался не закрытым.
#work
Суть в чём - у нас есть новый тип шмотки, и у неё 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
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
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
YouTube
Steve Ballmer Runs Around Like A Maniac On Stage (Motivational Presentation)
http://www.ebookpublishingschool.com
Steve Ballmer is worth $16 Billion.
Get more motivation and internet marketing tips on the blog: http://onlineinternetmarketinghelp.com/
Steve Ballmer is worth $16 Billion.
Get more motivation and internet marketing tips on the blog: http://onlineinternetmarketinghelp.com/
[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. Только он реально понимает строчку в которой ошибка, и берёт не весь файл в контекст, а только пять строчек до ошибки и пять строчек после - соответственно он генерит реально только исправления в нужном месте, а не весь файл заново. Но и он не понимает место в строчке, в которой ошибка. И поэтому такие места, как
с ошибкой (100,9): error CS0266: Cannot implicitly convert type 'int?' to 'int'. An explicit conversion exists (are you missing a cast?) никто из агентов не способен исправить на
i
Ещё меня максимально удивило, что лучшей моделью оказалась бесплатная моделька cascade из windsurf. Только она реально делала только нужные исправления - не внося случайных улучшений или комментариев в код, не добавляя комментариев, не переименовывая переменные и т.д. как все остальные.
#work #tools
Всю неделю вайбкодил - в 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?) никто из агентов не способен исправить на
i
nt x = _x1 + _x2 + (_x3 ?? 0) + _x4;
И это не только потому, что он у них трудности со счётом символов, но ещё и потому, что у них нет синтаксического дерева в контексте, а также возможностей узнавать тип переменных, указанных в другом файле.Ещё меня максимально удивило, что лучшей моделью оказалась бесплатная моделька cascade из windsurf. Только она реально делала только нужные исправления - не внося случайных улучшений или комментариев в код, не добавляя комментариев, не переименовывая переменные и т.д. как все остальные.
#work #tools
Windsurf
Windsurf (formerly Codeium) - The most powerful AI Code Editor
Windsurf (formerly Codeium) is the world's most advanced AI coding assistant for developers and enterprises. Windsurf Editor — the first AI-native IDE that keeps developers in flow.
[2/2]
И вот ещё: делать такие AI инструменты поверх редактора кода - неудобный подход. На текущим этапе вайбкодинг должен быть надстройкой не над редактором, а над крутым инструментом просмотра диффов.
MCP для юнити, кстати, не получилось применить, потому что там невозможно запустить компиляцию, дождаться её окончания, и получить список ошибок. Из функционала там только редактирование сцены.
Но вообще, если не верить в то, что AGI появится через пару лет и ещё через три нас уничтожит, то более удобный пайплайн для крупных рефакторингов на ближайшее время - это иметь локальную ллмку, которую юзать через скрипт - самому давать ей в контекст нужное место в файле, текст ошибки, промпт инструкций и контекст на старый и новый тип поля, вызывающего ошибку. И вот так вот в цикле запускать её с нуля на каждую ошибку, пока она всё не поправит.
#work #tools
И вот ещё: делать такие AI инструменты поверх редактора кода - неудобный подход. На текущим этапе вайбкодинг должен быть надстройкой не над редактором, а над крутым инструментом просмотра диффов.
MCP для юнити, кстати, не получилось применить, потому что там невозможно запустить компиляцию, дождаться её окончания, и получить список ошибок. Из функционала там только редактирование сцены.
Но вообще, если не верить в то, что AGI появится через пару лет и ещё через три нас уничтожит, то более удобный пайплайн для крупных рефакторингов на ближайшее время - это иметь локальную ллмку, которую юзать через скрипт - самому давать ей в контекст нужное место в файле, текст ошибки, промпт инструкций и контекст на старый и новый тип поля, вызывающего ошибку. И вот так вот в цикле запускать её с нуля на каждую ошибку, пока она всё не поправит.
#work #tools
Windsurf
Windsurf (formerly Codeium) - The most powerful AI Code Editor
Windsurf (formerly Codeium) is the world's most advanced AI coding assistant for developers and enterprises. Windsurf Editor — the first AI-native IDE that keeps developers in flow.
Разрекламировал на работе переход на LitMotion с PrimeTween, потому что там "лучше" api + есть инспектор, с которым будет возможно верстальщикам делать разные стейты юая(ну типа твинами с 0 duration). И при демонстрации тут же столкнулся с багом либы. И есть такое подозрение, что его и не пофиксят, и пул реквест не примут, хотя одним из пунктов моей агитации было наличие более активного разраба и нормальной лицензии. Мдя :(
#work
#work
GitHub
GitHub - annulusgames/LitMotion: Lightning-fast and Zero Allocation Tween Library for Unity.
Lightning-fast and Zero Allocation Tween Library for Unity. - annulusgames/LitMotion
This media is not supported in your browser
VIEW IN TELEGRAM
[1/2]
30 апреля на работе устроили хакатон. Задача была такая: есть чёрно-белая картинка (массив размером width × height из 0 и 1), и нужно угадать её за минимальное число попыток. После каждой попытки говорят, сколько точек угадано правильно, но не их позиции.
Задача крутая, я таких в интернете не нашёл. Даже ИИ с первого раза решение не выдаёт, а подходов к решению много. Время было с 11 утра до 17 вечера. Я сначала попробовал сложный метод с теоремой Байеса (пересчитывал вероятности для каждой клетки после попытки), но к 15 понял, что это не работает. В итоге написал простое решение с перебором и парой оптимизаций. К дедлайну только я сдал рабочее решение (ещё одно было, но оно крашилось). Остальные не успели отладить свои сложные алгоритмы. Но организаторы решили, что присудить мне победу "несправедливо" из-за малого числа решений, и дали ещё выходные на доработку.
#work
30 апреля на работе устроили хакатон. Задача была такая: есть чёрно-белая картинка (массив размером width × height из 0 и 1), и нужно угадать её за минимальное число попыток. После каждой попытки говорят, сколько точек угадано правильно, но не их позиции.
Задача крутая, я таких в интернете не нашёл. Даже ИИ с первого раза решение не выдаёт, а подходов к решению много. Время было с 11 утра до 17 вечера. Я сначала попробовал сложный метод с теоремой Байеса (пересчитывал вероятности для каждой клетки после попытки), но к 15 понял, что это не работает. В итоге написал простое решение с перебором и парой оптимизаций. К дедлайну только я сдал рабочее решение (ещё одно было, но оно крашилось). Остальные не успели отладить свои сложные алгоритмы. Но организаторы решили, что присудить мне победу "несправедливо" из-за малого числа решений, и дали ещё выходные на доработку.
#work
[2/2]
В итоге появилось 10 решений, и я занял третье место с новым алгоритмом, получив 10к призовых. Бюджет оказался больше, чем я ожидал, так что я доволен, но обидно что не получил доплату за то, что единственный предложил рабочий алгоритм в первоначальный срок. Мой итоговый алгоритм был самым сложным, но эта сложность только ухудшила результат.
Первые два места заняли решения с бинарным поиском. Я думал, что моё "адаптивное деление" (делю сегменты не пополам, а на n/(max(k, 2)), где k — число единиц в области) будет лучше, но оно оказалось хуже. Без этой "оптимизации" я всё равно был бы третьим, потому что решал задачу на одномерном массиве со "спиральной" конвертацией, а победители делили прямоугольники пополам, что лучше подходило под тесты.
Кстати, код я почти не писал — всё делал через Grok. Но его рекурсивное решение не подходило под API проверки, и он не мог без ошибок переписать рекурсию в цикл. Пришлось оставить рекурсию, но запускать её асинхронно в фоне, а для проверки результата использовать TaskCompletionSource, такой вот хак.
#work
В итоге появилось 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
А чтобы пакеты перекомпилировались с дефайнами проекта нужно или переоткрыть юнити, или сделать 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
Что круто - сам расчёт боёвки по сути просто метод с сигнатурой
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
YouTube
Hustle Castle TOP1 fight: zoza (23 mil. Vs. Top1 player in the world (29 mil.) - January 2021
Увы, в LitMotion ноль реакции на issue, как и ожидал. Посмотрим, как быстро отреагирует на баг PrimeTween.
#everythingelse
#everythingelse
Telegram
NuclearBand
Разрекламировал на работе переход на LitMotion с PrimeTween, потому что там "лучше" api + есть инспектор, с которым будет возможно верстальщикам делать разные стейты юая(ну типа твинами с 0 duration). И при демонстрации тут же столкнулся с багом либы. И есть…