Даже сравнительно простой мир, такой как ArtifactoryMMO, приподносит не мало неожиданностей. Хотя есть много примеров кода для управления этим миром из Javascript и Python, я выбрал более серьезный язык, расчитывая прикрутить туда какие-нибудь интересные алгоритмы машинного обучения. Но все равно слишком часто, по крайней мере при отладке, приходится отдавать отдельные команды и анализировать что получилось вручную. Несмотря на прекрасный REPL в Julia, один из лучших, что мне доводилось использовать, и для отладки своего кода, и просто как калькулятор, здесь это оказалось не очень удобно. Конечно, есть curl и jq, но по эргономичности он тоже не идеален. Не curl-ом единым, удобный HTTP-клиент встроен, например, в PowerShell. Но мне захотелось чего-то нового и прогрессивного, и я решил посмотреть Nu. Эта статья предназначена, чтобы привлечь к этому shell любителей MMO-игр, и заинтересовать MMO-играми пользователей nu-shell, а если повезет, заинтересовать обоими темами тех, кто раньше про них и не знал.

Простой HTTP-запрос в Nu делается практически также, как и с помощью curl. Давайте посмотрим с чем нам предстоит бороться.
http gethttps://api.artifactsmmo.com/monsters

Визуальное представление json ответа
Визуальное представление json ответа

В чем-то представления ответа более человекочитаемое, чем тот же
curl https://api.artifactsmmo.com/monsters | jq

Но и программно обрабатывать результат тоже проще, например так:
(http get https://api.artifactsmmo.com/monsters).data | where type == boss | select name level drops

Можно записать результат запроса в переменную:

let items = (http get https://api.artifactsmmo.com/items).data

$items | where subtype == food | select name level

let вводит новую неизменяемую переменную в область видимости. Ее можно скрыть другим let, но старая переменная не исчизает, на нее могут ссылаться ранее определенные функции.
Изменяемые переменные вводятся ключевым словом mut (без let, как можно было бы предположить, зная что nu-shell написан на Rust). Но такие переменные не могут попадать в функции извне, что бывает неудобно.

Размышления об осмысленности этого ограничения.

Я прекрасно понимаю смысл ограничения доступности мутабельных переменных в языках, типа Rust, но для shell оно несколько странно. Shell - это скорее среда обитания, а не инструмент разработки сложных программ, и в ней полезно иметь доступ ко всем потрoхам, как принято в Lisp и Smalltalk. С другой стороны, возможно, это будет способствовать выработке полезных привычек.

Новые функции опредеяются остаточно просто:

def "mmo get" [query] {
  http get $"https://api.artifactsmmo.com/($query)"
}

Имя функции может состаять из нескольких слов, что добавляет читабельности и удобно при использовании автодополнения (completion).

Более сложная функция, загружающая данные, разбитые на несколько страниц.

def "mmo load" [query] {
  let r = mmo get $query
  mut data = $r.data
  let pages = try { $r.pages } catch { 0 }
  if $pages > 1 {
    for page in 2..$pages {
     let r = mmo get $"($query)?page=($page)"
     $data = $data | append $r.data
    }
  }
  $data
}

Для получения доступа к привязанным к аккаунту функциям ArtifactsMMO требуется токен. Мы его прочитаем из файла artifactsmmo.token. Конечно, сначала надо было зарегистрироваться в игре, получить токен и записать его в этот файл.

let token = open artifactsmmo.token | lines | get 0

open, неожидано, читает весь файл, а не возвращает дескриптор или канал для чтения.

Получим список своих персонажей. Его будет удобно сохранить в глобальной переменной и обновлять по мере необходимости. С простой пременной так сделать нельзя, но есть модифицируемый контейнер env, позволяющий обойти это ограничение. В нем хранятся так называемые "переменные среды", которые являются строками, но можно туда поместить и данные произвольной структуры.

$env.CHARACTERS = (http get --headers { Authorization: $"Bearer ($token)" } https://api.artifactsmmo.com/my/characters).data

Теперь можно заставить этих персонажей работать...

def act [name: string, action: string, data] {
  http post --headers { Authorization: $"Bearer ($token)", Content-Type: application/json } --content-type application/json $"https://api.artifactsmmo.com/my/($name)/action/($action)" $data
}

Теперь можно отдать команду

act ИмяПерсонажа move {x:0, y:1}
act ИмяПерсонажа fight {}

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

def characters [] { $env.CHARACTERS | get name }

def actions [] {
  [move, fight, rest, transition, equip, unequip, use, gathering,
   crafting, bank/deposit/gold, bank/deposit/item, bank/withdraw/item,
   bank/withdraw/gold, bank/buy_expansion, npc/buy, npc/sell,
   recycling, grandexchange/buy, grandexchange/sell,
   grandexchange/cancel, task/complete, task/exchange, task/new,
  task/trade, task/cancel, give/gold, give/item, delete]
}

def act [name: string@characters, action: string@actions, data] {
  http post --headers { Authorization: $"Bearer ($token)", Content-Type: application/json } --content-type application/json $"https://api.artifactsmmo.com/my/($name)/action/($action)" $data
}

Теперь можно управлять этим игровым миром не выходя из nu!

На самом деле все немного сложнее. Запросы могут выдавать неожиданные ошибки, например если пытаешься переместить персонажа на место, где он уже находится или если персонаж находится в состоянии cooldown после прошлого действия. Играя, нельзя забывать и про реальный мир: сервер иногда пятисотит, сеть иногда отваливается (а в nu таймаут по умолчанию бесконечный и процесс в таком случае зависает). Частично, все одно обрабатывается в более полном скрипте. Все-таки советую сначала попробовать упрощенные примеры из статьи прямо в командной строке, а потом уже разбираться с тонкостями.

Комментарии (6)


  1. azTotMD
    16.12.2025 20:51

    вот тоже думаю из свой игры сделать окружение для reinforcement learning


    1. potan Автор
      16.12.2025 20:51

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


      1. azTotMD
        16.12.2025 20:51

        как это, заключать смартконтракты?


        1. potan Автор
          16.12.2025 20:51

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


          1. azTotMD
            16.12.2025 20:51

            Вас видимо интересует именно эта тема со смартконтрактами.

            Я понимаю, что пример утрированный, но здесь если первый игрок будет получать кучу рыбы, то ему особо смысла ловить и нет. Т.е. это не сказать, что сильный запрет. Другое дело, зачем это второму игроку, он ведь тоже какую-то выгоду должен получить. То что кто-то другой не будет там ловить тоже не сказать, что высокая награда.

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


            1. potan Автор
              16.12.2025 20:51

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