Python. Напишем морской бой с достойным соперником-ИИ — Офтоп на DTF
Всем привет! В предыдущей статье мы написали игру в спички с ИИ. Как оказалось некоторые из вас даже открывали эту сатью, а некоторые из тех кто открывал даже поставили лайк. Спасибо! Сегодня будем писать морской бой в таком же «терминальном» стиле. На этот раз игра будет больше, а алгоритм стоящий за ходом противника — сложнее.
28 943 просмотров
Ознакомиться с полным кодом приложения и запустить прямо в браузере можно как всегда в конце статьи по ссылке.
Вот предыдущая статья про спички если кому-то интересно:
Прошлое приложение заняло 130 строк кода и я таки умудрился расписать его целиком в рамках поста, а вот морской бой уже получился на все 470+ строк. Всё расписать не получится. Будут расширеные комментарии внутри самого кода по ссылке. Как всегда начнём с самого морского боя, а ИИ (я буду называть это ИИ) оставим на вторую половину.
Морской бой
Сама игра не такая уж и сложная, но в ней хватает нюансов и если писать всё более-менее аккуратно, то получается много кода. В основном это связано с различными проверками при расстановке кораблей, доступности хода, проверками целости кораблей и прочее.
Начнём пожалуй с описания основных классов, которые нам понадобятся для реализации:
Game — сама игра. Этот класс будет группировать и манипулировать остальными классами.
Player — игрок. Будет иметь своё поле, будет совершать ходы.
Field — поле. Будет состоять из двух частей: основная карта и радар. Поле будет проверять возможность расположения кораблей, расставлять корабли, уничтожать их.
Ship — корабль. Можно было обойтись без него на самом деле, но с ним проще. В основном корабль будет хранить координаты и свои HP.
Кроме основных классов так же были написаны вспомогательные FieldPart — чтобы было проще обращаться к конкретной части поля (карта/радар).
Class Game(object)
class Game(object): letters = («A», «B», «C», «D», «E», «F», «G», «H», «I», «J») ships_rules = [1, 1, 1, 1, 2, 2, 2, 3, 3, 4] field_size = len(letters) def __init__(self): self.players = [] self.current_player = None self.next_player = None self.status = ‘prepare’ # просто названия функций входящих в Game # сами функции с детальным описанием можно найти в полном коде def start_game(self) def status_check(self) def add_player(self, player) def ships_setup(self, player) def draw(self) def switch_players(self) def clear_screen()
В классе Game мы опишем буквы используемые для отображения координат. Понятное дело внутри самой игры всё задаётся исключительно цифровыми координатами [0][4], [5][1] … но игрок должен во-первых видеть буквы на поле, а во-вторых делать свои ходы используя буквенные обозначения.
Игра будет хранить игроков в cписке players. Но зачем мучиться с коэффициентами при обращении к игрокам, если можно завести две переменные current_player и next_player,которые ссылаются на элементы списка players. Так мы всегда понимает как обратиться к текущему и следующему игроку. А в конце хода мы просто меняем значения этих переменных местами. Ну и я решил добавить такое понятие как status. У игры не то чтобы много статусов, но таким образом можно организовать более наглядный переход из одного состояния в другое в самом игровом цикле. Статусы будут примерно такие: prepare, in_game, game_over.
Функции у игры следующие:
start_game — просто объявляем текущего игрока и следующего.
status_check — игра каждый ход проверяет статусы и меняет их при достижении условий.
add_player — функция используется когда у игры статус prepare для добавления игроков и назначени им полей.
ship_setup — так же используется на старте игры. Передаём в функцию ссылку на игрока и игра просит его расставить корабли или расставляет автоматически. draw — рисует каждый ход поле и доп.инфу. switch_players — просто переключает текущего игрока и следующего, юзается в конце хода. clear_screen — чистит экран.
Class Player(object)
class Player(object): def __init__(self, name, is_ai, skill): self.name = name self.is_ai = is_ai self.auto_ship_setup = True self.skill = skill self.message = [] self.ships = [] self.enemy_ships = [] self.field = None # просто названия функций входящих в Player # сами функции с детальным описанием можно найти в полном коде def get_input(self, input_type) def make_shot(self, target_player) def receive_shot(self, shot)
Кратенько по игроку. name — просто имя. is_ai — является игрок ИИ или нет, в зависимости от этого меняет некоторая логика хода. auto_ship_setup — автоматически расставлять корабли для игрока или вручную. Опция для нетерпеливых. skill — относится к ИИ пока пропустим описание. ships наполняется имеющимися у игрока кораблями. enemy_ships — здесь будем считать какие корабли остались у противника — человек конечно и сам может посчитать, но для ИИ очень полезно. field — поле игрока (поле будет описано ниже)
Функции у игрока: get_input — запросить ввод. здесь мы передаем тип инпута либо ‘ship_setup’ либо ‘shot’. Всё зависит от статуса игры. Много логики внутри, тут лучше почитать код с комментами. make_shot — делаем выстрел по указанному игроку. В этой функции как раз и вызывается get_input с параметром ‘shot’. receive_shot — соответственно игрок принимает выстрел по координатам. На самом деле если чётко расписать действия игроков на бумаге и чем они оперируют в ходе игры всё становится гораздо очевиднее. receive_shot возвращает на выходе либо ‘miss’ (прмах) либо ‘get’ (ранил) либо ссылку на объект Ship (убил). Если убил логично вернуть весь корабль т.к. тогда проще закрасить на радаре клетки вокруг. Да, это конечно дерзко возвращать данные разных типов, но что поделать.
Class Field(object)
class Field(object): def __init__(self, size): self.size = size self.map = [[Cell.empty_cell for _ in range(size)] for _ in range(size)] self.radar = [[Cell.empty_cell for _ in range(size)] for _ in range(size)] self.weight = [[1 for _ in range(size)] for _ in range(size)] # просто названия функций входящих в Field # сами функции с детальным описанием можно найти в полном коде def draw_field(self, element) def check_ship_fits(self, ship, element) def mark_destroyed_ship(self, ship, element) def add_ship_to_field(self, ship, element) def get_max_weight_cells(self) def recalculate_weight_map(self, available_ships)
С полем всё просто: size — размер поля. map — основная карта поля по факту просто список списков изначально заполняемый пустыми клетками (Cell.empty_cell). radar — так же. weight уже по интереснее. Это относится к ИИ, поэтому обсудим чуть ниже.
Функции. draw_field — отрисовка поля. Здесь и далее параемтр element это часть поля с которой необходимо работать. либо Map либо Radar либо Weight. Check_ship_fits — прверяем что корабль помещается на ту или иную позицию. Используется при расстановке кораблей а так же для некоторых манипуляций с ИИ. mark_destroyed_ship — помечаем что корабль уничтожен. Ставим на нем кресты, вокруг — точки. add_ship_to_field — добавление корабля. тут все понятно. get_max_weight_cells и recalculate_weight_map относится к ИИ, это рассмотрим чуть ниже.
Class Ship(object)
class Ship:
def __init__(self, size, x, y, rotation):
self.size = size
self.hp = size
self.x = x
self.y = y
self. rotation = rotation
Тут вообще всё до безобразия просто: x, y — координаты начала корабля. size — размер. rotation — в какую сторону повёрнут (от 0 до 4). hp — текущее хп корабля. Конкретные поврежденные клетки не отслеживаются т.к. по факту за контроль отрисовки поля отвечает класс Field.
Последнее что мы рассмотрим перед написанием ИИ это основной цикл игры:
if __name__ == ‘__main__’:
players = []
players.append(Player(name=’Username’, is_ai=False, auto_ship=True, skill=1))
players.append(Player(name=’IQ180′, is_ai=True, auto_ship=True, skill=1))
game = Game()
while True:
game.status_check()
if game.status == ‘prepare’:
game.add_player(players.pop(0))
if game.status == ‘in game’:
Game.clear_screen()
game.current_player.message.append(«Ждём приказа: «)
game.draw()
game.current_player.message.clear()
shot_result = game.current_player.

