Высмеивание оболочки GraphQL вокруг универсального шахматного интерфейса

vysmeivanie obolochki graphql vokrug universalnogo shahmatnogo interfejsa
изображение-253
Фото Сэмюэля Целлера / Unsplash

Универсальный шахматный интерфейс (UCI) существует давно и используется многими шахматными механизмами. Что дает GraphQL?

Недавно я обменялся по электронной почте с владельцем шахматного веб-сайта, и он спросил меня, что я знаю об UCI и веб-сокетах. Это заставило меня поближе присмотреться к UCI и подумать о том, как и почему можно повернуть схему GraphQL вокруг нее.

Универсальный шахматный интерфейс

UCI существует уже более десяти лет и базируется на стандартном обмене сообщениями ввода-вывода между шахматной машиной и клиентом (обычно графическим интерфейсом пользователя). Клиент посылает сообщение шахматному движку, а движок может отправить ответ. я говорю может поскольку UCI не требует ответа на некоторые входящие сообщения в шахматную систему.

Двигатель тоже могущество отправить более одного ответа. При анализе игры движок будет посылать назад информация пакеты с подробным описанием его мышления. Клиент говорит, какова начальная позиция, говорит ему «Уходи», и двигатель продолжает работать, пока не достигнет точки a лучший ход. Во время процесса механизм возвращает сообщение о том, что он думает.

Спецификация UCI коротка, и вам не нужно ее глубокое понимание, чтобы понять основы того, как она работает — и сейчас она работала хорошо, и зачем с ней нервничать?

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

Chess Engine против Chess Server

Я хочу начать с моделирования службы UCI. Следующим шагом будет создание функционального прототипа, и, как всегда бывает, нетрудно найти пакет Node.js, который поможет с большей частью работы.

Один из самых популярных двигателей называется Stockfish. Кто-то потрудился транспилировать его с C++ в JavaScript, чтобы двигатель можно было запускать полностью в Node.js.

Вот так просто:

  • создать и записать в папку
  • npm init
  • npm install stockfish
  • node node_modules/stockfish/src/stockfish.js

Теперь вы находитесь в командной оболочке Stockfish и можете начать вводить команды.

Первое, что нужно сделать, это запустить интерфейс UCI, введя текст uci. Вы должны получить такой ответ:

id name Stockfish.js 8
id author T. Romstad, M. Costalba, J. Kiiski, G. Linscott

option name Contempt type spin default 0 min -100 max 100
option name Threads type spin default 1 min 1 max 1
option name Hash type spin default 16 min 1 max 2048
option name Clear Hash type button
option name Ponder type check default false
option name MultiPV type spin default 1 min 1 max 500
option name Skill Level type spin default 20 min 0 max 20
option name Move Overhead type spin default 30 min 0 max 5000
option name Minimum Thinking Time type spin default 20 min 0 max 5000
option name Slow Mover type spin default 89 min 10 max 1000
option name nodestime type spin default 0 min 0 max 10000
option name UCI_Chess960 type check default false
option name UCI_Variant type combo default chess var chess var giveaway var atomic var crazyhouse var horde var kingofthehill var racingkings var relay var threecheck
option name Skill Level Maximum Error type spin default 200 min 0 max 5000
option name Skill Level Probability type spin default 128 min 1 max 1000
uciok

Он показывает, какие параметры установлены, а затем возвращается uciok, то есть интерфейс готов. Следующим шагом будет установка параметров, а затем вызов isreadyи когда двигатель реагирует readyokон может приступить к анализу шахматной позиции.

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

В реальной серверной реализации я запускал бы один механизм для каждого клиента (или, возможно, больше). GraphQL помогает мне определить API, который будет поддерживать несколько клиентов, работающих на нескольких механизмах.

GraphQL

Для этого макета я разделил компонент UCI на HTTP-запросы вызова/ответа и подписки на веб-сокеты для обработки потоковых ответов. Это означает, что сокет открыт, только если пользователь хочет подписаться на подробную информацию о том, что думает двигатель. Кроме того, я могу уточнить количество и типы информация сообщения, которые я получаю на клиенте, чтобы минимизировать трафик сокета.

Каждая команда получает ответ

Поскольку взаимодействие клиент-сервер происходит (в большинстве случаев) из-за ненадежного HTTP, важно, чтобы клиент (запущенный в браузере) знал, что его сообщение попало на сервер. Команда UCI setoptionнапример, не посылает ответ в соответствии со спецификацией.

