Схемы Avro
Чтобы хранить JSON-данные в виде кортежей, мы воспользуемся популярной практикой, которая уменьшает объем данных и гарантирует корректность всех сохраняемых документов. Мы будем использовать Tarantool-модуль avro-schema, который проверяет схему JSON-документа и преобразует его в кортеж Tarantool. Кортеж будет содержать только значения полей и, следовательно, занимать значительно меньше места, чем исходный документ. В терминах avro-schema преобразование JSON-документов в кортежи называется «flattening» (уплощение), а восстановление исходных документов — «unflattening» (разуплощение).
Сначала нужно установить модуль
командой tt rocks install avro-schema.
Дальнейшее использование довольно просто:
(1) Для каждой сущности нужно определить схему в синтаксисе
Apache Avro schema, где
перечисляются поля сущности с их именами
и типами данных Avro.
(2) При инициализации вызываются avro-schema.create(), создающий
объекты в памяти для всех сущностей схемы, и compile(),
генерирующий методы flatten/unflatten для каждой сущности.
(3) В дальнейшем остается лишь вызывать методы flatten/unflatten для
соответствующей сущности при получении/отправке данных этой
сущности.
Вот как выглядят определения схем для сущностей player (игрок) и pokémon (покемон):
local schema = {player = {type="record",name="player_schema",fields={{name="id", type="long"},{name="name", type="string"},{name="location",type= {type="record",name="player_location",fields={{name="x", type="double"},{name="y", type="double"}}}}}},pokemon = {type="record",name="pokemon_schema",fields={{name="id", type="long"},{name="status", type="string"},{name="name", type="string"},{name="chance", type="double"},{name="location",type= {type="record",name="pokemon_location",fields={{name="x", type="double"},{name="y", type="double"}}}}}}}
А вот так мы создаем и компилируем сущности при инициализации:
-- load avro-schema module with require()local avro = require('avro_schema')-- create modelslocal ok_m, pokemon = avro.create(schema.pokemon)local ok_p, player = avro.create(schema.player)if ok_m and ok_p then-- compile modelslocal ok_cm, compiled_pokemon = avro.compile(pokemon)local ok_cp, compiled_player = avro.compile(player)if ok_cm and ok_cp then-- start the game<...>elselog.error('Schema compilation failed')endelselog.info('Schema creation failed')endreturn false
Что касается сущности map (карта), то вводить для нее схему было бы избыточно, поскольку в игре есть только одна карта, у нее совсем немного полей и, что важнее всего, карта используется только внутри нашей логики и никогда не передается внешним пользователям.
Далее нам потребуются методы для реализации игровой логики. Чтобы
сымитировать объектно-ориентированное программирование в нашем Lua-коде,
сохраним все Lua-функции и общие переменные в одной локальной переменной
(назовем ее game). Это позволит обращаться к функциям и переменным
внутри модуля как self.func_name или self.var_name. Например:
local game = {-- a local variablenum_players = 0,-- a method that prints a local variablehello = function(self)print('Hello! Your player number is ' .. self.num_players .. '.')end,-- a method that calls another method and returns a local variablesign_in = function(self)self.num_players = self.num_players + 1self:hello()return self.num_playersend}
В терминах ООП локальные переменные внутри game можно рассматривать
как поля объекта, а локальные функции — как методы объекта.
Таким образом, наш игровой модуль будет содержать следующие методы:
catch()— для вычисления того, пойман ли покемон (помимо координат игрока и покемона, этот метод применяет фактор вероятности, поэтому не каждый покемон в пределах досягаемости игрока будет пойман);respawn()— для добавления недостающих покемонов на карту, например, каждые 60 секунд (мы исходим из того, что испуганный покемон убегает, поэтому при любой попытке поимки покемон удаляется с карты, а через некоторое время возвращается обратно);notify()— для записи в лог информации о пойманных покемонах (например, «Игрок 1 поймал покемона A»);start()— для инициализации игры (создает спейсы базы данных, создает и компилирует схемы Avro, а также запускаетrespawn()).
Кроме того, удобно иметь методы для работы с хранилищем Tarantool. Например:
add_pokemon()— для добавления покемона в базу данных, иmap()— для заполнения карты всеми покемонами, хранящимися в Tarantool.
Эти два метода нужны в первую очередь при инициализации игры, но их также можно вызывать и позже — например, для тестирования кода.