1
0
Fork
You've already forked tutorial.yart
0
код для проходження туторіала yet another roguelike tutorial (https://rogueliketutorials.com/tutorials/tcod/v2)
  • Python 100%
Find a file
2024年03月02日 19:15:04 -05:00
components mod: рефакторинг engine.py, виправлення помилок /ti 2024年03月02日 19:09:27 -05:00
resources mod: зміна тайлсету на buddy-graphical з dwarffortresswiki.org /ti 2024年01月21日 11:38:11 -05:00
screenshots mod: readme; завершення 5 розділу /ti 2024年02月11日 09:03:11 -05:00
.gitignore fix: gitignore; акуратне виключення venv та __pycache__ /ti 2024年02月11日 08:31:05 -05:00
main.py mod: refactor main.py /ti 2024年02月18日 15:48:27 -05:00
readme.md fix: помилка в readme /ti 2024年03月02日 19:15:04 -05:00
requirements.txt add: підготовка, встановлення залежностей /ti 2024年01月18日 18:16:24 -05:00

нотатки до підказки yet another roguelike tutorial - written in python 3 and tcod

я маю досвід програмування і непогано даю собі раду з алгоритмами, — але хотів би відполірувати практичні знання python (особливо організацію проєктів та об'єктне програмування), тож захотів «пробігти» декілька більш чи менш детальних підказок. цей файл (чи публікація, якщо я колись викладу цей матеріял до щоденника) — не переклад чи переказ підказки yet another roguelike tutorial, а лише мої нотатки, питання (й інколи відповіді) самому собі щодо основного тексту чи коду.

зміст

частина 0 - підготовка

url: https://rogueliketutorials.com/tutorials/tcod/v2/part-0/

0.0 налаштування git

ця підказка... ні, навіть так: більшість підказок з програмування, які мені довелося бачити, повністю ігнорують, що сьогодні програмувати без використання системи контролю версій принаймні дивно. чому ж не інтегрувати підказку з налаштування й базового використання git просто до тюторіалів? хз.

початківці не розуміють, що таке git, і як воно відрізняється від github (може, вірять, що для використання системи контролю версій обов'язково потрібен профіль на github і безліч різних налаштувань?) git можна використовувати локально, для цього потрібен мінімум налаштувань, і його можна використовувати разом із іншими онлайновими сервісами, які не належать microsoft — приміром, codeberg чи gitlab.

0.0.1 локальний репозиторій git

скажу так: якщо ти відкриваєш туторіал з програмування, і не починаєш із git init — ти починаєш із помилки. отже:

~> mkdir -p ~/projects/python/tutorials/yart
~> cd ~/projects/python/tutorials/yart
.../yart> git init
.../yart> git config user.name "tivasyk"
.../yart> git config user.email "tivasyk@gmail.com"

перший файл, перший коміт:

.../yart> touch readme.md
.../yart> git add .
.../yart> git commit -m "add: readme.md /ti"

складно? довго? ніт. відтепер головне не забувати:

  • перед початком якоїсь значної зміни в коді (приміром, початок розділу) — створити гілку: git switch -c "part-0";
  • змінивши код — індексувати й закомітити: git add <файл>; git commit -m "нотатка";
  • наприкінці якогось етапу (приміром, кінець розділу) — переконатися, що код працює, і злити гілку до основної: git switch master; git merge -m "нотатка" <гілка>;
  • якщо щось пішло не за планом — відкотити зміни (але про це — коли виникне потреба).

0.0.2 репозиторій на codeberg

це опційно (хіба що над проєктом працює команда?), але я додам codeberg, щоби мати змогу за потреби продовжувати туторіал на різних комп'ютерах. не github, тому що give up github. потрібно створити новий проєкт (tutorial.yart), і отримати підказку з налаштування його як віддаленого репозиторія (remote) в локальному git (url для ssh, тому що вже маю налаштовані криптоключі ssh):

.../yart> git remote add origin git@codeberg.org:tivasyk/tutorial.yart.git
.../yart> git push -u origin master

і це, по суті, всі налаштування; тепер стан проєкту можна бачити на сайті, якщо не забувати періодично «штовхати» локальні зміни на сервер: git push.

0.1 встановлення python та залежностей

на gnu/linux, ще й на збірці для щоденного використання, відносно свіженький python вже встановлено:

.../yart> python --version
Python 3.11.6

для туторіала знадобляться дві залежності: numpy (швидше за все вже є «з коробки») та tcod (якого, швидше за все, немає):

.../yart> pip list | grep numpy
numpy 1.26.3
.../yart> pip list | grep tcod

тут починаються типові для python складнощі: встановлювати глобально, чи налаштовувати віртуальне середовище (venv) для проєкту? що краще, як менеджити те й інше локально, і що буде, коли поділишся кодом з кимось іншим? good luck в пошуках детальної але зрозумілої підказки, — я пробував і здався, тому й взявся експериментувати. найперше — нова «експериментальна» гілка git:

.../yart> git switch -c "part0/setup"

спроба встановити модуль tcod за допомогою pip (класичний, правильний спосіб):

.../yart> pip install tcod
error: externally-managed-environmen×ばつ This environment is externally managed
╰─> To install Python packages system-wide, try 'pacman -S
 python-xyz', where xyz is the package you are trying to
 install.
 If you wish to install a non-Arch-packaged Python package,
 create a virtual environment using 'python -m venv path/to/venv'.
 Then use path/to/venv/bin/python and path/to/venv/bin/pip.
 If you wish to install a non-Arch packaged Python application,
 it may be easiest to use 'pipx install xyz', which will manage a
 virtual environment for you. Make sure you have python-pipx
 installed via pacman.
note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

принаймні повідомлення про помилки детальні й зрозумілі. в оф. репозиторії tcod'у немає, але знайшовся в aur'і:

.../yart> pacman -Ss python-tcod
.../yart> pamac search python-tcod
python-tcod 13.8.1-1 AUR
 High-performance Python port of libtcod

хоч встановлення з aur — це гра в росіянську рулетку, але спробую:

.../yart> pamac install python-tcod
...
==> Перевіряємо підписи файлів початкового коду за допомогою gpg...
 python-tcod git repo ... ПОМИЛКА (невідомий публічний ключ 5814977902B194CC)
 libtcod git repo ... ПОМИЛКА (невідомий публічний ключ 5814977902B194CC)
==> ПОМИЛКА: Один або кілька PGP-підписів неможливо перевірити!
Помилка: Не вдалося зібрати python-tcod

всі залежності з оф. репів встановилися, але сам tcod і його бібліотека — ні через невідомі ключі gpg. підглянути id необхідних ключів можна в PKGBUILD на aur.manjaro.org (python-tcod, libtcod), але імпортувати з сервера ключів — дзуськи:

.../yart> gpg -recv-keys 9EF1E80F3817BC043097A7C15814977902B194CC
gpg: помилка під час спроби отримання даних з сервера ключів: Немає даних

дупа? не зовсім... в одному з двох PKGBUILD є таке:

## GPG key: https://github.com/hexdecimal.gpg

як грати в ту росіянську рулетку, то вже з шампанським:

wget https://github.com/hexdecimal.gpg
...
gpg --import hexdecimal.gpg
pg: ключ 5814977902B194CC: імпортовано відкритий ключ «Kyle Benesch (Git signing key) <4b796c65+github@gmail.com>»

спробую ще раз встановити python-tcod — тепер перевірку gpg проходить, але нова проблема...

.../yart> pamac install python-tcod
...
==> Починаємо prepare()...
Підмодуль "libtcod" (https://github.com/libtcod/libtcod) зареєстровано для шляху "libtcod"
Клонування в "/var/tmp/pamac-build-tivasyk/python-tcod/src/python-tcod/libtcod"..
попередження: --depth ігнорується у локальних клонах; використовуйте file:// замість цього.
збій: засіб передачі "file" не дозволений
збій: не вдалося клонувати "/var/tmp/pamac-build-tivasyk/python-tcod/src/libtcod" у шлях підмодуля "/var/tmp/pamac-build-tivasyk/python-tcod/src/python-tcod/libtcod"
Не вдалося клонувати "libtcod". Запланована повторна спроба
Клонування в "/var/tmp/pamac-build-tivasyk/python-tcod/src/python-tcod/libtcod"..
попередження: --depth ігнорується у локальних клонах; використовуйте file:// замість цього.
збій: засіб передачі "file" не дозволений
збій: не вдалося клонувати "/var/tmp/pamac-build-tivasyk/python-tcod/src/libtcod" у шлях підмодуля "/var/tmp/pamac-build-tivasyk/python-tcod/src/python-tcod/libtcod"
Не вдалося клонувати "libtcod" вдруге, переривання
==> ПОМИЛКА: Стався збій у prepare().
 Припинення...

гхм. а якщо окремо?

.../yart> pamac install libtcod
...
Операція успішно завершена.

але... намарне: python-tcod не збирається, з тією самою помилкою. якби я завчасу й уважно поглянув до PKGBUILD для python-tcod, там є таке:

## Cannot use libtcod as dependency; statically linked

тому...

.../yart> pamac remove libtcod

на цьому етапі я здаюсь... час спробувати другу підказку — встановити tcod у віртуальному середовищі (venv), а не глобально. власне, принаймні одна хороша на позір підказка прямо каже створювати свій venv для кожного проєкту на python, а не намагатися встановлювати залежності глобально.

.../yart> python -m venv ./venv
.../yart> ls -l
-rw-r--r-- 1 tivasyk tivasyk 12663 січ 18 17:10 readme.md
drwxr-xr-x 5 tivasyk tivasyk 4096 січ 18 17:17 venv
.../yart> source ./venv/bin/activate
.../yart> which python
/home/tivasyk/projects/python/tutorials/yart/venv/bin/python
.../yart> which pip
/home/tivasyk/projects/python/tutorials/yart/venv/bin/pip

source ./venv/bin/activate доведеться робити щоразу, працюючи з проєктом; до того ж, якщо встановлено powerline чи подібний покращувач командного рядка та інтерфейсу vim тощо на основі python — активування venv ламає ці прикраси, тож... ласкаво просимо до світу простого програмування на python, ага. тека venv не є частиною проєкту, варто додати до .gitignore, щоби git не відслідковував змін в ній:

.../yart> vim .gitignore
# ----- >8 -----
venv/
# ----- 8< -----

і нарешті те, заради чого знадобилося окреме віртуальне середовище:

.../yart> pip install tcod
...
Successfully installed cffi-1.16.0 numpy-1.26.3 pycparser-2.21 tcod-16.2.2 typing-extensions-4.9.0

згідно підказки, tcod (версії >= 11.13)є головною залежністю для проєкту, тож треба додати до requirements.txt:

.../yart> echo "tcod>=11.13" > requirements.txt

підказка каже встановити libsdl2-dev, — але з великою долею ймовірності бібліотеки sdl2 вже є в системі (і навряд чи треба ставити саме libsdl2-dev — скоріше за все метапакунок sld2 підтягне все, що треба):

.../yart> pamac search sdl2
...
sdl2 2.28.5-1 [Встановлено] extra
 A library for portable low-level access to a video framebuffer, audio output, mouse, and keyboard (Version 2)

нарешті, для малювання в псевдоконсоль знадобиться файл з графічним зображенням шрифта (dejavu10x10_gs_tc.png), тільки я не хочу зберігати його в корені проєкту:

.../yart> mkdir resources
.../yart> wget -P ./resources/ https://rogueliketutorials.com/images/dejavu10x10_gs_tc.png

майже все готово для першого файлу на пайтоні, час закомітити, що є:

.../yart> git status
а гілці part0/setup
Зміни, не додані до майбутнього коміту:
 (скористайтесь "git add <файл>...", щоб оновити майбутній коміт)
 (скористайтесь "git restore <файл>...", щоб скасувати зміни в робочій директорії)
 змінено: readme.md
Невідстежувані файли:
 (скористайтесь "git add <файл>...", щоб додати до майбутнього коміту)
 .gitignore
 requirements.txt
 resources/
не додано жодних змін до коміту (скористайтесь "git add" та/або "git commit -a")
.../yart> git add .
.../yart> git commit -m "add: підготовка, встановлення залежностей /ti"

перевірка, і злити гілку part0/setup до master:

.../yart> git log --oneline
ce1f704 (HEAD -> part0/setup) add: підготовка, встановлення залежностей /ti
ae70c53 (origin/master, master) add: розділ про git до readme.md /ti
9f131cd add: readme.md /ti
.../yart> git switch master
.../yart> git merge part0/setup

0.2 hello tcod

для цієї невеличкої частини я зроблю окрему гілку git:

.../yart> git switch -c "part0/hello"

нарешті перший файл з пайтоном; я прибрав кранчбенг (#!/usr/bin/env python3), бо він, скоріш за все, не працюватиме до пуття (без venv) — ще раз, ласкаво просимо до світу простого програмування з python:

.../yart> vim main.py
# ----- >8 -----
import tcod
def main():
 print("Hello tcod!")
if __name__ == "__main__":
 main()
# ----- 8< -----
.../yart> python main.py
Hello tcod!

схоже, що tcod імпортується і примітивний hello world виконується, можна зберегти стан проєкту в git, і цього разу заштовхнути зміни на codeberg:

.../yart> tree --gitignore
.
├── main.py
├── readme.md
├── requirements.txt
├── resources
│  └── dejavu10x10_gs_tc.png
└── venv
3 directories, 4 files
.../yart> git status
На гілці part0/hello
Зміни, не додані до майбутнього коміту:
 (скористайтесь "git add <файл>...", щоб оновити майбутній коміт)
 (скористайтесь "git restore <файл>...", щоб скасувати зміни в робочій директорії)
 змінено: readme.md
Невідстежувані файли:
 (скористайтесь "git add <файл>...", щоб додати до майбутнього коміту)
 main.py
не додано жодних змін до коміту (скористайтесь "git add" та/або "git commit -a")
.../yart> git add .
.../yart> git commit -m "add: main.py і тест імпорту tcod /ti"
.../yart> git push
.../yart> git switch master && git merge part0/hello
.../yart> git push

частина 1 - малювання @ і пересування

url: https://rogueliketutorials.com/tutorials/tcod/v2/part-1/

1.0 малювання персонажа

найперше: новий розділ — нова гілка git:

.../yart> git switch -c "part1/character"

перші розділи цієї підказки мені сподобались: автор не полінувався детально пояснювати, що і навіщо він робить; на жаль, далі більшість коментарів зведуться до «що» без детальних «чому», і це дуже погіршить мені враження. фрагмент про використання tcod для виводу символа з тайлсету дуже простий: лише невеликий експеримент із main.py, і в кутику нового чорного вікра виводиться класичне @.

суто для документування додам теку screenshots до проєкту. перший зняток — ввесь екран зі зручним розташуванням вікон для розваг із підказкою yart, але надалі ворушити мишкою заради знятків — забагато мороки, тому буде scrot -s screenshots/Screenshot_$(date +'%Y%m%d_%H%M%S').png.

.../yart> mkdir screenshots

середовище для розваг з підказкою yart

все працює, можна закомітити стан проєкту, і перейти до цікавіших експериментів:

.../yart> git status
а гілці part1/character
Зміни, не додані до майбутнього коміту:
 (скористайтесь "git add <файл>...", щоб оновити майбутній коміт)
 (скористайтесь "git restore <файл>...", щоб скасувати зміни в робочій директорії)
 змінено: main.py
 змінено: readme.md
Невідстежувані файли:
 (скористайтесь "git add <файл>...", щоб додати до майбутнього коміту)
 screenshots/
не додано жодних змін до коміту (скористайтесь "git add" та/або "git commit -a")
.../yart> tree --gitignore
.
├── main.py
├── readme.md
├── requirements.txt
├── resources
│  └── dejavu10x10_gs_tc.png
├── screenshots
│  └── Screenshot_20240121_090900.png
└── venv
4 directories, 5 files
.../yart> git add .
.../yart> git commit -m "mod: main.py виводить @ для персонажа /ti"
...

1.1.1 вибір іншого тайлсету

новий експеримент — нова гілка:

.../yart> git switch -c "part1/tileset"

для простоти не штовхатиму експериментальні гілки (part0/..., part1/...) на codeberg, лише master. отже, до розваг: main.py, завантаження тайлсету (шрифта для малювання ascii-подібної графіки):

tileset = tcod.tileset.load_tilesheet(
 "resources/dejavu10x10_gs_tc.png", # шлях до файлу з тайлсетом
 32, 8, # кількість колонок, рядків у тайлсеті
 tcod.tileset.CHARMAP_TCOD # таблиця символів (порядок) у тайлсеті
 )

підказка використовує таблицю CHARMAP_TCOD, але документація каже, що вона застаріла (deprecated); альтернатива — класична CHARMAP_CP437, яку використовує dwarf fortress. я хочу одразу використати якийсь інший тайлсет 10x10:

.../yart> wget -O resources/buddy-graphical.png https://dwarffortresswiki.org/index.php/File:Buddy--graphical.png

зміни до main.py:

tileset = tcod.tileset.load_tilesheet(
 "resources/buddy-graphical.png", # шлях до файлу з тайлсетом
 16, 16, # кількість колонок, рядків у тайлсеті
 tcod.tileset.CHARMAP_CP437 # таблиця символів (порядок) у тайлсеті
 )

якщо все гаразд, головне вікно main.py не повинно відрізнятися від попереднього, за винятком смайлика на місці @:

.../yart> python main.py
Traceback (most recent call last):
 File "/home/tivasyk/projects/python/tutorials/yart/./main.py", line 34, in <module>
 main()
 File "/home/tivasyk/projects/python/tutorials/yart/./main.py", line 8, in main
 tileset = tcod.tileset.load_tilesheet(
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/home/tivasyk/projects/python/tutorials/yart/venv/lib/python3.11/site-packages/tcod/tileset.py", line 348, in load_tilesheet
 _raise_tcod_error()
 File "/home/tivasyk/projects/python/tutorials/yart/venv/lib/python3.11/site-packages/tcod/_internal.py", line 64, in _raise_tcod_error
 raise RuntimeError(ffi.string(lib.TCOD_get_error()).decode("utf-8"))
RuntimeError: libtcod 1.24.0 libtcod/src/libtcod/tileset.c:397
Error loading font image: /home/tivasyk/projects/python/tutorials/yart/resources/buddy-graphical.png
incorrect PNG signature, it's no PNG or corrupted

сам винуватий: забув перевірити, чи справді wget затяг файл png із тайлсетом, а не html-заглушку чи щось подібне... перезавантажую з правильним посиланням, і тепер все гаразд:

.../yart> wget -O resources/buddy-graphical.png https://dwarffortresswiki.org/images/f/fb/Buddy--graphical.png

можна закомітити гілку:

.../yart> git status # стан поточної гілки, простий перелік змінених файлів?
...
.../yart> git add . # індексувати всі змінені/додані файли в проєкті
.../yart> git commit -m "mod: зміна тейлсету на buddy-graphical з dwarffortresswiki.org /ti"
.../yart> git log --oneline # журнал комітів/гілок
* 079a55f (HEAD -> part1/tileset) mod: зміна тайлсету на buddy-graphical з dwarffortresswiki.org /ti
* 330e40a (origin/part1/character, part1/character) mod: main.py виводить @ для персонажа /ti
* 455f7e4 (origin/master, part0/hello, master) add: main.py і тест імпорту tcod /ti
* 0bb05b2 (part0/setup) add: підготовка, встановлення залежностей /ti
* ae70c53 add: розділ про git до readme.md /ti
* 9f131cd add: readme.md /ti
.../yart> git show --oneliune --stat # детально про останній коміт, з переліком змінених файлів
079a55f (HEAD -> part1/tileset) mod: зміна тайлсету на buddy-graphical з dwarffortresswiki.org /ti
 main.py | 6 +++---
 readme.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 resources/buddy-graphical.png | Bin 0 -> 7226 bytes
 3 files changed, 102 insertions(+), 5 deletions(-)

1.2 пересування персонажа

нова гілка (я просто люблю мікро-гілки і мікро-коміти: на роботі це дозволяє мінімізувати час між зливаннями гілок до master і мінімізувати роботу по розбиранню конфліктів зливання, якщо кілька людей працюють над проєктом):

.../yart> git switch -c "part1/movement"

я легко забуваю команди git (крім add і commit, звісно), тому зазвичай роблю собі простенький псевдонім git stat, котрий поєднує їх:

.../yart> git config --global alias.stat '!git status; echo; git show --oneline --stat; git log --oneline -n5 HEAD^'
.../yart> git stat
На гілці part1/movement
Зміни, не додані до майбутнього коміту:
 (скористайтесь "git add <файл>...", щоб оновити майбутній коміт)
 (скористайтесь "git restore <файл>...", щоб скасувати зміни в робочій директорії)
 змінено: readme.md
не додано жодних змін до коміту (скористайтесь "git add" та/або "git commit -a")
079a55f (HEAD -> part1/movement, part1/tileset) mod: зміна тайлсету на buddy-graphical з dwarffortresswiki.org /ti
 main.py | 6 +++---
 readme.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 resources/buddy-graphical.png | Bin 0 -> 7226 bytes
 3 files changed, 102 insertions(+), 5 deletions(-)
330e40a (origin/part1/character, part1/character) mod: main.py виводить @ для персонажа /ti
455f7e4 (origin/master, part0/hello, master) add: main.py і тест імпорту tcod /ti
0bb05b2 (part0/setup) add: підготовка, встановлення залежностей /ti
ae70c53 add: розділ про git до readme.md /ti
9f131cd add: readme.md /ti

тут надходить момент, коли підказка yart починає накопичувати файли .py, котрі перехресно імпортують класи одне одного, або код повністю, — і мені кардинально не подобається, що автор підказки то кидає ці файли в корінь проєкту, то використовує теку components для модулів; я одразу створю теку components і зберігатиму весь код на пайтоні там як пакунок:

.../yart> mkdir components
.../yart> touch components/__init__.py
.../yart> vim components/actions.py
...
.../yart> vim components/input_handlers.py
...

запуск main.py тепер генерує кеш пакунку components (підтека components/__pycache__), який мені не потрібно зберігати як частину проєкту, до того ж файли там мають абсолютно гидотне розширення (.pyc), тому:

.../yart> tree --gitignore
.
├── components
│  ├── actions.py
│  ├── __init__.py
│  ├── input_handlers.py
│  └── __pycache__
│  ├── actions.cpython-311.pyc
│  ├── __init__.cpython-311.pyc
│  └── input_handlers.cpython-311.pyc
├── main.py
├── readme.md
├── requirements.txt
├── resources
│  ├── buddy-graphical.png
│  └── dejavu10x10_gs_tc.png
├── screenshots
│  └── Screenshot_20240121_090900.png
└── venv
.../yart> vim .gitignore
# ----- >8 -----
venv/
**/__pycache__/
# ----- 8< -----

з важливого: фрагменти components/input_handlers.py з використанням tcod.event.K_UP тощо генерують помилки, тому що константи з кодами клавіш змінилися, треба tcod.event.KeySym.UP тощо. на додачу, в мене на клавіатурі немає додаткової ані цифрової частини, ані навіть виділених клавіш керування курсором, — я хочу керувати за допомогою hjkl, плюс yu та bn для діагонального руху (як в cataclysm: dda), тому:

if key == tcod.event.KeySym.UP or key == tcod.event.KeySym.k:
 action = MovementAction(dx = 0, dy = -1) # догори
elif key == tcod.event.KeySym.DOWN or key == tcod.event.KeySym.j:
 action = MovementAction(dx = 0, dy = 1) # додолу
elif key == tcod.event.KeySym.LEFT or key == tcod.event.KeySym.h:
 action = MovementAction(dx = -1, dy = 0) # ліворуч
elif key == tcod.event.KeySym.RIGHT or key == tcod.event.KeySym.l:
 action = MovementAction(dx = 1, dy = 0) # праворуч
elif key == tcod.event.KeySym.y:
 action = MovementAction(dx = -1, dy = -1) # догори-ліворуч
elif key == tcod.event.KeySym.u:
 action = MovementAction(dx = 1, dy = -1) # догори-праворуч
elif key == tcod.event.KeySym.b:
 action = MovementAction(dx = -1, dy = 1) # додолу-ліворуч
elif key == tcod.event.KeySym.n:
 action = MovementAction(dx = 1, dy = 1) # додолу-праворуч
elif key == tcod.event.KeySym.ESCAPE:
 action = EscapeAction() # вихід з гри

перевірка, все працює, можна комітити і зливати з master'ом, бо це кінець розділу:

.../yart> git add .
.../yart> git commit -m "mod: персонаж може рухатися /ti"
.../yart> git push
.../yart> git switch master && git merge part1/movement
.../yart> git push

частина 2 - базовий клас, функції промальовування й мапа

url: https://rogueliketutorials.com/tutorials/tcod/v2/part-2/

2.0 нагадування про venv, огляд гілок git

після перезавантаження комп'ютера віртуальне середовище саменьке не активується (хіба що скористатися conda чи чимось подібним), доводиться пам'ятати й робити руцями:

.../yart> python main.py
Traceback (most recent call last):
 File "/home/tivasyk/projects/python/tutorials/yart/main.py", line 2, in <module>
 import tcod
ModuleNotFoundError: No module named 'tcod'
# перевірка (повинно вказувать на python з вірт. середовища, не глобальний)
.../yart> which python
/usr/bin/python
# активувати vevn
.../yart> source venv/bin/activate
.../yart> which python
home/tivasyk/projects/python/tutorials/yart/venv/bin/python

щодо гілок git: наявність логіки в назвах, групування («part0/...», «part1/...») полегшують роботу з великими проєктами; періодично треба перевіряти, чи всі гілки синхронізовано з віддаленим сховищем, і якщо ні:

.../yart> git push --all

нові гілки не хочуть синхронізуватися, але git щоразу нагадує про --set-upstream:

.../yart> git push --set-upstream origin part2/reminders

2.1 базовий клас (entity)

нова гілка, новий файл:

.../yart> git checkout -b "part2/entity" # повний відповідник git switch -c "part2/entity"
.../yart> touch components/entity.py

ха, перше вживання docstring в цій підказці. новий компонент: components/entity.py; отут в мене починають виникати питання щодо архітектури проєкту:

  • чому потрібен цілий окремий файл лише для базового класу Entity?
  • чому для кольорів (Tuple[int, int, int]) та координат (x: int, y: int) не створити відповідні типи чи класи?

поки що роблю, як каже підказка:

.../yart> vim components/entity.py
...

періодично vim збентежено звітує про неочікувані тимчасові файли чи свопи в теці проєкту... нагадуючи мені, що варто додати дещо в глобальний git/ignore:

.../yart> vim ~/.config/git/ignore
# ----- >8 -----
**/*~
**/*.swp
# ----- 8< -----

коли логічну частину підказки пройдено — не забути закомітити:

.../yart> git add components/entity.py
.../yart> git commit -m "add: entity.py і базовий клас /ti"
.../yart> git push --set-upstream origin part2/entity
.../yart> git switch master && git merge part2/entity
.../yart> git push

2.2 рушій (engine)

нова гілка, новий файл:

.../yart> git switch part2/engine
.../yart> touch components/engine.py

як я собі розумію з визначення ігрового рушія, engine.py мав би поєднати різні компоненти проєкту в якусь систему; наразі він відповідає лише за:

  • обробку подій (натискання з клавіатури): handle_events();
  • запуск промальовування екрана: render().

трохи підглянувши наперед, я не певен, що ця підказка є хорошою демонстрацією ідеї, на жаль, — все буде розкидано по модулях і теках, ще й із перетасуванням через декілька рефакторингів. але побачимо, на все свій час. принаймні, основний цикл у main.py спростився й абстрагувався від деталей завдяки рушію:

while True:
 engine.render(console = root_console, context = context)
 events = tcod.event.wait()
 engine.handle_events(events)

можна закомітити:

.../yart> git add components/engine.py
.../yart> git commit -m "add: engine.py для обробки подій та промальовки /ti"
.../yart> git push --set-upstream origin part2/engine
.../yart> git switch master && git merge part2/engine
.../yart> git push

2.3 плитки (tiles)

гілка, файл:

.../yart> git switch -c part2/tiles
.../yart> touch components/tiles.py # в підказці tile_types.py

тут не все зрозміло — мені треба (пере)читати документацію numpy, мабуть:

  • numpy.dtype() реєструє тип graphic_dt, що зберігає символ і два кольори (символу й тла);
  • інший numpy.dtype() реєструє тип tile_dt, що зберігає дві властивості плитки (прохідна? прозора?) і graphic_dt (вище);
  • numpy.array() допомагає запакувати властивості плитки, подані як розрізнені аргументи, у... 0-розмірний (лише один елемент!) масив:
return np.array((walkable, transparent, dark), dtype = tile_dt)

цей момент — необхідність 0-розмірного масиву, — ніяк не пояснено в підказці. власне, тут і починаються мої проблеми з якістю цієї публікації саме як підказки (згадував про це на початку): автор продовжує коментувати примітивні речі, ігноруючи потенційно цікаві питання архітектури; фу. відповідь на моє питання, мабуть, ховається у фразі «a compound dtype is never callable», але хотілося б детальнішого пояснення, бо планування структури для зберігання властивостей плиток, з яких складатиметься мапа рівня — нмсд, один із ключових елементів, принаймні в цьому розділі.

нарешті, заради чого знадобився tiles.py: створення об'єктів для позначення підлоги (floor) та стіни (wall). коміт:

.../yart> git add components/tiles.py
.../yart> git commit -m "add: components/tiles.py /ti"
.../yart> git push --set-upstream origin part2/tiles
.../yart> git switch master && git merge part2/tiles
.../yart> git push

2.4 мапа

гілка, новий файл:

.../yart> git switch -c part2/game_map
.../yart> touch components/game_map.py

майбутня мапа рівня: numpy.full() заповнює масив width x height плитками підлоги (floor); зі справді цікавого: order = "F" (стиль fortran, column-major) — знову ж, автор не пояснює, чи це важливо в даному випадку, чи просто питання звички для нього?

self.tiles = np.full((width, height), fill_value = tiles.floor, order = "F")

коли всі game_map.py створено і всі необхідні зміни до main.py та engine.py зроблено... тиць — дзуськи:

.../yart> python main.py
/home/tivasyk/projects/python/tutorials/yart/components/tiles.py:16: FutureWarning: In the future `np.bool` will be defined as the corresponding NumPy scalar.
 ("walkable", np.bool),
Traceback (most recent call last):
 File "/home/tivasyk/projects/python/tutorials/yart/main.py", line 6, in <module>
 from components.engine import Engine
 File "/home/tivasyk/projects/python/tutorials/yart/components/engine.py", line 11, in <module>
 from components.game_map import GameMap
 File "/home/tivasyk/projects/python/tutorials/yart/components/game_map.py", line 6, in <module>
 import components.tiles # в підказці: tile_types.py
 ^^^^^^^^^^^^^^^^^^^^^^^
 File "/home/tivasyk/projects/python/tutorials/yart/components/tiles.py", line 16, in <module>
 ("walkable", np.bool),
 ^^^^^^^
 File "/home/tivasyk/projects/python/tutorials/yart/venv/lib/python3.11/site-packages/numpy/__init__.py", line 324, in __getattr__
 raise AttributeError(__former_attrs__[attr])
AttributeError: module 'numpy' has no attribute 'bool'.
`np.bool` was a deprecated alias for the builtin `bool`. To avoid this error in existing code, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.
The aliases was originally deprecated in NumPy 1.20; for more details and guidance see the original release note at:
 https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations. Did you mean: 'bool_'?

страшно? мені ні. отже, numpy.bool (в коді np.bool бо numpy імпортовано як np) більше нема, треба використати рідний пайтонівський bool, або ж (причини?) numpy.bool_; проблема в components/tiles.py, але про всяк випадок перевірю grep'ом, і «ремонтую»:

.../yart> grep -r -l --exclude-dir venv "np.bool" ./*
components/tiles.py
readme.md
.../yart> vim components/tiles.py
...

друга спроба:

.../yart> python main.py
Traceback (most recent call last):
 File "/home/tivasyk/projects/python/tutorials/yart/main.py", line 60, in <module>
 main()
 File "/home/tivasyk/projects/python/tutorials/yart/main.py", line 37, in main
 game_map = GameMap(map_width, map_height)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/home/tivasyk/projects/python/tutorials/yart/components/game_map.py", line 11, in __init__
 self.tiles = np.full((width, height), fill_value = tiles.floor, order = "F")
 ^^^^^
NameError: name 'tiles' is not defined

соромно, але забув імпортувати tiles до game_map.py? так і є. третя спроба — працює, попри ще одне попередження, яке легко «поремонтувати»:

.../yart> python main.py
/home/tivasyk/projects/python/tutorials/yart/components/game_map.py:22: FutureWarning: This attribute has been renamed to `rgb`.
 console.tiles_rgb[0:self.width, 0:self.height] = self.tiles["dark"]
.../yart> vim components/game_map.py
# ----- >8 -----
 def render(self, console: Console) -> None:
 # console.tiles_rgb[0:self.width, 0:self.height] = self.tiles["dark"]
 console.rgb[0:self.width, 0:self.height] = self.tiles["dark"]
# ----- 8< -----

стіна на мапі рівня

можна комітити й злити гілку з main. насправді я роблю окремий коміт після змін до кожного файлу в проєкті (так простіше згодом орієнтуватися в змінах), і окремо — після поновлення readme.md, тож лишається «виштовхнути» всі зміни в гілці part2/game_map на codeberg, злити з main і «заштовхнути» її також:

.../yart> git push --set-upstream origin part2/game_map
.../yart> git switch master && git merge part2/game_map
.../yart> git push

2.5 дії (actions)

це ще не фінал другої частини, тож нова гілка:

.../yart> git switch -c "part2/actions"
.../yart> vim components/actions.py

цікаві зміни до components/actions.py. автор підказки почина щоразу імпортувати «з майбутнього»:

from __future__ import annotations

але... документація каже, що це спосіб python використати можливості модулів до офіційного релізу? їпать, ще раз ласкаво просимо до захоплюючого світу програмування на пайтоні, рукалице. оскільки я вже на python 3.11 (а annotations релізнули в 3.7), це можна замінити на import annotations, правильно? далі:

from typing import TYPE_CHECKING
if TYPE_CHECKING:
 from components.engine import Engine
 from components.entity import Entity

в загальних рисах розумію, що тут відбувається (імпортуємо класи заради статичної перевірки типів для класів, що їх використовують), але ще треба багато читати документацію, перш ніж навчусь розумно це використовувати, а не просто ліпити там, де це робить автор.

ось справді цікаве: в actions.py (і також у engine.py) тепер є таке:

if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
 ...

але game_map.tiles формується так:

class GameMap:
 def __init__(self, width: int, height: int):
 ...
 self.tiles = np.full((width, height), fill_value = tiles.floor, order = "F")

бентежить адресування ["walkable"][dest_x, dest_y]; знову-таки, я розумію, що відбувається: є двовимірний масив tiles з елементів tile_dt, і тут адресується значення "walkable" елемента dest_x, dest_y... але сам би я переставив би адреси місцями — і отримав би помилку. розуміння, мабуть, ховається в тексті про структуровані масиви numpy.

невелике спрощення engine.py, і фінал розділу 2:

.../yart> git push --set-upstream origin part2/actions
.../yart> git switch master && git merge part2/actions
.../yart> git push

частина 3 - генерація підземелля

url: https://rogueliketutorials.com/tutorials/tcod/v2/part-3/

3.0 корисне читання

для того, хто читає це заради вступу до git: знаю, поки що не ясно, заради чого всі ці git add, git commit, git push... але рано чи пізно я зроблю десь критичну помилку й доведеться відновлювати файли з попередньої гілки чи коміту, або порівнювати код між двома великими змінами — тоді буде про це. наразі — невеликий список корисного тематичного читання.

3.1 генератор підземелля (procgen)

нова гілка, новий файл:

.../yart> git switch part3/procgen
.../yart> touch components/procgen.py

з цікавого для мене, в components/procgen.py — заповнення масиву numpy скалярним значенням «в один рядок»:

dungeon.tiles[room_1.inner] = tiles.floor

я би писав цикл, бо «стара школа», — pascal. оці особливості, що роблять python таким «читабельним» і «простим для розуміння» для нових поколінь, мене бентежать; але треба звикати:

.../yart> python
>>> import numpy
# ініціалізація масиву 3х3 нулями
test = numpy.full((3, 3), 0)
# заповнення всього масиву одиницями
test[slice(0,3), slice(0,3)] = 1
# заповнення першого рядка двійками
test[0, slice(0,3)] = 2
# заповнення останньої колонки трійками
test[slice(0,3), -1] = 3
# перевірка
print(test)
[[2 2 3]
 [1 1 3]
 [1 1 3]]

далі, функція tunnel_between() практично вся цікава:

 def tunnel_between(start: Tuple[int, int], end: Tuple[int, int]) -> Iterator[Tuple[int, int]]:
 x1, y1 = start
 x2, y2 = end
 if random.random() < 0.5: # ймовірність ~1/2: тунель гориз., потім верт.
 corner_x, corner_y = x2, y1
 else: # ймовірність ~1/2: тунель верт., потім гориз.
 corner_x, corner_y = x1, y2
 # генерування списку координат L-подібного коридоку (bresenham тут — як гармата по горобцях)
 for x, y in tcod.los.bresenham((x1, y1), (corner_x, corner_y)).tolist():
 yield x, y
 for x, y in tcod.los.bresenham((corner_x, corner_y), (x2, y2)).tolist():
 yield x, y

про пітонічні генератори та yield варто почитати окремо. не розумію, звідки в автора ця фіксація на Tuple[int, int]? він вже імпортував typing, то чому не...

from typing import Tuple, TypeAlias
...
coords: TypeAlias = Tuple[int, int] # координати (x, y)
rgb: TypeAlias = Tuple[int, int, int] # колір rgb (r, g, b)
...
def tunnel_between(start: coords, end: coords) -> Iterator[coords]:
 ...

код працює, відмальовуючи дві кімнати й тунель. проміжний фініш:

.../yart> git push --set-upstream origin part3/procgen
.../yart> git switch master && git merge part3/procgen
.../yart> git push

3.2 кімнати, зневадження коду

.../yart> git switch -c part3/rooms

за автором повністю переписую generate_dungeon() для процедурної генерації кімнат із коридорами між ними, отримую помилку — десь у коді я викликаю generate_dungeon(x, y) з індексом y більшим за висоту мапи...

Traceback (most recent call last):
 File "/home/tivasyk/projects/python/tutorials/yart/main.py", line 73, in <module>
 main()
 File "/home/tivasyk/projects/python/tutorials/yart/main.py", line 43, in main
 game_map = generate_dungeon(
 ^^^^^^^^^^^^^^^^^
 File "/home/tivasyk/projects/python/tutorials/yart/components/procgen.py", line 100, in generate_dungeon
 dungeon.tiles[x, y] = tiles.floor
 ~~~~~~~~~~~~~^^^^^^
IndexError: index 50 is out of bounds for axis 1 with size 50

так не повинно бути; поверхневий перегляд коду не дав відповіді на питання, звідки можуть братися завеликі індекси. час розчехляти зневаждувач (gdb) і розставляти breakpoint() (і додаткові print(), тому що читабельність = швидкість):

else:
 print("Cutting a corridor between:", rooms[-1].center, new_room.center)
 breakpoint() # DEBUG
 for x, y in tunnel_between(rooms[-1].center, new_room.center):
 dungeon.tiles[x, y] = tiles.floor

швидко виявляється, що час від часу код генерує кімнати з розміром більшим, ніж передбачено проєктною документацією (в цьому прикладі остання згенерована кімната має y1 69 за максимуму 50):

Generated coords for a new room: 1 25 10 6
Cutting a new room: 1 25 10 6
Generated coords for a new room: 34 20 9 8
Cutting a new room: 34 20 9 8
Cutting a corridor between: (6, 28) (38, 24)
> /home/tivasyk/projects/python/tutorials/yart/components/procgen.py(103)generate_dungeon()
-> for x, y in tunnel_between(rooms[-1].center, new_room.center):
(Pdb) c
...
Generated coords for a new room: 24 69 7 6
Cutting a new room: 24 69 7 6
Cutting a corridor between: (22, 5) (27, 72)
> /home/tivasyk/projects/python/tutorials/yart/components/procgen.py(103)generate_dungeon()
-> for x, y in tunnel_between(rooms[-1].center, new_room.center):
(Pdb)

координати і розміри кімнат генерує randint:

y = random.randint(0, dungeon.width - room_width - 1)
x = random.randint(0, dungeon.height - room_height - 1)

рукалице: якийсь бовдур (я) переплутав місцями x та y. можна прибирати зайві print() та breakpoint() з коду, все працює.

генеровані кімнати з коридорами

для проєкту, ідея котрого жевріє в голові, такий простенький генератор не підходить (хіба що для якихось частин), але він цікавий простотою й винахідливістю підходу. кінець частини 3:

.../yart> git push --set-upstream origin part3/rooms
.../yart> git switch master && git merge part3/rooms
.../yart> git push

частина 4 - зорове поле

url: https://rogueliketutorials.com/tutorials/tcod/v2/part-4/

4.1 видимість плиток

гілка:

.../yart> git switch c "part4/fov"

автор зберігає інформацію про те, які плитки на мапі персонаж бачить зараз, і які вже бачив колись (пам'ятає), у двох повномірних (такого ж розміру, як уся мапа!) масивах в класі, котрий зберігає саму мапу:

self.tiles = np.full((width, height), fill_value = tiles.wall, order = "F") # вся мапа
self.visible = np.full((width, height), fill_value = False, order = "F") # видимість
self.explored = np.full((width, height), fill_value = False, order = "F") # «пам'ять»

перевага — це просто, тому добре підходить для такого простого проєкту з невеликою мапою. недоліків бачу декілька:

  • витратно (пам'ять, час) на великих мапах — можливо, поле зору ефективніше зберігати в меншому масиві?
  • видимість зберігається (а відтак може швидко відмальовуватися) лише для головного персонажа; на перший погляд не проблема? але припустімо я хочу додати до гри джерела світла, і використовувати їхні «зорові поля» для промальовування освітлених ділянок? потрібні масиви видимих плиток як частина класу entity (чи дочірнього) радше як класу мапи;
  • подібна проблема з пам'яттю: в цій реалізації мапа зберігає інформацію про плитки, які потрапляли в зорове поле лише головного персонажа, але вона не підходить ані щоби симулювати пам'ять на предмети (вони можуть рухатися чи зникати після того, як персонаж їх побачив), ані запрограмувати пам'ять npc (скажімо, щоби монстри шукали персонажа там, де востаннє його бачили).

найкраще пам'ять і поле видимості реалізовані в cataclysm: dark days ahead, це взірець. для промальовки невідомих (ніколи не були видимі) — окрема константа типу numpy.dtype:

SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype = graphic_dt)

гілку part4/fov варто було назвати інакше. поки не заштовхнув на codeberg, можна безпечно перейменувати:

.../yart> git branch -m part4/light

4.2 поле зору (fov)

нова гілка:

.../yart> git switch -c part4/fov

а ось чому знадобилися окремі повномірні масиви для visible та explored — автор покладається на numpy.select() для наповнення буфера console.rgb[] згідно якихось умов, який виводиться на екран:

console.rgb[0:self.width, 0:self.height] = np.select(
 condlist = [self.visible, self.explored],
 choicelist = [self.tiles["light"], self.tiles["dark"]],
 default = tiles.SHROUD
 )

обрахування видимості плиток автор робить за допомогою фукнції compute_fov() з модуля tcod.map; функція бере двовимірний масив з інформацією (true/false) про прозорість (game_map.tiles["transparent"]), і повертає масив такого ж розміру з інформацією про видимість з даної точки — в цім випадку положення персонажа (player.x, player.y); лише тепер прояснюються деякі вибори автора щодо архітектури (робота з повновимірними масивами):

self.game_map.visible[:] = compute_fov(
 self.game_map.tiles["transparent"],
 (self.player.x, self.player.y),
 radius = 8)

за ось такі рядки я, мабуть, найбільше закоханий в програмування! ну хіба ж оце |= не прекрасне?!

self.game_map.explored |= self.game_map.visible

код працює, четверту частину зроблено.

.../yart> git push --set-upstream origin part4/fov
.../yart> git switch master && git merge part4/fov
.../yart> git push

частина 5 - вороги й копняки

url: https://rogueliketutorials.com/tutorials/tcod/v2/part-5/

5.1 прибирання в коді

розділ починається з невеликого прибирання в коді й підготовки до додавання ворожнечі; виділю це в окрему гілку:

.../yart> git switch -c part5/cleaning

з цікавого: маю перечитати вступ до перевірки типів у python.

from typing import Iterable, TYPE_CHECKING
...
if TYPE_CHECKING:
 from components.entity import Entity

також переглянути підказку до множин, щоби зрозуміти, навіщо (окрім як щоби забезпечити унікальність елементів?) авторові знадовся set() ось тут:

self.entities = set(entities)

пересування коду, відповідального за промальовування персонажів та npc до модуля game_map виглядає нелогічним: для цього простого проєкту, можливо, згодиться, але в загальному випадку це має бути в engine, нмсд.

.../yart> git push --set-upstream origin part5/cleaning
.../yart> git switch master && git merge part5/cleaning

5.2 додавання ворогів

я забагато гілок генерую; з наступного розідлу буде одна на розділ:

.../yart> git switch -c part5/monsters

з цікавого — використання пітонічного any() в place_entities() в модулі procgen.py; невже це справді легше читати, аніж старий добрий цикл for?

if not any(entity.x == x and entity.y == y for entity in dungeon.entities):

в секції 3.0 про корисне читання я радив realpython.com; мені подобається якість їхніх матеріялів, але щойно прибрав — через paywall, замаскований під regwall. надалі рекомендуватиму лише відкриті ресурси, як от freecodecamp.org.

орків і тролів задекларовано статично в entity_factories.py, — ані згадочки про те, що в сучасному проєкті описи та харакреристики npc завантажувало би з окремого файлу (json):

orc = Entity(
 char = "o",
 color = (63, 127, 63),
 name = "Ork",
 blocks_movement = True
 )

мені категорично не подобається, що main.py тепер потребує import copy задля deepcopy() в одному рядку — це мало би бути абстраговано, навіть у такій «несправжній» задачі, як підказка:

player = copy.deepcopy(entity_factories.player)

з цим рядком пов'язано ще один нюанс: оскільки на відміну від автора підказки, я вперто складаю ввесь код python до теки components, я маю охайнішу (і логічнішу, нмсд) структуру проєкту:

.../yart> tree --gitignore
.
├── components
│  ├── actions.py
│  ├── engine.py
│  ├── entity_factories.py
│  ├── entity.py
│  ├── game_map.py
│  ├── __init__.py
│  ├── input_handlers.py
│  ├── procgen.py
│  └── tiles.py
├── main.py
├── readme.md
├── requirements.txt
├── resources
│  ├── buddy-graphical.png
│  └── dejavu10x10_gs_tc.png
└── screenshots
 ├── Screenshot_20240121_090900.png
 ├── Screenshot_20240130_183848.png
 ├── Screenshot_20240204_095835.png
 └── Screenshot_20240210_175818.png

але через це мені доводиться щоразу уважно переписувати import — я вважаю, це невелика ціна за кращу організацію:

# в оригінальній підказці:
import entity_factories
# в мене:
import components.entity_factories as entity_factories

ще одна незрозуміла цікавинка в entity.py, потребу в якій автор ніяк не пояснює:

T = TypeVar("T", bound="Entity")
...
 def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
 clone = copy.deepcopy(self)
 ...
 return clone

якщо я правильно розумію, T = TypeVar(...) декларує базовий тип T для того, щоби spawn(self: T, ...) отримував свіжостворений екземпляр класу Entity. але що буде, якщо спробувати spawn(self: Entity, ...) -> Entity? спробував — код працює так само... поки що не розумію, навіщо автор це робить; це треба вивчити, але я ще раз повертаюся до питання про якість підказки: якщо автор до пуття не пояснює ані архітектурних рішень, ані технічних нюансів — в чім сенс підказки?

тим часом: монстрів розставлено по кімнатах і їм можна роздавати копняки, але вони наразі нічого не роблять.

зорове поле, орки й тролі

час закрити гілку і все, що накомічено в п'ятому розділі:

.../yart> git push --set-upstream origin part5/monsters
.../yart> git switch master && git merge part5/monsters
.../yart> git push --all

частина 6 - шкода

url: https://rogueliketutorials.com/tutorials/tcod/v2/part-6/

6.1 рефакторинг

гілка:

.../yart> git switch -c part6/refactoring

можу помилятися, але нмсд рушій (engine) мав би бути «верхівкою» структури проєкту, тим рівнем абстракції, з якою взаємодіють «зовнішні» компоненти (ресурси); він мав би викликати решту «внутрішніх» компонентів (класи, що відповідають за вивід, обробку подій тощо); рішення передавати посилання на екземпляр рушія до обробника подій лише для того, щоби прив'язати хід ворогів і обрахунок зорового поля персонажа видяється дивним — але можливо, що я поки що не до кінця розумію логіку:

class EventHandler(tcod.event.EventDispatch[Action]):
 # ...
 def handle_events(self) -> None:
 for event in tcod.event.wait():
 action = self.dispatch(event)
 if action is None:
 continue
 action.perform()
 self.engine.handle_enemy_turns()
 self.engine.update_fov()

мені треба почитати детальніше про пакування й розпакування списків (func(*a)) і словників (func(**a)) як агрументів функції:

@property
def blocking_entity(self) -> Optional[Entity]:
 return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)

десь тут я зробив дурницю:

  1. закомітив зміну entities.py до гілки part6/refactoring і заштовхнув гілку на codeberg;
  2. тоді зробив ще одну невелику зміну до того ж файлу, і перестворив коміт: git commit --amend -m "..."

забув, що цього не можна робити, якщо коміт вже пішов до віддаленого репозиторію. тепер при спробі заштовхнути новий коміт:

.../yart> git status
На гілці part6/refactoring
Ваша гілка і гілка "origin/part6/refactoring" розійшлися,
і мають 2 і 1 різних коміти відповідно.
 (скористайтесь "git pull", якщо ви хочете інтегрувати віддалену гілку зі своєю)
...
.../yart> git pull
підказка: У вас є розбіжні гілки, і вам потрібно вказати, як їх узгодити.
підказка: Ви можете зробити це, виконавши одну з наступних команд
підказка: до вашого наступного затягування:
підказка:
підказка: git config pull.rebase false # merge
підказка: git config pull.rebase true # rebase
підказка: git config pull.ff only # fast-forward only
підказка:
підказка: Ви можете замінити "git config" на "git config --global", щоб встановити налаштування за замовчуванням
підказка: для всіх сховищ. Ви також можете передати --rebase, --no-rebase,
підказка: або --ff-only у командному рядку, щоб перевизначити налаштування за замовчуванням для кожного
підказка: виклику.

історія комітів локально й на сервері відрізняються. на щастя, ніхто крім мене з віддаленим репозиторієм не працює, тому можна безпечно «примусити» codeberg переписати історію комітів (--force, але безпечніше --force-with-lease):

.../yart> git --force-with-lease origin part6/refactoring
...
.../yart> git status
На гілці part6/refactoring
Ваша гілка не відрізняється від "origin/part6/refactoring".

(далі буде...)