Это хорошо для интерфейса, основанного на надежных сокетах, не очень хорошо для запросов HTTP. GraphQL обеспечивает отправку ответа на каждый полученный запрос, хотя бы только для подтверждения получения запроса.

Каждая команда и ее аргументы безопасны для типов

Интерфейсы GraphQL базируются на схеме, интерфейсы UCI – нет (они базируются на описательном тексте в спецификации). Если клиент отправляет недействительную команду, сервер шахматного механизма не должен иметь дело с ней. Определив UCI в терминах типов в GraphQL, я могу предотвратить ошибочную команду на уровне API — в GraphQL — до того, как она попадет в механизм.

Резолверы GraphQL могут разлагать ответы на структуры JSON

JavaScript – это язык Интернета, а GraphQL возвращает ответы JSON. Благодаря тому, что резолверы GraphQL принимают ответ UCI и разбивают его детально и структурированно, клиент облегчает задачу анализа ответа UCI.

Я могу легко поиздеваться над своим API с помощью инструментов Apollo GraphQL

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

Я могу взаимодействовать с API через службу GraphiQL

ГрафикiQL – это интерактивный сервис, который можно запускать на сервере GraphQL. Это удобно для специального тестирования API на основе макета или реализации.

К насмешке!

Давайте сначала рассмотрим зависимости:

"dependencies": {
    "apollo-server-express": "^1.3.2",
    "babel-cli": "^6.26.0",
    "babel-preset-env": "^1.6.1",
    "express": "^4.16.2",
    "graphql": "^0.12.3",
    "graphql-subscriptions": "^0.5.7",
    "graphql-tag": "^2.7.3",
    "graphql-tools": "^2.21.0",
    "stockfish": "^8.0.0",
    "subscriptions-transport-ws": "^0.9.5"
  },
  "devDependencies": {
    "casual": "^1.5.19",
    "randexp": "^0.4.8"
  },

Я звоню на этот сервер chessQа сам сервер будет основан на apollo-server-express, реализация сервера GraphQL от Apollo Group The stockfish.js пакет, упомянутый ранее, включен как встроенный механизм. Хотя этот макет не использует его, он для справки. В реальной реализации можно получить доступ к внешнему двигателю.

В комплекте есть casual и randexp за помощь с макетами. наконец, graphql-subscriptions и subscriptions-transport-ws будет обрабатывать потоковые сообщения, поступающие из нашего макета, пока он делает вид, что анализирует.

Схема chessQ

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

Прежде всего необходимо определить запросы верхнего уровня. Это точки входа для клиента:

type Query {
    createEngine: EngineResponse
    uci(engineId: String!): UciResponse!
    register(engineId: String!, name: String, code: String): String
    registerLater(engineId: String!): String
    setSpinOption(engineId: String!, name: String!, value: Int!): String!
    setButtonOption(engineId: String!, name: String!): String!
    setCheckOption(engineId: String!, name: String!, value: Boolean!): String!
    setComboOption(engineId: String!, name: String!, value: String!): String!
    quit(engineId: String!): String!
    isready(engineId: String!): String!
  }

The createEngine запрос вернет EngineResponse, внутри которого содержится идентификатор экземпляра двигателя, используемого для следующих запросов:

{
  "data": {
    "createEngine": {
      "engineId": "46d89031-03c3-4851-ae97-34e4b5d1d7c6"
    }
  }
}

The uci запрос вернет UciResponse с подробным описанием текущих параметров параметров. В схеме GraphQL каждый тип параметра (спин, чек, кнопка и комбо) имеет свои специальные поля:

interface Option {
    name: String!
    type: String!
  }
    
type SpinOption implements Option {
    name: String!
    type: String!
    value: Int!
    min: Int!
    max: Int!
  }
    
type ButtonOption implements Option {
    name: String!
    type: String!
  }
    
type CheckOption implements Option {
    name: String!
    type: String!
    value: Boolean!
  }
    
type ComboOption implements Option {
    name: String!
    type: String!
    value: String!
    options: [String!]!
  }

Макет uci query может быть:

query uci {
  uci(engineId: "46d89031-03c3-4851-ae97-34e4b5d1d7c6") {
    uciokay
    options {
      name
      type
      ... on SpinOption {
        value
        min
        max
      }
    }
  }
}

и ответ:

{
  "data": {
    "uci": {
      "uciokay": true,
      "options": [
        {
          "name": "Porro tempora minus",
          "type": "check"
        },
        {
          "name": "Id ducimus",
          "type": "combo"
        },
        {
          "name": "Aliquam voluptates",
          "type": "button"
        },
        {
          "name": "Voluptatibus illo ullam",
          "type": "spin",
          "value": 109,
          "min": 0,
          "max": 126
        },
        {
          "name": "Temporibus et nisi",
          "type": "check"
        }
      ]
    }
  }
}

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

