Рисуване в конзолата
Накратко: Рисуването в конзолата става в буфер от клетки (знак и цвят), който се пълни с текст, рамки и ленти и се показва наведнъж от терминал: обикновен, с ANSI цветове или само с променените клетки.
Проблем
Инвентарът излезе под картата
Първата версия на всеки конзолен екран е печат, докато играта върви: картата, после лентата с живота, после съобщенията. Играчът натиска i и инвентарът трябва да излезе върху картата, като прозорец върху прозорец:
for row in MAP: print(row)print("Ж 24/30 М 5/12")# Играчът натиска i: инвентарът трябва да е върху картата.print("+- Инвентар ---+")print("| Отвара x2 |")print("| Брадва |")print("+--------------+")###############################......#..........~~~........##......+..........~~~...>....##......#..........~~~........#####.#####...................##............................###############################Ж 24/30 М 5/12+- Инвентар ---+| Отвара x2 || Брадва |+--------------+Излезе отдолу. Редовете на картата вече са в терминала, а печатът върви само напред. Това е първият проблем. Има още три:
- Кадърът се къса. Боят печата „Вълкът те ухапа“ в момента, в който се случи, а това може да е по средата на картата.
- Цветовете са в логиката. Кодът на боя знае кой код прави текста червен:
# В кода на боя:if plain: print("Вълкът те ухапа: -3")else: print("\x1b[31mВълкът те ухапа: -3\x1b[0m")- Два изхода, двоен код. Проверката на играта иска обикновен текст, за да го сравни ред по ред, а играчът иска цветове. С такава проверка във всеки ред, който печата, двата изхода рано или късно се разминават.
Четирите имат един корен: играта решава какво да покаже и как да го покаже на едно и също място, в един и същ момент.
Идеята
Буфер от клетки
Разделяме двете. Играта рисува в буфер: таблица с по една клетка за всяко място на екрана. Клетката пази знак и цвят. Рисуването само пише в таблицата и нищо не излиза на екрана. Когато кадърът е готов, буферът се показва целият наведнъж.
Така редът на рисуване решава кое е отгоре: инвентарът, нарисуван след картата, просто презаписва клетките ѝ. А понеже нищо не се показва, преди кадърът да е готов, нищо не се и къса.
Буферът е два двумерни масива, за знаците и за цветовете. Първият индекс е редът (y), вторият е колоната (x), защото редовете се печатат един след друг:
class CellBuffer: def __init__(self, width, height): self.width = width self.height = height self.chars = [[" "] * width for _ in range(height)] self.colors = [["default"] * width for _ in range(height)]
def put(self, x, y, ch, color): if x < 0 or y < 0 or x >= self.width or y >= self.height: return self.chars[y][x] = ch self.colors[y][x] = color
def text(self, x, y, text, color): for i, ch in enumerate(text): self.put(x + i, y, ch, color)
def row_text(self, y): return "".join(self.chars[y]).rstrip(" ")putпише една клетка. Ако мястото е извън буфера, не прави нищо.textпише низ знак по знак, всеки сput.row_textвръща един ред като низ, без интервалите в края: те не се виждат, а правят изхода по-дълъг.
Цветът е само име: "red", "gray". Какво значи то, решава терминалът по-късно.
Защо put мълчи, вместо да спре с грешка? Името на героя е дълго, рамката стои до ръба, съобщението е по-широко от екрана. Отрязан текст е по-добре от игра, която пада заради едно име:
buffer = CellBuffer(12, 2)buffer.text(0, 0, "Ж 24/30", "red")buffer.text(8, 1, "Пазачът", "red")for y in range(buffer.height): print(buffer.row_text(y))Ж 24/30 ПазаПроверката е x >= width, не x > width. Индексите са от 0 до width - 1, затова клетка width вече е извън реда.
По-точно, за любопитните
Python и JavaScript нямат отделен тип за знак: клетката пази низ с дължина 1. Кирилицата, латиницата и знаците #, +, | са по един знак в низа, затова един знак е една клетка. Кога това спира да е вярно, е в „По-точно“ най-долу.
Може и масив от обекти „клетка“ със знак и цвят. Два отделни масива са по-прости за копиране и сравняване, а това ще ни трябва в края на урока.
Идеята
Рамки и ленти
Над put и text идват нещата, които играта рисува постоянно: празен правоъгълник, рамка със заглавие и лента за живот:
def fill_rect(self, x, y, width, height): for row in range(y, y + height): for col in range(x, x + width): self.put(col, row, " ", "default")
def box(self, x, y, width, height, title): self.fill_rect(x, y, width, height) for col in range(x, x + width): self.put(col, y, "-", "gray") self.put(col, y + height - 1, "-", "gray") for row in range(y, y + height): self.put(x, row, "|", "gray") self.put(x + width - 1, row, "|", "gray") self.put(x, y, "+", "gray") self.put(x + width - 1, y, "+", "gray") self.put(x, y + height - 1, "+", "gray") self.put(x + width - 1, y + height - 1, "+", "gray") if title: self.text(x + 2, y, " " + title + " ", "yellow")
def bar(self, x, y, width, value, maximum, color): filled = value * width // maximum if maximum > 0 else 0 if value > 0 and filled == 0: filled = 1 if filled > width: filled = width self.put(x, y, "[", "gray") for i in range(width): if i < filled: self.put(x + 1 + i, y, "#", color) else: self.put(x + 1 + i, y, "-", "gray") self.put(x + width + 1, y, "]", "gray")box първо изчиства мястото си с fill_rect: иначе картата отдолу щеше да се вижда между буквите. После рисува ръба: - горе и долу, | отстрани, + в ъглите. Заглавието е върху горния ръб, с по един интервал от двете страни.
bar смята колко места да запълни: стойност × ширина / максимум, с цяло деление. Делението закръгля надолу, затова лентата е пълна само при пълен живот: 29 от 30 не изглежда като 30 от 30. Отгоре има едно правило: жив герой никога не вижда празна лента. При 1 от 30 делението дава 0, а лентата показва едно място.
buffer = CellBuffer(20, 4)values = [30, 15, 1, 0]for i, value in enumerate(values): buffer.bar(0, i, 10, value, 30, "red") buffer.text(13, i, f"{value}/30", "default")for y in range(buffer.height): print(buffer.row_text(y))[##########] 30/30[#####-----] 15/30[#---------] 1/30[----------] 0/30По-точно, за любопитните
Защо не закръгляне към най-близкото? Тогава 29 от 30 дава 10 места (пълна лента, а героят е ранен), а 1 от 30 дава 0 (празна лента, а героят е жив). Двете крайности са точно мястото, където играчът гледа лентата. С цяло деление и правилото „поне едно място“ пълната лента значи пълен живот, а празната значи смърт.
Ширината в bar е само на запълнимата част: лентата заема ширина + 2 клетки заради [ и ].
Пробвай
Предвиди лентите
Героят има 29 живота от 30, а слугата до него 1 от 30. Какво печата програмата?
buffer = CellBuffer(12, 2)buffer.bar(0, 0, 10, 29, 30, "red")buffer.bar(0, 1, 10, 1, 30, "red")for y in range(buffer.height): print(buffer.row_text(y))29 × 10 / 30 е 9,67, а цялото деление дава 9: лентата не е пълна, защото и героят не е. 1 × 10 / 30 дава 0, но слугата е жив, затова лентата показва едно място.
Пробвай
Десният ръб
Това е цикълът в box, който рисува страничните ръбове. Левият е в колона x. В коя колона е десният?
for row in range(y, y + height):
self.put(x, row, "|", "gray")
self.put(, row, "|", "gray")Рамка с ширина width заема колоните от x до x + width - 1. Колона x + width е първата след рамката: с нея рамката става с една колона по-широка, а десните ъгли не са на ръба.
Виж го
Картата, инвентарът и лентата
Ето целият кадър от началото: картата, героят, вълкът, инвентарът върху картата и долната лента. Буферът се печата чак накрая, ред по ред:
buffer = CellBuffer(30, 9)for y, row in enumerate(MAP): buffer.text(0, y, row, "gray")buffer.put(3, 2, "@", "white")buffer.put(25, 4, "w", "red")buffer.box(7, 1, 15, 5, "Инвентар")buffer.text(9, 2, "Отвара x2", "red")buffer.text(9, 3, "Брадва", "default")buffer.text(9, 4, "Злато 35", "yellow")buffer.text(0, 7, "Ива, вещица ур.2", "white")buffer.text(0, 8, "Ж", "red")buffer.bar(2, 8, 10, 24, 30, "red")buffer.text(15, 8, "24/30", "default")buffer.text(21, 8, "М", "blue")buffer.bar(23, 8, 5, 5, 12, "blue")for y in range(buffer.height): print(buffer.row_text(y))###############################......+- Инвентар --+.......##..@...| Отвара x2 |..>....##......| Брадва |.......#####.##| Злато 35 |...w...##......+-------------+.......###############################Ива, вещица ур.2Ж [########--] 24/30 М [##---]Инвентарът е върху картата, а вълкът се вижда вдясно от него, защото рамката покрива само своите клетки. Редовете в кода не са подредени като редовете на екрана: долната лента можеше да се нарисува първа и кадърът щеше да е същият.
Идеята
Терминалът решава как
Остава въпросът как буферът стига до екрана. Отговорът зависи от това кой гледа: проверката иска обикновен текст, играчът иска цветове, браузърът иска HTML. Затова буферът не печата сам. Печата терминал, а всички терминали имат един метод: present(buffer), „покажи този кадър“.
class PlainTerminal: def __init__(self): self.last_frame = ""
def present(self, buffer): lines = [buffer.row_text(y) for y in range(buffer.height)] while lines and lines[-1] == "": lines.pop() frame = "\n".join(lines) + "\n----\n" if frame == self.last_frame: return write(frame) self.last_frame = frame
class AnsiTerminal: def present(self, buffer): frame = ESC + "[2J" + ESC + "[H" for y in range(buffer.height): row = buffer.row_text(y) color = "default" for x, ch in enumerate(row): if buffer.colors[y][x] != color: color = buffer.colors[y][x] frame += CODES[color] frame += ch frame += ESC + "[0m\n" write(frame)PlainTerminal печата редовете с row_text, маха празните редове в края и слага ---- за край на кадъра. Ако кадърът е същият като предишния, не печата нищо: команда, която не сменя нищо (грешна команда, поглед към вече видяното), не трябва да пълни изхода с еднакви кадри.
AnsiTerminal праща кодове, които терминалът изпълнява, вместо да ги покаже. Всеки започва със знака ESC (код 27):
ESC[2Jчисти екрана,ESC[Hслага курсора горе вляво;ESC[31mзначи „оттук нататък червено“,ESC[90mсиво,ESC[33mжълто,ESC[0m„върни обикновения цвят“.
Цветът в терминала е състояние: стои, докато не го смениш. Затова AnsiTerminal пише код само там, където цветът на клетката е друг от предишния, и връща обикновения цвят в края на всеки ред.
Python няма интерфейси. Двата класа имат метод present с еднакви параметри и това стига: играта вика present, без да пита какъв е терминалът (duck typing). Ако искаш договорът да е записан, typing.Protocol с метод present го прави, без класовете да наследяват нещо.
В програмите на урока ESC, кодовете и изходът към терминала са тук:
ESC = "\x1b"CODES = { "default": ESC + "[0m", "red": ESC + "[31m", "yellow": ESC + "[33m", "blue": ESC + "[34m", "gray": ESC + "[90m", "white": ESC + "[97m",}
# Към терминала. ESC се печата като \e, за да се вижда в изхода.def write(text): print(text.replace(ESC, "\\e"), end="")write печата ESC като текста \e, за да се вижда в изхода на страницата. В истински терминал на негово място е самият знак, и вместо \e[31m виждаш червено.
Виж го
Един буфер, два терминала
buffer = CellBuffer(30, 9)for y, row in enumerate(MAP): buffer.text(0, y, row, "gray")buffer.put(3, 2, "@", "white")buffer.put(25, 4, "w", "red")buffer.box(7, 1, 15, 5, "Инвентар")buffer.text(9, 2, "Отвара x2", "red")buffer.text(9, 3, "Брадва", "default")buffer.text(9, 4, "Злато 35", "yellow")buffer.text(0, 7, "Ива, вещица ур.2", "white")buffer.text(0, 8, "Ж", "red")buffer.bar(2, 8, 10, 24, 30, "red")buffer.text(15, 8, "24/30", "default")buffer.text(21, 8, "М", "blue")buffer.bar(23, 8, 5, 5, 12, "blue")
plain = PlainTerminal()ansi = AnsiTerminal()plain.present(buffer)plain.present(buffer) # същият кадър: не се печата пакansi.present(buffer)Стъпка по стъпка, с думи
- Празен буфер: 30 × 9 = 270 клетки, всяка е интервал с цвят default. Нищо не е отпечатано.
- Картата: text пише всеки ред знак по знак, а всеки знак е един put в една клетка. 7 реда по 30, сиви.
- put пише един знак с цвят: героят @ в (3, 2), бял, и вълкът w в (25, 4), червен. Клетката помни и двете.
- box първо изчиства своите 15 × 5 клетки (fill_rect), после рисува ръба и заглавието. Картата под рамката е изтрита в буфера, а не на екрана: екранът още е празен.
- Предметите влизат в рамката: Отвара x2, Брадва, Злато 35. Редът на рисуване решава кое е отгоре: инвентарът е след картата.
- Долната лента: 24 от 30 в 10 места са 8 #, а 5 от 12 в 5 места са 2. Цялата картина е готова, а терминалът още не е видял нищо.
- PlainTerminal: само знаците, ред по ред, без интервалите в края (ред y = 7 е 16 знака, не 30), и ---- за край на кадъра. Цветовете се губят.
- Същият буфер пак: PlainTerminal сравнява кадъра с последния отпечатан и не печата нищо.
- AnsiTerminal: същият буфер, с цветове. Код за цвят се пише само там, където цветът се сменя: 35 кода за 9 реда, не по един на клетка.
- Играта рисува веднъж. Кой терминал ще го покаже, решава един ред при старта, а кодът, който рисува, не знае.
Програмата рисува кадъра от по-горе и го дава на двата терминала. PlainTerminal го получава два пъти, но го печата веднъж:
plain = PlainTerminal()ansi = AnsiTerminal()plain.present(buffer)plain.present(buffer) # същият кадър: не се печата пакansi.present(buffer)###############################......+- Инвентар --+.......##..@...| Отвара x2 |..>....##......| Брадва |.......#####.##| Злато 35 |...w...##......+-------------+.......###############################Ива, вещица ур.2Ж [########--] 24/30 М [##---]----\e[2J\e[H\e[90m##############################\e[0m\e[90m#......+-\e[33m Инвентар \e[90m--+.......#\e[0m\e[90m#..\e[97m@\e[90m...|\e[0m \e[31mОтвара x2\e[0m \e[90m|..>....#\e[0m\e[90m#......|\e[0m Брадва \e[90m|.......#\e[0m\e[90m####.##|\e[0m \e[33mЗлато 35\e[0m \e[90m|...\e[31mw\e[90m...#\e[0m\e[90m#......+-------------+.......#\e[0m\e[90m##############################\e[0m\e[97mИва, вещица ур.2\e[0m\e[31mЖ\e[0m \e[90m[\e[31m########\e[90m--]\e[0m 24/30 \e[34mМ\e[0m \e[90m[\e[34m##\e[90m---]\e[0mВиж колко рядко има код в изхода на AnsiTerminal: 35 кода за 270 клетки. Ред от сивата карта е един код, а \e[0m вътре в рамката е там, където изчистените клетки са с обикновен цвят.
Пробвай
Намери грешката
Колега пипна AnsiTerminal и съобщението под лентата за живот излезе сиво, а трябва да е с обикновен цвят. Ето какво праща програмата (\e е ESC):
buffer = CellBuffer(30, 2)buffer.text(0, 0, "Ж", "red")buffer.bar(2, 0, 10, 24, 30, "red")buffer.text(0, 1, "Вълкът те ухапа.", "default")AnsiTerminal().present(buffer)\e[2J\e[H\e[31mЖ\e[0m \e[90m[\e[31m########\e[90m--]Вълкът те ухапа.На кой ред е грешката?
Избери реда с грешката.
Редът свършва, без да върне цвета. Последната клетка на лентата е ], сива, и терминалът остава сив и на следващия ред. Програмата пък започва новия ред с color = "default", мисли, че цветът вече е обикновен, и не праща код за първата буква. Верният край на реда е ESC[0m и после новият ред: тогава това, което програмата мисли, и това, което е в терминала, пак са едно и също.
Идеята
Само промененото
PlainTerminal вече пропуска кадър, еднакъв с предишния. Това е спестяване на ниво кадър: всичко или нищо. Но в походова игра повечето ходове сменят две-три клетки: героят прави крачка, а останалите 250 са същите. AnsiTerminal въпреки това чисти екрана и праща всичко наново.
По-добрият терминал помни какво е показал (втори буфер, last) и сравнява клетка по клетка. За всяка различна клетка мести курсора с ESC[ред;колонаH и пише само нея:
class DiffTerminal: def __init__(self): self.last = None self.cells = 0
def present(self, buffer): write(self.changes(buffer))
def changes(self, buffer): frame = "" if self.last is None: self.last = CellBuffer(buffer.width, buffer.height) frame = ESC + "[2J" self.cells = 0 for y in range(buffer.height): for x in range(buffer.width): ch = buffer.chars[y][x] color = buffer.colors[y][x] if ch == self.last.chars[y][x] and color == self.last.colors[y][x]: continue frame += ESC + f"[{y + 1};{x + 1}H" frame += CODES[color] + ch self.last.put(x, y, ch, color) self.cells += 1 if self.cells > 0: frame += CODES["default"] return frameДва детайла хващат всеки, който го пише за първи път. Редът и колоната в ESC[ред;колонаH са от 1, а не от 0, и редът е пръв. А last се обновява веднага с изпратената клетка: иначе следващият кадър праща същите клетки пак.
Програмата рисува света без инвентара. После героят прави крачка, а вълкът хапе. show печата колко клетки и колко знака праща всеки кадър, а късите обновявания и целите:
buffer = CellBuffer(30, 9)screen = DiffTerminal()draw_world(buffer)show(1, screen, buffer)buffer.put(3, 2, ".", "gray") # героят прави крачка надясноbuffer.put(4, 2, "@", "white")show(2, screen, buffer)buffer.bar(2, 8, 10, 21, 30, "red") # вълкът хапе: 24 → 21buffer.text(15, 8, "21/30", "default")show(3, screen, buffer)show(4, screen, buffer) # нищо не се е променилокадър 1: 252 клетки, 3199 знакакадър 2: 2 клетки, 28 знака\e[3;4H\e[90m.\e[3;5H\e[97m@\e[0mкадър 3: 2 клетки, 29 знака\e[9;11H\e[90m-\e[9;17H\e[0m1\e[0mкадър 4: 0 клетки, 0 знака- Кадър 1 праща 252 клетки и 3199 знака: екранът е празен и всичко е ново. Това е повече от целия кадър на AnsiTerminal (420 знака), защото всяка клетка идва със свое преместване и свой цвят.
- Крачката е 2 клетки и 28 знака: старото място става
., новото@. - Ухапването сменя едно
#в лентата и една цифра: от24в21се сменя само4. - Кадър 4 не праща нищо. Пропускането на еднакъв кадър от PlainTerminal е същото, сведено до „0 клетки“.
Защо си струва? Когато терминалът е през мрежа (ssh, сървър, при който играят няколко души), всеки знак минава по мрежата: 28 знака вместо 420, колкото праща AnsiTerminal за целия кадър, е разлика, която се усеща на всеки ход. Бавните терминали рисуват знак по знак, а ESC[2J първо показва празен екран и после го пълни отгоре надолу: екранът мига. С обновяване на място мигане няма.
Кадър 1 показва и обратната страна: когато се сменя почти всичко (нов екран, отваря се инвентар), цял кадър е по-евтин от клетка по клетка. Затова библиотеките за терминал (като curses) при голяма промяна пращат целия екран, а при малка само разликата.
Идеята
В истинската игра
Каноничната игра има същите части. Буферът е 60 × 18 и има още clear(): на всеки ход стекът от екрани рисува всичко отначало в изчистен буфер.
# Играта не печата направо: рисува в буфер от клетки (знак + цвят), а терминалът решава как да го# покаже. Така едно и също рисуване става и обикновен текст, и цветен ANSI екран.SCREEN_WIDTH = 60SCREEN_HEIGHT = 18VIEW_WIDTH = 40VIEW_HEIGHT = 13
class CellBuffer: def __init__(self, width, height): self.width = width self.height = height self.chars = [] self.colors = [] for y in range(height): self.chars.append([' '] * width) self.colors.append(['default'] * width)
def clear(self): for y in range(self.height): for x in range(self.width): self.chars[y][x] = ' ' self.colors[y][x] = 'default'
def put(self, x, y, ch, color): if x < 0 or y < 0 or x >= self.width or y >= self.height: return self.chars[y][x] = ch self.colors[y][x] = color
def text(self, x, y, text, color): for i in range(len(text)): self.put(x + i, y, text[i], color)
def fill_rect(self, x, y, width, height): for row in range(y, y + height): for col in range(x, x + width): self.put(col, row, ' ', 'default')
# Рамка с празна вътрешност: прозрачните екрани (меню, инвентар) се рисуват в рамка върху картата. def box(self, x, y, width, height, title): self.fill_rect(x, y, width, height) for col in range(x, x + width): self.put(col, y, '-', 'gray') self.put(col, y + height - 1, '-', 'gray') for row in range(y, y + height): self.put(x, row, '|', 'gray') self.put(x + width - 1, row, '|', 'gray') self.put(x, y, '+', 'gray') self.put(x + width - 1, y, '+', 'gray') self.put(x, y + height - 1, '+', 'gray') self.put(x + width - 1, y + height - 1, '+', 'gray') if len(title) > 0: self.text(x + 2, y, ' ' + title + ' ', 'yellow')
# Лента [#####-----]: запълнената част е пропорционална на стойността. def bar(self, x, y, width, value, maximum, color): filled = (value * width) // maximum if maximum > 0 else 0 if value > 0 and filled == 0: filled = 1 if filled > width: filled = width self.put(x, y, '[', 'gray') for i in range(width): if i < filled: self.put(x + 1 + i, y, '#', color) else: self.put(x + 1 + i, y, '-', 'gray') self.put(x + width + 1, y, ']', 'gray')
def row_text(self, y): end = self.width while end > 0 and self.chars[y][end - 1] == ' ': end -= 1 line = '' for x in range(end): line += self.chars[y][x] return lineДвата терминала:
# Два терминала с еднакъв интерфейс: present(buffer) показва кадъра, echo(line) показва командата.# PlainTerminal печата текст (за проверката и уроците) и пропуска кадър, еднакъв с предишния.class PlainTerminal: def __init__(self, io): self.io = io self.last_frame = ''
def present(self, buffer): lines = [] for y in range(buffer.height): lines.append(buffer.row_text(y)) while len(lines) > 0 and len(lines[-1]) == 0: lines.pop() frame = '\n'.join(lines) + '\n----\n' if frame != self.last_frame: self.io.write(frame) self.last_frame = frame
def echo(self, line): self.io.write('> ' + line + '\n')
ANSI_CODES = { 'default': '\x1b[0m', 'red': '\x1b[31m', 'green': '\x1b[32m', 'yellow': '\x1b[33m', 'blue': '\x1b[34m', 'magenta': '\x1b[35m', 'cyan': '\x1b[36m', 'white': '\x1b[1;37m', 'gray': '\x1b[90m',}
# AnsiTerminal чисти екрана и оцветява: кръвта е червена, магията синя, отровата зелена.class AnsiTerminal: def __init__(self, io): self.io = io
def present(self, buffer): out = '\x1b[2J\x1b[H' for y in range(buffer.height): end = len(buffer.row_text(y)) color = 'default' for x in range(end): if buffer.colors[y][x] != color: color = buffer.colors[y][x] out += '\x1b[0m' + ANSI_CODES[color] out += buffer.chars[y][x] out += '\x1b[0m\n' self.io.write(out + '> ')
def echo(self, line): passРазликите от урока са три, и всяка има причина:
echo(line). PlainTerminal пише командата (> w) след кадъра: така минаването, с което се проверява играта, е четимо като разговор. AnsiTerminal не я пише, защото играчът вижда какво е написал.ESC[0mпреди всеки цвят. Белият в играта е1;37: удебелен и бял. Без връщане удебеленото остава и на следващия цвят. В урока всички кодове сменят само цвета, затова връщането не трябва.- Няма DiffTerminal. Играта е походова: един кадър на команда, а играчът и без това чака, докато напише следващата. Ако тръгне по мрежа, DiffTerminal е трети клас, а нищо друго не се сменя.
Кой терминал, решава един ред в началото на цикъла на играта: с --plain е PlainTerminal, иначе AnsiTerminal. Долната лента е функция, която рисува в буфера и не знае за терминала:
# Долната лента: име, клас, ниво, живот и мана с ленти, злато, място. Три последни съобщения.def draw_hud(buffer, game, row): hero = game.hero next_xp = xp_for_level(hero.level + 1) buffer.text(0, row, hero.name + ', ' + nm(hero.hero_class()) + ' ' + t('hud.level', {'level': hero.level}) + ' ' + t('hud.xp', {'xp': hero.xp, 'next': next_xp}) + ' ' + t('hud.gold', {'gold': hero.gold}), 'white') buffer.text(0, row + 1, t('hud.hp'), 'red') buffer.bar(2, row + 1, 12, hero.hp, hero.max_hp(), 'red') buffer.text(17, row + 1, str(hero.hp) + '/' + str(hero.max_hp()), 'default') buffer.text(25, row + 1, t('hud.mana'), 'blue') buffer.bar(27, row + 1, 8, hero.mana, hero.max_mana(), 'blue') buffer.text(38, row + 1, str(hero.mana) + '/' + str(hero.max_mana()), 'default') buffer.text(45, row + 1, effect_labels(hero), 'green')
def draw_messages(buffer, game, row, count): first = max(0, len(game.messages) - count) for i in range(first, len(game.messages)): buffer.text(0, row + i - first, game.messages[i], 'default')
def effect_labels(creature): labels = [] for effect in creature.effects: labels.append(effect.label()) return ' '.join(labels)
# Цифрата след команда: „d5“ е пет стъпки надясно. Връща None, ако не е число.def command_number(command, start): if len(command) <= start: return None return parse_int_strict(command[start:])В края на региона има и малка помощна функция за командите (d5 е пет крачки надясно): тя е тук, защото и лентата, и командите са част от картата.
По-точно
Буфери, ширини, цветове
Двоен буфер. last в DiffTerminal е това, което графиката нарича двоен буфер: един буфер е показаният, а в другия се рисува следващият кадър. В игрите с видеокарта двата се разменят наведнъж, за да не се види половин кадър. Тук рисуваме целия кадър, преди да викнем present, затова играчът никога не вижда половин кадър.
Ширината на знаците. В обичайния терминал кирилицата и знаците за рамки (─, │, ┌) заемат по една колона. По стандарта Unicode обаче и двете са с „двусмислена“ ширина: терминал, настроен за китайски или японски, може да ги рисува в две колони. Емоджи и йероглифите са винаги в две. Има и втори проблем: емоджи е извън основната равнина на Unicode, и в Java, C# и JavaScript дължината на низа с едно емоджи е 2 (две половини от UTF-16), а в Python е 1. Буферът, който пише по един знак от низа на клетка, ще го разреже на две. Затова каноничната игра рисува рамките с ASCII: +, -, |, # са една колона навсякъде и един знак във всеки език.
16, 256 и милиони цветове. ESC[31m до ESC[37m и ESC[90m до ESC[97m са 16-те основни цвята. Точният им нюанс решава темата на терминала, затова червеното е четимо и на светъл, и на тъмен фон. Има още ESC[38;5;Nm (палитра от 256) и ESC[38;2;R;G;Bm (точен цвят), но не всеки терминал ги разбира, а точният цвят може да се слее с фона на някого. Играта ползва 16-те.
Windows и пренасочен изход. Windows Terminal разбира ANSI кодовете. Старият конзолен прозорец ги разбира от Windows 10 нататък, но програмата трябва да ги включи (режимът ENABLE_VIRTUAL_TERMINAL_PROCESSING), иначе на екрана излиза ←[31m. Отделно, когато изходът е пренасочен във файл, кодовете са боклук във файла. Програмите проверяват дали пишат в терминал: sys.stdout.isatty(). Играта оставя избора на играча: --plain.
Упражнение
Прозорецът на боя
Реши задачата в редактора си, с CellBuffer от урока.
Прозорецът на боя. Нарисувай в буфер 36 × 5 рамка на целия буфер със заглавие „Бой“. Вътре, по един ред за всеки враг: името от колона 2, лента с ширина 10 от колона 17 и живота като текущ/макс от колона 30. Мъртвият враг (живот 0) е сив и няма лента и живот, само мъртъв от колона 17. Отпечатай буфера ред по ред.
names = ["вълк кръвник", "гигантски плъх", "гоблин шаман"]hp = [20, 1, 0]max_hp = [20, 9, 13]+- Бой ----------------------------+| вълк кръвник [##########] 20/20|| гигантски плъх [#---------] 1/9 || гоблин шаман мъртъв |+----------------------------------+Още задачи
- Дневник в рамка. Функция, която рисува последните N съобщения от боя в рамка и реже дългите по ширината ѝ, с
...накрая. - По-малко премествания. В DiffTerminal не пращай
ESC[ред;колонаH, когато променената клетка е точно след предишната на същия ред: курсорът вече е там. Колко знака спестяваш в първия кадър? - Трети терминал. HtmlTerminal, който връща кадъра като
<pre>с<span style="color: …">при всяка смяна на цвета. Играта не се променя.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Полиморфизъм · Интерфейси като абстрактни класове · Списъци: append, remove и sort · dict и set
- Задачи по темата: Хрониките на Арканум