Все достаточно просто. В цикле бесконечно проверяется статус игры и в зависимости от этого выполняются те либо иные действия. В статусе ‘in game’ происходят основные события игры. Получение данных о выстреле игрока, добавление подходящего сообщения для обоих игроков, передача хода другому игроку при промахе и прочее.
Если наступает статус ‘game over’ — выводим основные поля обоих игроков. Пишем кто выиграл, кто проиграл.
Алгоритм поведения ИИ
Как всегда я сел и стал прикидывать какой логикой оперирует человек при игре в морской бой.
- Первый ход случайный.
- Если попал и убил — обрисовал точками вокруг
- Если попал и не убил — стреляем в клетки сверху/снизу/слева/справа
- По диагоналям от клетки «попал» не может быть кораблей
- Прикидываем если остались только большие корабли — затираем мелкие скопления пустых клеток
Более глубокой логики поведения человека я придумать не смог. Дальше начал думать как перенести всё это на какие-то условия. Надо бы держать в уме предыдущий ход, а еще лучше предыдущий удачный ход. И если ты «попал», то следующий выстрел по сторонам. Если второй выстрел не попал, то вернуться к предыдущему ходу и выстрел в другую сторону рядом. Но дальше начинаютя сложности. Так как нужно понимать, что корабль прямой. Значит надо, чтобы ИИ понимал направление корабля и ходил после повторного попадания уже не по четырём направлениям, а в общем-то по одному. И опять-таки приходим к тому, что нужно помнить откуда начался этот корабль. Но это всё просто по сравнению с последним пунктом. Дойдя до него я понял: прежде чем стрелять по клетке — проверь, а помещается ли какой-нибудь корабль хоть как-то в нее.
И тут рассуждения увели меня чуть в сторону и стала вырисовываться идея коэффициентов: назначаем клеткам коэффициенты и стреляем по наиболее весомой. Если корабль помещается в какие-то клетки — прибавляем им коэффициенты. Если нет — коэффициент останется нулевым. Функция check_ship_fits уже была готова т.к. использовалась для расстановки кораблей до начала игры. Отлично.
Я начал развивать идею с коэффициентами. Решил так: изначально каждую клетку поля проверяем на возможность начала с нее корабля. Причем в любую из четырёх сторон. Итого клетка за каждый корабль может получить 4 балла. Естественно речь про поле-радар, единственно доступная информация о кораблях противника расположена на нем. Со временем поле будет заполнятья убитыми кораблями и промахами а коэффициенты этих клеток будут автоматически иметь 0.
Далее в систему коэффициентов нужно как-то вписать все пункты алгоритма:
«Первый ход случайный» — с введением коэффициентов это понятие немного изменится. Теперь каждый ход будет набираться список клеток с самым высоким коэффицентом, а уже из него будет случайным образом выбираться точка обстрела. Зачастую будет получатья так, что список будет состоять из одной единственной клетки которая методом подсчета получила максимальный коэффициент. Так что в целом становитя не важно первый это ход или нет.
«Если попал и убил» — тоже просто. В таких случаях клетке сразу ставится коэффициент 0 как и всем клеткам вокруг нее.
здесь и далее приведен небольшой кусочек поля
«Если попал и не убил» — мы усиливаем вес клеток по всем четырём направлениям т.к. корабль может иметь продолжение только в 4 стороны. По диагоналям от попадения — нули. Тут есть хитрость. Мы не просто выставляем коэффицинет всем вокруг мы умножаем имеющийся, а значит защищаем себя от возможности случайно увеличить нулевой кэффициент
первое попадание. увеличиваем коэффициенты
второе попадание
Ну и последний пункт — если остались только большие корабли — затираем мелкие скопления пустых клеток. Это будет выполняться на основе начального вычисления коэффициентов. Для каждой клетки будут браться оставшиеся корабли и будет совершаться попытка их вписать. Пример:
остался только трехпалубный корабль и как бы мы ни старались он не может проходить через клетку с «?» значит эта клетка не получит увеличения коэффицинета. И как результат коэффициент там будет 0. То же справедливо и для двух незанятых клеток чуть ниже.
Теперь осталось всего лишь написать это:
# пересчет веса клеток
def recalculate_weight_map(self, available_ships):
# Для начала мы выставляем всем клеткам 1.
# нам не обязательно знать какой вес был у клетки в предыдущий раз:
# эффект веса не накапливается от хода к ходу.
self.weight = [[1 for _ in range(self.size)] for _ in range(self.size)]
# Пробегаем по всем полю.
# Если находим раненый корабль — ставим клеткам выше ниже и по бокам
# коэффициенты умноженые на 50 т.к. логично что корабль имеет продолжение в одну из сторон.
# По диагоналям от раненой клетки ничего не может быть — туда вписываем нули
for x in range(self.size):
for y in range(self.size):
if self. radar[x][y] == Cell.damaged_ship:
self.weight[x][y] = 0
if x — 1 >= 0:
if y — 1 >= 0:
self.weight[x — 1][y — 1] = 0
self.weight[x — 1][y] *= 50
if y + 1 < self.size:
self.weight[x — 1][y + 1] = 0
if y — 1 >= 0:
self.weight[x][y — 1] *= 50
if y + 1 < self.size:
self.weight[x][y + 1] *= 50
if x + 1 < self.size:
if y — 1 >= 0:
self.weight[x + 1][y — 1] = 0
self.weight[x + 1][y] *= 50
if y + 1 < self.size:
self.weight[x + 1][y + 1] = 0
# Перебираем все корабли оставшиеся у противника.
# Это открытая инафа исходя из правил игры. Проходим по каждой клетке поля.
# Если там уничтоженый корабль, задамаженый или клетка с промахом —
# ставим туда коэффициент 0.
Больше делать нечего — переходим следующей клетке.
# Иначе прикидываем может ли этот корабль с этой клетки начинаться в какую-либо сторону
# и если он помещается прбавляем клетке коэф 1.
for ship_size in available_ships:
ship = Ship(ship_size, 1, 1, 0)
# вот тут бегаем по всем клеткам поля
for x in range(self.size):
for y in range(self.size):
if self.radar[x][y] in (Cell.destroyed_ship, Cell.damaged_ship, Cell.miss_cell) \
or self.weight[x][y] == 0:
self.weight[x][y] = 0
continue
# вот здесь ворочаем корабль и проверяем помещается ли он
for rotation in range(0, 4):
ship.set_position(x, y, rotation)
if self.check_ship_fits(ship, FieldPart.radar):
self.weight[x][y] += 1
У нас есть поле с выставленными коэффициентами. Надо бы дописать функцию get_max_weight_cells которая будет возвращать список координат с максимальным коэффициентом.
def get_max_weight_cells(self): weights = {} max_weight = 0 for x in range(self.size): for y in range(self.size): if self.weight[x][y] > max_weight: max_weight = self.weight[x][y] weights.setdefault(self.weight[x][y], []).append((x, y)) return weights[max_weight]
Занесем координаты всех клеток в словарь weights. Ключом будет вес клеток. Значением — список содержащий пары координат.
Пробегаем по всем клеткам и заносим их в словарь. Заодно запоминаем максимальное значение веса. Далее просто берём из словаря список координат с ключом максимальным заначением веса: weights[max_weight]
Всё что осталось сделать ИИ, для совершения адекватного хода, это из полученного списка координат выбрать случайную:
x, y = choice(self.field.get_max_weight_cells())
Вот вроде бы и всё. Здесь не описал многие детали реализации, но, как я писал выше, комментарии есть еще в коде.
Строку 335 можно раскомментировать, чтобы получить доступ к отладочному полю с коэффициентами и видеть их распределение.
Вы можете опробовать морской бой прямо здесь, а так же прочитать дополнительные комментарии
Если хотите еще статью похожей тематики — ставьте лайк и предлагайте идеи.
В следующих раз можем попробовать написать например телеграмм-бот тамагочи: )
Интерактивная игра «Морской бой»; 1 — 4 класс — Внеклассная работа — Начальные классы
Егорова Елена 5.0Отзыв о товаре ША PRO Анализ техники чтения по классам
и четвертям
Хочу выразить большую благодарность от лица педагогов начальных классов гимназии
«Пущино» программистам, создавшим эту замечательную программу! То, что раньше мы
делали «врукопашную», теперь можно оформить в таблицу и получить анализ по каждому
ученику и отчёт по классу. Великолепно, восторг! Преимущества мы оценили сразу. С
начала нового учебного года будем активно пользоваться. Поэтому никаких пожеланий у
нас пока нет, одни благодарности. Очень простая и понятная инструкция, что
немаловажно! Благодарю Вас и Ваших коллег за этот важный труд. Очень приятно, когда
коллеги понимают, как можно «упростить» работу учителя.
учитель химии и биологии, СОШ с. Чапаевка, Новоорский район, Оренбургская область
Отзыв о товаре ША Шаблон Excel Анализатор результатов ОГЭ
по ХИМИИ
Спасибо, аналитическая справка замечательная получается, ОГЭ химия и биология. Очень облегчило аналитическую работу, выявляются узкие места в подготовке к экзамену. Нагрузка у меня, как и у всех учителей большая. Ваш шаблон экономит время, своим коллегам я Ваш шаблон показала, они так же его приобрели. Спасибо.
Чазова Александра 5.0Отзыв о товаре ША Шаблон Excel Анализатор результатов ОГЭ по
МАТЕМАТИКЕ
Очень хороший шаблон, удобен в использовании, анализ пробного тестирования занял считанные минуты. Возникли проблемы с распечаткой отчёта, но надо ещё раз
разобраться. Большое спасибо за качественный анализатор.
учитель начальных классов, МБОУ СОШ №1, г. Красновишерск, Пермский край
Отзыв о товаре Изготовление сертификата или свидетельства конкурса
Большое спасибо за оперативное изготовление сертификатов! Все очень красиво. Мой ученик доволен, свой сертификат он вложил в портфолио. Обязательно продолжим с Вами сотрудничество!
Язенина Ольга Анатольевна 4.0учитель начальных классов, ОГБОУ «Центр образования для детей с особыми образовательными потребностями г. Смоленска»
Отзыв о товаре Вебинар Как создать интересный урок:
инструменты и приемы
Я посмотрела вебинар! Осталась очень довольна полученной
информацией. Всё очень чётко, без «воды». Всё, что сказано, показано, очень
пригодится в практике любого педагога. И я тоже обязательно воспользуюсь полезными материалами вебинара. Спасибо большое лектору за то, что она
поделилась своим опытом!
ША Табель посещаемости + Сводная для ДОУ ОКУД
Хотела бы поблагодарить Вас за такую помощь. Разобралась сразу же, всё очень аккуратно и оперативно. Нет ни одного недостатка. Я не пожалела, что доверилась и приобрела у вас этот табель. Благодаря Вам сэкономила время, сейчас же составляю табель для работников. Удачи и успехов Вам в дальнейшем!
Дамбаа Айсуу 5.0Отзыв о товаре ША Шаблон Excel Анализатор результатов ЕГЭ по
РУССКОМУ ЯЗЫКУ
Спасибо огромное, очень много экономит времени, т.к. анализ уже готовый, и особенно радует, что есть варианты с сочинением, без сочинения, только анализ сочинения! Превосходно!
Морской бой для печати PDF — OriginalMOM
Делиться заботой!
- Делиться!
«Ты потопил мой линкор!» Классическая игра с карандашом и бумагой на этих бесплатных печатных шаблонах настольной игры Морской бой! Играйте с друзьями и семьей, участвуя в веселой битве, чтобы узнать, кто потопит больше кораблей, чтобы победить.
Я помню, как в детстве играл в Морской бой, и мне всегда было так весело играть против моих братьев и сестер. Нам особенно нравилось играть во время длительных автомобильных поездок или в плохую погоду.
Теперь, как мама, я всегда ищу, чем мои дети могут заниматься, не пользуясь устройством или перед экраном. Мы все знаем, что детям нужно меньше экранного времени, но мои ведут себя так, будто не знают, что делать, когда экраны гаснут. Вот почему я работаю над печатными играми, в которые можно играть с карандашом и бумагой, такими как игра «Морской бой» и «Точки и квадраты».
Дети по-прежнему могут играть в действительно веселые игры, не сидя на экране!
В эти шаблоны настольной игры «Морской бой» можно играть карандашом и бумагой или заламинировать их и использовать для игры маркеры сухого стирания. В любом случае, мне очень нравится, что мне не нужно искать кусочки по всему дому, чтобы мы действительно могли играть в игру.
Как играть на линкоре, который можно распечатать?
Версия для печати «Морской бой» играется так же, как и в обычную настольную игру «Морской бой». Все, что вам нужно для распечатки шаблона настольной игры «Морской бой», — это карандаш и 2 распечатки досок.
На каждой доске есть сетка для «противника» и сетка для «себя».
В сетке, которую вы используете для себя, вы разместите все свои линкоры.
- Авианосец — AAAAA
- Линкор — BBBB
- Крейсер — CCC
- Подводная лодка — SSS
- Эсминец — DD
Каждая буква помещается в одну соседнюю ячейку.
В сетке противников вы будете записывать свои выстрелы и попадания по их кораблям.
Чтобы ознакомиться с официальными правилами, ознакомьтесь с этими Правилами для Battleship от создателя игры Милтона Брэдли.
Как лучше разместить линкор?
Лучшей стратегией размещения линкоров будет расстояние между линкорами не менее 1-2 мест. Вы также должны иметь один или два корабля вдоль края сетки. Большинство игроков, как правило, начинают стрелять ближе к середине доски. Размещение корабля на внешнем краю сетки позволит вам получить один линкор, который будет трудно найти. Будьте осторожны, чтобы не поставить все свои корабли на ребро, потому что ваш противник узнает ваш рисунок, и вам будет легче потопить ваши корабли.
Скачать игру «Морской бой» для печати
Загрузите этот бесплатный шаблон для печати игры «Морской бой», чтобы играть с детьми! Эта классическая игра полна стратегии и немного удачи. Его легко загрузить, просто нажмите на фиолетовую ссылку, и PDF-файл Battleship Game (бесплатная версия для печати) появится на вашем компьютере. Я рекомендую загрузить PDF-файл на свой компьютер, а затем распечатать для достижения наилучших результатов.
Шаблон игры «Морской бой» для бесплатной печати (PDF)
Printable Battleship Game (бесплатные игровые доски для печати)
Опубликовано: Автор: Molly · Этот пост может содержать партнерские ссылки.
Делиться заботой!
12 акции
- Поделиться
Сегодня у нас есть печатная версия классической игры про морской бой! Берите свою любимую версию и приступайте к игре.
Вы помните, как в детстве играли в Морской бой?
Теперь вы можете пережить ностальгию по этой классической игре вместе с семьей в нашей бумажной настольной игре!
Ищете ли вы увлекательный способ провести дождливый день или просто хотите хорошо провести время вместе, распечатанная игровая доска «Морской бой» — это то, что нужно вашей семье.
Итак, распечатайте наши сетки боевых кораблей и пусть битва начнется!
Если вам нравится эта игра для печати, вам понравятся и другие наши игры, такие как наша доска для печати в крестики-нолики или игра в точки для печати.
Содержание
- Как играть в «Морской бой»
- Игровое поле «Морской бой»
- Бумажная игра «Хватай линкор»
Как играть в «Морской бой»
Цель игры — потопить все «корабли» противника до того, как они потопят ваш.
Это настолько просто, что играть могут даже маленькие дети.
Все, что вам нужно, это распечатанное игровое поле «Морской бой» и два игрока (или команды).
Вы можете скачать бесплатную PDF-версию с нашего веб-сайта здесь.
Каждый игрок по очереди стреляет по линкорам противника, пока один из игроков не потопит все корабли другого игрока.
Подготовка
После того, как вы распечатаете две копии игрового поля «Морской бой», в каждом наборе должно быть пять различных типов кораблей:
- Авианосец (5 квадратов)
- Линкор (4 квадрата)
- Подводная лодка (4 клетки)
- Крейсер (3 клетки)
- Эсминец (2 клетки)
Перед началом игры каждый игрок должен разместить свои корабли на своих полях, расположив их в любом направлении по своему выбору.
Вы можете пометить свои корабли на игровом поле или использовать наши распечатанные корабли в качестве маркеров.
Игра в игру
Как только оба игрока разместили все свои корабли на соответствующих полях, пришло время начать игру!
Игроки по очереди угадывают, где их противник разместил свои корабли, а затем отмечают, удалось ли им поразить или промахнуться по этим целям.
Например, если игрок А догадается, что игрок Б разместил один из своих кораблей на F7, то игрок Б отметит это либо как попадание, либо как промах, в зависимости от того, действительно ли там находился корабль.
Этот процесс продолжается до тех пор, пока один игрок не потопит все корабли другого игрока.
Игровая доска для печати «Морской бой»
Наши сетки для печати «Морской бой» представлены в двух версиях. Веселая красочная версия и черно-белая версия для печати.
Мы также добавили корабли для печати.
Вы можете захватить соответствующие цветные корабли или захватить черно-белые.
Бумажная игра «Морской бой»
Чтобы получить распечатки «Морской бой», отправьте адрес электронной почты в поле ниже. Мы отправим их прямо на ваш почтовый ящик.
Пожалуйста, используйте их только для личного или классного использования. Не для коммерческого использования. Но вы можете распечатать столько копий игры, сколько захотите.
Советы по печати* Если вы хотите распечатать только одну версию игры, обязательно перейдите к настройкам принтера.