the-apprentice / oracles /wizard_text.py
AndrewRqy
Initial commit — The Apprentice for Build Small
5afb7b3
Raw
History Blame Contribute Delete
12.5 kB
"""The mentor's voice for each spread of the grimoire.
MENTOR_TEXT[lang_code][step_index] -> markdown-like string.
Spreads 1 and onward may contain ``{hero_name}`` and ``{village_name}``
placeholders that get filled in at render time.
"""
from __future__ import annotations
# Inline markup the renderer supports:
# **bold** (rendered as <strong>)
# *italic* (rendered as <em>)
# blockquote lines starting with ``> ``
# --- on its own line for a thematic break
# blank line = paragraph break
MENTOR_TEXT: dict[str, list[str]] = {
"en": [
# Step 0 — Language
"""The candle is lit. The grimoire is open. The pen is sharpened
to a fine bead of black ink.
But first, before I write a word of any of this — in what tongue
shall I write? My apprentice will need to read every parchment
through tears and through dragon-light. Whatever I choose now, I
will keep to. The mentor who switches languages mid-spell loses
the spell.
*Choose for me. I shall write in —*""",
# Step 1 — Hero name
"""Seven generations the Iron Peak has cast its shadow. Seven
generations of burned villages and silenced kings. I have
watched, I have waited, and tonight I write.
The Pact binds me — no mentor with magic in his blood may strike
at **Vraskar** directly. The dragon's curse can see the spell in
the hand that casts it, and burn it back tenfold. So the spell
must walk in on someone smaller. Someone the curse will not look
for.
He must be young. He must be unknown. He must have a name a
dragon would forget the moment after hearing it.
*I will name him —*""",
# Step 2 — Village name
"""**{hero_name}**, then. Common enough; the dragon will look past
it. I write it twice, as a charm, and the ink dries quickly.
Now — his place of beginning. Somewhere far enough from the Iron
Peak that no smoke has yet darkened his rooftiles. Somewhere with
a well and a bell and a road that does not look like it leads
anywhere. Somewhere a boy can leave without breaking too many
hearts.
*His village will be —*""",
# Step 3 — Oracle I
"""**{hero_name} of {village_name}**. Written, sealed in my
memory. Good.
He kneels by the well at dawn. I am no longer present in his
sight; I am no longer present in any room he has ever entered.
But the parchments will speak to him, and he will understand
them.
Now — the words themselves.
I do not know what {hero_name} will face. Only the shape of the
dangers. A creature with appetite. A barrier of water. A
guardian who asks a question. A force without a face. The dragon
himself.
I cannot guess which trial will draw which parchment. So I write
what I have, and I trust the road to choose.
*The first parchment, then. What word does he most need?*""",
# Step 4 — Oracle II
"""The first is sealed. I press the wax. I do not feel any wiser
for having written it. Perhaps that is the point — perhaps
wisdom dressed as wisdom is no use at all.
The candle bends in a draft I cannot find the source of.
*The second parchment. Something different. Something I would
never think to say twice.*""",
# Step 5 — Oracle III
"""Two parchments sealed. Three remain. The candle is low and the
night is loud beyond the shutters.
The third. **{hero_name}** will read this one only in an hour where
he is no longer the boy I have chosen — only after the road has
asked something of him that I cannot ask for him.
*What shall I leave him for that hour?*""",
# Step 6 — Oracle IV
"""The fourth. Three sealed and two to go. He will not know which
parchment he opens first, nor which he saves for last. He will
only know the order in which his life narrows.
Some of these I write because I know they save him. This one I
write because I do not know what else to give.
*The fourth parchment.*""",
# Step 7 — Oracle V (the dragon)
"""The fifth and last. The ink hesitates.
This one he will open at the threshold of **Vraskar** — I know
this because there will be no others left in his pack.
If anything I write may save him from a dragon, let it be this.
*The fifth and final parchment.*""",
# Step 8 — Closing
"""All five are sealed. The candle is out.
I leave the parchments in his pack as he sleeps. He will not see
my face. He will not know my name. He will know only that they
are there, and that I have whispered, before sleep:
> *"Open one only when your life is in peril. Trust the words I
> have written, however strange they seem."*
I have done what I can. The road takes him from here.""",
],
"zh": [
# Step 0
"""烛火已燃。卷书已开。笔尖磨得极细,墨珠饱满如黑曜石。
但在我落笔之前——我当以何种文字书写?我的使者必须在泪水
中、在龙焰之下,依然能读懂这五张羊皮卷上的每一个字。我此
刻所选的语言,便要从一而终。中途换语的导师,咒语必失。
*请代我抉择。我将以——*""",
# Step 1
"""艾恩峰的阴影已笼罩这片大地七代之久。七代之久的焚毁村
庄,七代之久的沉默王者。我一直凝视,一直等待,而今夜
——我落笔。
古老的契约束缚着我——血脉中怀有法力之人,不可亲手对
**瓦斯卡尔**出手。巨龙的诅咒能看见施法之手,并十倍奉
还。所以法术必须借由更小的人走进去。诅咒所看不见的人。
他必须年轻。他必须无名。他必须有一个名字——一条龙听过
后转瞬便会忘记的名字。
*我将为他起名——*""",
# Step 2
"""**{hero_name}**。这个名字够寻常,龙的目光会越过它。我提笔
重写了一遍,作为护持,墨迹很快便干。
现在——他的起点。一个离艾恩峰足够远的地方,远到他屋顶
上的瓦片还没被烟熏黑。一个有水井、有钟声的小地方。一
个弟子离开时,不至让太多人心碎的地方。
*他的村庄将是——*""",
# Step 3
"""**{village_name}的{hero_name}**。已写,已封于我的记忆中。
好。
他在黎明时分跪于井边。我已不在他的视野中;我已不在他踏
足过的任何房间里。但羊皮卷会向他说话,而他能听懂。
现在——便是文字本身了。
我不知道{hero_name}将面对什么。只知道凶险的形状:一只
怀着饥饿的生灵,一道水的阻隔,一个发问的守门者,一种
没有面容的力量,还有那条龙本身。
我无从猜测哪一重劫难会牵出哪一卷羊皮。所以我写下我所
能写的,余下的,便交给道路来抉择。
*第一卷羊皮,那么。他此刻最需要的词,是什么?*""",
# Step 4
"""第一卷已封缄。我按下蜡印。落笔之后,我并未感到自己变
得更睿智。或许这正是真意——也许装作智慧的智慧,本就
毫无用处。
烛火朝一个我寻不见源头的方向歪去。
*第二卷羊皮。换些别的。换些我永远不会重说一遍的话。*""",
# Step 5
"""两卷已封。三卷在前。烛火将尽,窗外的夜声喧响。
第三卷。**{hero_name}**只会在他不再是我所选的那个弟子
的那一刻才会开启它——只会在道路向他索取我无法替他承
受之事时。
*那一刻,我该留给他什么?*""",
# Step 6
"""第四卷。三卷已封,两卷在前。他不会知道他最先开启的是
哪一卷,也不会知道最后留下的是哪一卷。他只会知道,他
的生命在以何种顺序,渐渐收窄。
有些卷我写下,是因为我知道它们能救他。这一卷,我写
下,只因我不知道还能给他什么。
*第四卷。*""",
# Step 7
"""第五卷,也是最后一卷。笔尖犹疑。
这一卷他将在**瓦斯卡尔**的门槛上开启——我知道,因为
那时他行囊中再无其他可供他选择。
若我所书的任何一字仍可救他于龙焰之下,便让此言成为那
一字。
*第五卷,也是最后一卷。*""",
# Step 8
"""五卷皆已封缄。烛火已灭。
我将羊皮卷放入他熟睡的行囊中。他不会见我的容貌。他不
会知我的名姓。他只会知道:它们在那里,而我,在他入眠
之前,于他梦中低语过:
> *"惟有性命攸关之时方可启封。无论我所书之言多么奇
> 异,亦当信之。"*
我已尽我所能。前路,从此处起,便是他的了。""",
],
}
def text_for(lang_code: str, step: int) -> str:
"""Return the mentor's text for ``(lang_code, step)``.
Falls back to English if ``lang_code`` isn't supported, and clamps
``step`` to the valid range.
"""
block = MENTOR_TEXT.get(lang_code) or MENTOR_TEXT["en"]
if step < 0:
step = 0
if step >= len(block):
step = len(block) - 1
return block[step]
# ---------------------------------------------------------------------------
# Per-spread pixel-art sigils (one per inscribe step).
# Each grid is a list of strings; ``#`` is an amber pixel, anything else is
# transparent. Designed at 8x8 (some at 9x8 for visual balance).
# ---------------------------------------------------------------------------
GRIMOIRE_SIGILS: list[list[str]] = [
# Step 0 — QUILL (the mentor chooses his tongue / picks up the pen)
[
".......#",
"......##",
".....##.",
"....##..",
"...##.#.",
"..###...",
"..####..",
"##......",
],
# Step 1 — STAR (naming the hero — like naming a constellation)
[
"...##...",
"...##...",
"########",
".######.",
".##..##.",
"##....##",
"........",
"........",
],
# Step 2 — HOUSE (the village he leaves behind)
[
"...##...",
"..####..",
".######.",
"########",
".##.#.##",
".##.#.##",
".##.#.##",
"........",
],
# Step 3 — SUN (Oracle I — first light, opening)
[
".#....#.",
"..#..#..",
"...##...",
".######.",
".######.",
"...##...",
"..#..#..",
".#....#.",
],
# Step 4 — MOON (Oracle II — the second mark, balance)
[
"..####..",
".######.",
"###.....",
"###.....",
"###.....",
"###.....",
".######.",
"..####..",
],
# Step 5 — HOURGLASS (Oracle III — time, midpoint)
[
"########",
"########",
".##..##.",
"..####..",
"..####..",
".##..##.",
"########",
"########",
],
# Step 6 — EYE (Oracle IV — vision)
[
".######.",
"########",
"##.##.##",
"##.##.##",
"##.##.##",
"########",
".######.",
"........",
],
# Step 7 — FLAME (Oracle V — the dragon's parchment)
[
"...#....",
"..##....",
".###.#..",
"####.#..",
"#####...",
"######..",
".####...",
"..##....",
],
# Step 8 — KEY (closing — the sealed parchments hand off to the road)
[
".####...",
"######..",
".####...",
"..##....",
"..##....",
"..####..",
"..#..#..",
"..####..",
],
]
def sigil_svg(step: int, color: str = "#f0c060", render_px: int = 36) -> str:
"""Render the pixel-art sigil for ``step`` as inline SVG.
Uses ``shape-rendering="crispEdges"`` so the pixels stay crisp at any
rendered size. Returns the bare ``<svg>`` element (no wrapper)."""
if step < 0:
step = 0
if step >= len(GRIMOIRE_SIGILS):
step = len(GRIMOIRE_SIGILS) - 1
grid = GRIMOIRE_SIGILS[step]
h = len(grid)
w = max(len(row) for row in grid) if grid else 0
rects: list[str] = []
for y, row in enumerate(grid):
for x, c in enumerate(row):
if c == "#":
rects.append(
f'<rect x="{x}" y="{y}" width="1" height="1" fill="{color}"/>'
)
return (
f'<svg viewBox="0 0 {w} {h}" '
f'width="{render_px}" height="{render_px}" '
f'shape-rendering="crispEdges" '
f'style="display:block;image-rendering:pixelated">'
+ "".join(rects)
+ "</svg>"
)