Python. Игра в города

Alexandr Sokolov
5 min readSep 13, 2020

--

[Level: Junior]

Получаем список городов.

Это просто, открываем Вики. Остается сделать следующее:

  1. скачать страницу
  2. спарсить города из таблицы
  3. сохранить в файл

Получаем html

# -*- encoding: utf-8 -*-
import
requests

URL = "https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%B3%D0%BE%D1%80%D0%BE%D0%B4%D0%BE%D0%B2_%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D0%B8"

r = requests.get(URL)
with open("cities.html", "w") as f:
f.write(r.text)

Парсим города в файл

# -*- encoding: utf-8 -*-
from bs4 import BeautifulSoup


with open("cities.html", "r") as f:
html = f.read()

soup = BeautifulSoup(html, features="html.parser")
table = soup.find("table", attrs={"class": "standard sortable"})

with open("cities.txt", "w") as f:
for row in table.find_all("tr")[2:]:
city = row.find_all("td")[2].get_text().strip()
f.write(f"{city}\n")

Теперь у нас есть cities.txt в котором 1117 городов.

Что будем делать?

Обычная игра в города, которую все мы знаем. Пользователь всегда будет начинать первым. Игра заканчивается либо когда пользователь сдается либо когда у нас не осталось городов на текущую букву.

Для этого нам потребуется 2 хранилища: одно для набора городов, которые были названы, второе для оставшихся городов.

Еще нам нужно фильтровать последние буквы города если его название заканчивается на твердый знак, например, то нужно взять следующую букву с конца. Всего таких букв будет 4: (“Ъ”, “ь”, “ы”, “й”).

Также нужно произвести некоторую оптимизацию имен городов: перевести все имена в нижний регистр и заменить “ё” на “е”.

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

Кодируем

Для начала реализуем метод, который будет приводить имя города в общий вид

def normalize_city_name(name):
return name.strip().lower().replace('ё', 'е')

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

Загружаем список городов

cache = set()  # инициализируем кеш
cities = {normalize_city_name(x) for x in open("cities.txt", "r").readlines() if x.strip()}

Открываем файл, получаем список строк и обрабатываем через функцию normalize_city_name все строки из файла которые содержат хотябы один печатный символ.

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

with open("cities.txt", "r") as f:
cities = {normalize_city_name(x) for x in f.readlines() if x.strip()}

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

Теперь нам нужно попросить пользователя ввести город, после этого произвести все необходимые проверки

def user_point(char):
user_say = input(f"[{char or 'any'}] Start:") # 1
city = normalize_city_name(user_say) # 2
kw = {"char": char, "cache": cache, "cities": cities} # 3
if not all(x(city, **kw) for x in check_list): # 4
return user_point(char) # 5
return city
  1. Получаем ввод от пользователя. char or “any” потому что char изначально будет None, когда игра только началась.
  2. нормализуем строку
  3. создаем словарь который будет передан как **kwargs в методы проверки
  4. производим все проверки и ожидаем что все будут True
  5. если не все проверки были пройдены, то повторяем все еще раз

check_list — это список содержащий ссылки на функции которые принимают первым аргументом город и что-то что им нужно для выполнения проверки. Таких метода у нас 3

@check_point
def is_city_startswith_char(city, char, **kwargs):
if char is None or city.startswith(char):
return True
else
:
print(f'Город должен начинаться с буквы {char.capitalize()}.')
return False


@check_point
def is_non_cached(city, cache, **kwargs):
if city not in cache:
return True
else
:
print("Этот город уже был назван.")
return False


@check_point
def is_available(city, cities, **kwargs):
if city in cities:
return True
else
:
print("Я такого города не знаю.")
return False

Первая функция проверяет что город начинается с нужной буквы, второй — что город еще не был назван и третий — что такой город нам известен.

Декоратор check_point позволяет зарегистрировать любой метод как метод проверки города от пользователя, для его работы нужно объявить список, в котором будут все проверки и обернуть требуемые функции в декоратор.

check_list = []


def check_point(fun):
check_list.append(fun)
return fun

Как мы видим код декоратора прост. Мы используем знание о том что тело декоратора выполняется на этапе загрузки модуля и добавляем ссылку на функцию в список. Декоратор возвращает сам метод никак его не изменяя. Более подробно про декораторы описано в серии статей на хабре.

Мне нравится данный подход тем, что так явно видно что функция is_available является функией проверки. Хотя есть и минус, который решается через ООП, отсутствие явного интерфейса. Только задокументировав, можно сообщить, что такой метод должен принимать и возвращать.

После всех проверок, нам нужно будет переместить названный город в кеш и убрать его из списка доступных

def move_to_cache(city, cities, cache):
# убираем из списка доступных
cities.remove(city)
# перекидываем город в кэш
cache.add(city)

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

def get_next_char(city):
wrong_char = ("Ъ", "ь", "ы", "й")
# выбираем букву для следующего города
for char in city[::-1]:
if char in wrong_char:
continue
else
:
break
else
:
raise RuntimeError
return char

city[::-1] — позволяет проитерировать название города в обратном порядке, то же самое делает функция reversed(). Блок else у for выполнится только в том случае если не сработает break, что невозможно.

Теперь можно реализовать функцию которая будет описывать ход компьютера

def ai_point(char):
# выбираем город
for city in cities:
if city.startswith(char):
break
else
:
raise SystemExit("Вы победили!")
print(city)
return city

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

И осталось реализовать точку входа

from itertools import cycle
def
main():
char = None
for
point in cycle((user_point, ai_point)):
next_city = point(char)
move_to_cache(next_city, cities, cache)
char = get_next_char(next_city)

Функция cycle из модуля itertools принимает в качестве аргумента итерируемый объект и позволяет бесконечно его итерировать. Таким образом мы на каждую итерацию меняем функцию *_point, описывающую кто делает ход, так мы реализуем пошаговую логику игры. Сначала будет выполнен метод user_point, потом ai_point и так по кругу.

Profit

Весь код можно увидеть в репозитории https://github.com/alexsok-bit/citygame

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

В качестве домашнего задания остается следующее:

  1. Реализовать уровни сложности. Так на данный момент игра знает все города России (во всяком случае те, что были на странице вики). Кроме изменения кол-ва известных городов также можно менять и стратегию выбора города в зависимости от того на какую букву он оканчивается и какова популяция городов начинающихся на эту букву.
  2. Дать пользователю возможность выйти из игры по какому-то более адекватному событию, чем по шот-кату Ctrl+C
  3. Добавить графический интерфейс. Рекомендую PyQt5.
  4. Устранить проблему с городом Йошкар-Ола, единственный город на “Й” и на данный момент он вне игры.
  5. Устранить плюрализм с городами из нескольких слов. Как написать Ростов на Дону? Для игры воспринимается только ростов-на-дону. Т.е. только с дефисами, также есть и города которые только с пробелами. Надо привести это к общему или придумать как сделать так чтобы пробелы или дефисы не влияли на правильность (ростов на дону, ростов-на-дону, ростов-на дону, ростов на-дону).

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Alexandr Sokolov
Alexandr Sokolov

Written by Alexandr Sokolov

Software Engineer. Python Developer.

No responses yet

Write a response