main.py
import os
from random import randrange
from random import choice
class FieldPart(object):
main = 'map'
radar = 'radar'
weight = 'weight'
# здесь просто задаем цвета. Они не соответствуют своим названиям, но главное всё сгруппировано в одном месте
# при желании цвета можно легко поменять не колупаясь во всей логике приложения
class Color:
yellow2 = '\033[1;35m'
reset = '\033[0m'
blue = '\033[0;34m'
yellow = '\033[1;93m'
red = '\033[1;93m'
miss = '\033[0;37m'
# функция которая окрашивает текст в заданный цвет.
def set_color(text, color):
return color + text + Color.reset
# класс "клетка". Здесь мы задаем и визуальное отображение клеток и их цвет.
# по визуальному отображению мы проверяем какого типа клетка. Уж такая реализация.
# По этой причине нельзя обозначать одним символом два разных типа. Иначе в логике возникнет путаница.
class Cell(object):
empty_cell = set_color(' ', Color.yellow2)
ship_cell = set_color('■', Color.blue)
destroyed_ship = set_color('X', Color.yellow)
damaged_ship = set_color('□', Color.red)
miss_cell = set_color('•', Color.miss)
# поле игры. состоит из трех частей: карта где расставлены корабли игрока.
# радар на котором игрок отмечает свои ходы и результаты
# поле с весом клеток. используется для ходов ИИ
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)]
def get_field_part(self, element):
if element == FieldPart.main:
return self.map
if element == FieldPart.radar:
return self.radar
if element == FieldPart.weight:
return self.weight
# Рисуем поле. Здесь отрисовка делитя на две части. т.к. отрисовка весов клеток идёт по другому
def draw_field(self, element):
field = self.get_field_part(element)
weights = self.get_max_weight_cells()
if element == FieldPart.weight:
for x in range(self.size):
for y in range(self.size):
if (x, y) in weights:
print('\033[1;32m', end='')
if field[x][y] < self.size:
print(" ", end='')
if field[x][y] == 0:
print(str("" + ". " + ""), end='')
else:
print(str("" + str(field[x][y]) + " "), end='')
print('\033[0;0m', end='')
print()
else:
# Всё что было выше - рисование веса для отладки, его можно не использовать в конечной игре.
# Само поле рисуется всего лишь вот так:
for x in range(-1, self.size):
for y in range(-1, self.size):
if x == -1 and y == -1:
print(" ", end="")
continue
if x == -1 and y >= 0:
print(y + 1, end=" ")
continue
if x >= 0 and y == -1:
print(Game.letters[x], end='')
continue
print(" " + str(field[x][y]), end='')
print("")
print("")
# Функция проверяет помещается ли корабль на конкретную позицию конкретного поля.
# будем использовать при расстановке кораблей, а так же при вычислении веса клеток
# возвращает False если не помещается и True если корабль помещается
def check_ship_fits(self, ship, element):
field = self.get_field_part(element)
if ship.x + ship.height - 1 >= self.size or ship.x < 0 or \
ship.y + ship.width - 1 >= self.size or ship.y < 0:
return False
x = ship.x
y = ship.y
width = ship.width
height = ship.height
for p_x in range(x, x + height):
for p_y in range(y, y + width):
if str(field[p_x][p_y]) == Cell.miss_cell:
return False
for p_x in range(x - 1, x + height + 1):
for p_y in range(y - 1, y + width + 1):
if p_x < 0 or p_x >= len(field) or p_y < 0 or p_y >= len(field):
continue
if str(field[p_x][p_y]) in (Cell.ship_cell, Cell.destroyed_ship):
return False
return True
# когда корабль уничтожен необходимо пометить все клетки вокруг него сыграными (Cell.miss_cell)
# а все клетки корабля - уничтожеными (Cell.destroyed_ship). Так и делаем. только в два подхода.
def mark_destroyed_ship(self, ship, element):
field = self.get_field_part(element)
x, y = ship.x, ship.y
width, height = ship.width, ship.height
for p_x in range(x - 1, x + height + 1):
for p_y in range(y - 1, y + width + 1):
if p_x < 0 or p_x >= len(field) or p_y < 0 or p_y >= len(field):
continue
field[p_x][p_y] = Cell.miss_cell
for p_x in range(x, x + height):
for p_y in range(y, y + width):
field[p_x][p_y] = Cell.destroyed_ship
# добавление корабля: пробегаемся от позиции х у корабля по его высоте и ширине и помечаем на поле эти клетки
# параметр element - сюда мы передаем к какой части поля мы обращаемся: основная, радар или вес
def add_ship_to_field(self, ship, element):
field = self.get_field_part(element)
x, y = ship.x, ship.y
width, height = ship.width, ship.height
for p_x in range(x, x + height):
for p_y in range(y, y + width):
# заметьте в клетку мы записываем ссылку на корабль.
# таким образом обращаясь к клетке мы всегда можем получить текущее HP корабля
field[p_x][p_y] = ship
# функция возвращает список координат с самым большим коэффициентом шанса попадения
def get_max_weight_cells(self):
weights = {}
max_weight = 0
# просто пробегаем по всем клеткам и заносим их в словарь с ключом который является значением в клетке
# заодно запоминаем максимальное значение. далее просто берём из словаря список координат с этим
# максимальным значением weights[max_weight]
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]
# пересчет веса клеток
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
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'
# при старте игры назначаем текущего и следующего игрока
def start_game(self):
self.current_player = self.players[0]
self.next_player = self.players[1]
# функция переключения статусов
def status_check(self):
# переключаем с prepare на in game если в игру добавлено два игрока.
# далее стартуем игру
if self.status == 'prepare' and len(self.players) >= 2:
self.status = 'in game'
self.start_game()
return True
# переключаем в статус game over если у следующего игрока осталось 0 кораблей.
if self.status == 'in game' and len(self.next_player.ships) == 0:
self.status = 'game over'
return True
def add_player(self, player):
# при добавлении игрока создаем для него поле
player.field = Field(Game.field_size)
player.enemy_ships = list(Game.ships_rules)
# расставляем корабли
self.ships_setup(player)
# высчитываем вес для клеток поля (это нужно только для ИИ, но в целом при расширении возможностей
# игры можно будет например на основе этого давать подсказки игроку).
player.field.recalculate_weight_map(player.enemy_ships)
self.players.append(player)
def ships_setup(self, player):
# делаем расстановку кораблей по правилам заданным в классе Game
for ship_size in Game.ships_rules:
# задаем количество попыток при выставлении кораблей случайным образом
# нужно для того чтобы не попасть в бесконечный цикл когда для последнего корабля остаётся очень мало места
retry_count = 30
# создаем предварительно корабль-балванку просто нужного размера
# дальше будет видно что мы присваиваем ему координаты которые ввел пользователь
ship = Ship(ship_size, 0, 0, 0)
while True:
Game.clear_screen()
if player.auto_ship_setup is not True:
player.field.draw_field(FieldPart.main)
player.message.append('Куда поставить {} корабль: '.format(ship_size))
for _ in player.message:
print(_)
else:
print('{}. Расставляем корабли...'.format(player.name))
player.message.clear()
x, y, r = player.get_input('ship_setup')
# если пользователь ввёл какую-то ерунду функция возвратит нули, значит без вопросов делаем continue
# фактически просто просим еще раз ввести координаты
if x + y + r == 0:
continue
ship.set_position(x, y, r)
# если корабль помещается на заданной позиции - отлично. добавляем игроку на поле корабль
# также добавляем корабль в список кораблей игрока. и переходим к следующему кораблю для расстановки
if player.field.check_ship_fits(ship, FieldPart.main):
player.field.add_ship_to_field(ship, FieldPart.main)
player.ships.append(ship)
break
# сюда мы добираемся только если корабль не поместился. пишем юзеру что позиция неправильная
# и отнимаем попытку на расстановку
player.message.append('Неправильная позиция!')
retry_count -= 1
if retry_count < 0:
# после заданного количества неудачных попыток - обнуляем карту игрока
# убираем у него все корабли и начинаем расстановку по новой
player.field.map = [[Cell.empty_cell for _ in range(Game.field_size)] for _ in
range(Game.field_size)]
player.ships = []
self.ships_setup(player)
return True
def draw(self):
if not self.current_player.is_ai:
self.current_player.field.draw_field(FieldPart.main)
self.current_player.field.draw_field(FieldPart.radar)
# если интересно узнать вес клеток можно расскомментировать эту строку:
# self.current_player.field.draw_field(FieldPart.weight)
for line in self.current_player.message:
print(line)
# игроки меняются вот так вот просто.
def switch_players(self):
self.current_player, self.next_player = self.next_player, self.current_player
@staticmethod
def clear_screen():
os.system('cls' if os.name == 'nt' else 'clear')
class Player(object):
def __init__(self, name, is_ai, skill, auto_ship):
self.name = name
self.is_ai = is_ai
self.auto_ship_setup = auto_ship
self.skill = skill
self.message = []
self.ships = []
self.enemy_ships = []
self.field = None
# Ход игрока. Это либо расстановка кораблей (input_type == "ship_setup")
# Либо совершения выстрела (input_type == "shot")
def get_input(self, input_type):
if input_type == "ship_setup":
if self.is_ai or self.auto_ship_setup:
user_input = str(choice(Game.letters)) + str(randrange(0, self.field.size)) + choice(["H", "V"])
else:
user_input = input().upper().replace(" ", "")
if len(user_input) < 3:
return 0, 0, 0
x, y, r = user_input[0], user_input[1:-1], user_input[-1]
if x not in Game.letters or not y.isdigit() or int(y) not in range(1, Game.field_size + 1) or \
r not in ("H", "V"):
self.message.append('Приказ непонятен, ошибка формата данных')
return 0, 0, 0
return Game.letters.index(x), int(y) - 1, 0 if r == 'H' else 1
if input_type == "shot":
if self.is_ai:
if self.skill == 1:
x, y = choice(self.field.get_max_weight_cells())
if self.skill == 0:
x, y = randrange(0, self.field.size), randrange(0, self.field.size)
else:
user_input = input().upper().replace(" ", "")
x, y = user_input[0].upper(), user_input[1:]
if x not in Game.letters or not y.isdigit() or int(y) not in range(1, Game.field_size + 1):
self.message.append('Приказ непонятен, ошибка формата данных')
return 500, 0
x = Game.letters.index(x)
y = int(y) - 1
return x, y
# при совершении выстрела мы будем запрашивать ввод данных с типом shot
def make_shot(self, target_player):
sx, sy = self.get_input('shot')
if sx + sy == 500 or self.field.radar[sx][sy] != Cell.empty_cell:
return 'retry'
# результат выстрела это то что целевой игрок ответит на наш ход
# промазал, попал или убил (в случае убил возвращается корабль)
shot_res = target_player.receive_shot((sx, sy))
if shot_res == 'miss':
self.field.radar[sx][sy] = Cell.miss_cell
if shot_res == 'get':
self.field.radar[sx][sy] = Cell.damaged_ship
if type(shot_res) == Ship:
destroyed_ship = shot_res
self.field.mark_destroyed_ship(destroyed_ship, FieldPart.radar)
self.enemy_ships.remove(destroyed_ship.size)
shot_res = 'kill'
# после совершения выстрела пересчитаем карту весов
self.field.recalculate_weight_map(self.enemy_ships)
return shot_res
# здесь игрок будет принимать выстрел
# как и в жизни игрок должен отвечать (возвращать) результат выстрела
# попал (return "get") промазал (return "miss") или убил корабль (тогда возвращаем целиком корабль)
# так проще т.к. сразу знаем и координаты корабля и его длину
def receive_shot(self, shot):
sx, sy = shot
if type(self.field.map[sx][sy]) == Ship:
ship = self.field.map[sx][sy]
ship.hp -= 1
if ship.hp <= 0:
self.field.mark_destroyed_ship(ship, FieldPart.main)
self.ships.remove(ship)
return ship
self.field.map[sx][sy] = Cell.damaged_ship
return 'get'
else:
self.field.map[sx][sy] = Cell.miss_cell
return 'miss'
class Ship:
def __init__(self, size, x, y, rotation):
self.size = size
self.hp = size
self.x = x
self.y = y
self.rotation = rotation
self.set_rotation(rotation)
def __str__(self):
return Cell.ship_cell
def set_position(self, x, y, r):
self.x = x
self.y = y
self.set_rotation(r)
def set_rotation(self, r):
self.rotation = r
if self.rotation == 0:
self.width = self.size
self.height = 1
elif self.rotation == 1:
self.width = 1
self.height = self.size
elif self.rotation == 2:
self.y = self.y - self.size + 1
self.width = self.size
self.height = 1
elif self.rotation == 3:
self.x = self.x - self.size + 1
self.width = 1
self.height = self.size
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.make_shot(game.next_player)
# в зависимости от результата накидываем сообщений и текущему игроку и следующему
# ну и если промазал - передаем ход следующему игроку.
if shot_result == 'miss':
game.next_player.message.append('На этот раз {}, промахнулся! '.format(game.current_player.name))
game.next_player.message.append('Ваш ход {}!'.format(game.next_player.name))
game.switch_players()
continue
elif shot_result == 'retry':
game.current_player.message.append('Попробуйте еще раз!')
continue
elif shot_result == 'get':
game.current_player.message.append('Отличный выстрел, продолжайте!')
game.next_player.message.append('Наш корабль попал под обстрел!')
continue
elif shot_result == 'kill':
game.current_player.message.append('Корабль противника уничтожен!')
game.next_player.message.append('Плохие новости, наш корабль был уничтожен :(')
continue
if game.status == 'game over':
Game.clear_screen()
game.next_player.field.draw_field(FieldPart.main)
game.current_player.field.draw_field(FieldPart.main)
print('Это был последний корабль {}'.format(game.next_player.name))
print('{} выиграл матч! Поздравления!'.format(game.current_player.name))
break
print('Спасибо за игру!')
input('')