В конце концов каждый экземпляр двигателя должен будет поддерживать определенную индикацию своего состояния (не реализовано в этом макете). Это могут быть:

enum eEngineState {
    CREATED
    INITIALIZED
    READY
    RUNNING
    STOPPED
  }

Если, например, a go Команда отправляется в состояние двигателя READYэто будет ошибка.

Готовая схема

Когда двигатель ГОТОВ, возможны три новых команды:

  • ucinewgame: сообщите двигателю, что началась новая игра
  • position: сообщите двигателю, какая начальная позиция (наряду с любыми движениями с этой позиции)
  • go: запускай двигатель!

Перед выдачей go клиент имеет возможность подписаться на любой информация сообщения, поступающие через веб-сокет (иначе будет только a BestMove HTTP-ответ после завершения работы механизма).

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

Схема определяет типы Subscriptions доступный. Для этого макета есть только один:

type Subscription {
    info: Info
  }

The Info типа, как Option типа, является объединением нескольких конкретных информационных структур:

type Score {
    cp: Int!
    depth: Int!
    nodes: Int!
    time: Int!
    pv: [Move!]!
  }
  
type Depth {
    depth: Int!
    seldepth: Int!
    nodes: Int
  }
  
type Nps {
    value: Int!
  }
  
type BestMove {
    value: Move!,
    ponder: Move
  }
  
union Info = Score | Depth | Nps | BestMove

Точное значение этих Info сообщения не имеют отношения к этой дискуссии. Важно знать, что они поступают в любом порядке, кроме BestMove сообщение, которое является последним.

Клиент подписывается на информационные сообщения с помощью a subscription запрос вроде следующего:

subscription sub {
  info {
    ... on Score {
      pv
    }
    ... on BestMove {
      value
    }
  }
}

Существует развязчик для обработки Subscription запрос, использующий методы в graphql-subscriptions пакет:

import {PubSub, withFilter} from 'graphql-subscriptions';
...

resolvers: 
...
Subscription: {
      info: {
        subscribe: withFilter(() => pubsub.asyncIterator(TOPIC), (payload, variables) => {
          return true
        })
      }
    }...

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

Увидев это в действии

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

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

Загрузите пакет chessQ, запустите npm install и затем npm run dev . Теперь макет программы chessQ должен быть запущен.

Откройте две вкладки на http://localhost:3001/graphiql.

В одной вкладке введите:

subscription sub {
  info {
    __typename
    ... on Score {
      pv
    }
    ... on BestMove {
      value
    }
  }
}

Вы увидите следующее сообщение:

"Your subscription data will appear here after server publication!"
E2xSU2QP22UL1MvOuIqnnlc1ixS6zEU3nSLw
Готов к приему!

Для сообщений существует a go резолвер (показан ниже), который просматривает статический набор информационных сообщений и публикует их одно за другим. Потому что панель подписки отображает только одно сообщение за раз, есть простой sleep реализация, которая замедляет обмен сообщениями, чтобы вы могли видеть, как они пролетают:

function sleep(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms)
  })
}
...

resolvers: {...

Mutation: {
      go: async () => {
        let info;
        for (info of InfoGenerator()) {
          pubsub.publish(TOPIC, {info})
          await sleep(1000)
        }
        return info;
      }
    },...

Наконец, на вкладке без подписки начните анализ с go:

mutation go {
  go {
    __typename
    value
  }
}

Пока эта вкладка ждет go ответ, показывающий BestMove, subscription вкладка будет ловить информационные сообщения и отображать их один за другим.

Сеад -- yLEPWQIqOBgZlLM9czMcSB61AgRMxQ
Информационные сообщения…
Ag3amWB0PTUL--3UjwwK-96bpH7vEUN5pUAY
Анализ завершен!

Дальнейшие мнения

Прежде чем перейти от макета к реализации, несколько замечаний:

Простой механизм pub/sub, использованный в этом примере, не является ни надежным, ни масштабируемым. Это нормально, потому что существуют реализации graphql-subscription на Redis и RabbitMQ. Также может быть определена более уточненная спецификация подписки, которая предоставит подписчику детальный контроль над тем, какие сообщения получать.

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

Весь исходный код этой статьи можно найти здесь.

Добавить комментарий

Ваш адрес email не будет опубликован.