在側邊面板即時列出本機所有 Claude Code session 的狀態

兩個 Claude Code mods(以 function hooks 寫成的 plugin)。
這是個人作品,與 Anthropic 無關。Mods 是 Claude Code 的 early access 功能,需要 Claude Code 2.1.287 以上版本;API 可能隨版本變動。
在 Claude Code 裡輸入:
/plugin marketplace add likephp-github/likephp-mods
/plugin install token-weather@likephp-mods
/plugin install session-radar@likephp-mods
兩個 mod 互相獨立,可以只裝其中一個。
在輸入框上方顯示兩行:
☂ 陣雨 45% 90k/200k ▂▃▄▅ 上一輪 +8.3k
[Opus 5.5] │ my-project (main) │ 5h ██▌░░░░░ 32% (3h 41m後重置) │ 7d 18% │ ⏱ 18m │ $1.23
/token-weather:切換顯示;也可 /token-weather on|off|status側邊面板列出本機所有正在執行的 Claude Code session(讀取 ~/.claude/sessions/*.json,每 3 秒更新):
7 個 session · 1 個工作中 · 藍色為本視窗
● my-project-f8 工作中 3秒
○ 2: api-server-d8 閒置 12分
◆ 1: web-fb shell 2時
跳到其他 session:輸入 /sessions 2 就把面板上編號 2 的 session 所在的終端機分頁帶到最前面;也可以用滑鼠點面板上的 session 名稱(全螢幕版面)。
tmux attach -t <名稱>面板底部有一組像素角色演出本 session 的狀態,角色與背景由主題決定。狀態只有三種,與主題無關:
working:執行中resting:回覆結束後 30 秒內idle:閒置超過 30 秒每個主題在三個狀態各有自己的動作。角色體型隨本 session 的 context 用量變大變小(100% 時為面板容得下最大尺寸的一半,最多 1.5 倍;最小 0.5 倍);面板比角色窄時不畫角色。
/sessions theme 依序切換到下一個主題,/sessions theme tiger 或 /sessions theme puma 直接指定。選擇會寫進 ~/.claude/session-radar.json(例如 { "theme": "puma" },保留檔案裡的其他欄位),之後每個 session 都沿用;沒有這個檔案時用小老虎。
tiger,預設)老虎住在綠色的樹林裡:身後是一排樹,腳下是一行草地。樹固定高度、不隨老虎縮放,可以當比例尺看出老虎變胖了多少。老虎走動時背景以視差方式往反方向捲動(樹走得慢、草跟老虎同速),老虎停下來抓蝴蝶或睡覺時背景也停住。面板太矮放不下樹時只畫草地。
| 狀態 | 動作 |
|---|---|
working<br>執行中 | <br>巡邏:左右來回走 |
resting<br>回覆結束後 30 秒內 | <br>抓蝴蝶:甩尾、壓低身體、跳起來拍掌邊的蝴蝶 |
idle<br>閒置 30 秒後 | <br>睡覺:呼吸起伏、抖尾巴、抖耳朵、打呼 |
puma)綠雲帶著愛心住在閃爍的星空裡:沒有地面,角色腳底貼著面板底部;星星(·、✦、✧)位置固定,約一半會閃爍,不會蓋在角色或對話框上。最小倍數同樣從 0.5 倍起算,縮得太小看不到綠雲的黑眼珠時會自動稍微放大。
| 狀態 | 動作 |
|---|---|
working<br>執行中 | <br>走路:12 格走路圖循環,左右來回走,愛心跟在後面 |
resting<br>回覆結束後 30 秒內 | <br>♥+名言:停在原地,頭頂冒出 ♥,上面隨機出現一句名言 |
idle<br>閒置 30 秒後 | <br>吃丸子:一顆顆吃掉手上的三色丸子(吃到剩 2 顆時頭頂冒出 nom),吃完換新的一串 |
resting 的名言每進入一次 resting 隨機挑一句,這段期間不換;前後加「」,對話框自動換行,最多 3 行,面板太矮時縮成 1 行或只留 ♥:
動作圖由
scripts/gen-gifs.ts以 mod 內實際的像素圖與動畫邏輯產生;終端機裡以半格字元繪製,蝴蝶、打呼、♥、星星與nom為文字符號。小老虎的圖是 40 欄寬、1 倍;綠雲與愛心的圖用原圖大小(1 倍)以看清楚臉,面板放寬到 64 欄。GIF 不畫中文,名言的位置以…代替。
/sessions:開關面板(不會自動開啟);/sessions 1~/sessions 9:跳到該編號的 session;/sessions theme [tiger|puma]:切換主題。面板在全螢幕版面下會停靠在右側。
Mods 不在沙盒裡執行,安裝前請先閱讀原始碼。這兩個 mod 只讀取:
.git/HEAD~/.claude/sessions/*.json(不讀取同目錄的 .key 檔)、~/.claude/session-radar.json(選的主題)都不會連網。唯一會寫入的檔案是 session-radar 在你執行 /sessions theme 時寫的 ~/.claude/session-radar.json。
session-radar 只有在你按下某個 session 時,才會執行 ps(查它的 tty)、tmux(找 pane、切換)與 osascript(選取 iTerm2/Terminal.app 的分頁);不會啟動沒在執行的終端機 app。
claude plugin validate ./token-weather
claude plugin test ./token-weather
session-radar 的動作圖(docs/images/<主題>-<狀態>.gif)由腳本產生,每一格都用面板同一套動畫邏輯算出;改了主題的角色、背景或動作後重新產生:
node scripts/gen-gifs.ts # 重新產生每個主題 × 每個狀態的 GIF(需要 Node 24,不需安裝套件)
node scripts/gen-puma.ts # 從原圖重新產生綠雲與愛心的走路圖與預覽圖(docs/images/puma-preview.png)
node --test 'scripts/*.test.ts' # 腳本的測試
MIT
hooks/register.tsx 257 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Peer, Tiger } from '../types'
5import { jumpTo, messageFor } from './jump'
6import type { Run } from './jump'
7import { ago, hotkeys, look, parsePeer, parseSessionsArgs, peerForKey, sortPeers } from './sessions'
8import { paneFrame } from './frame'
9import { factorFor, joinCells, stamp, step, toCells } from './sprite'
10import { DEFAULT_THEME, stateFor, themeFor } from './theme'
11import { chooseTheme, parseThemeSetting, SETTINGS_PATH, withThemeSetting } from './choice'
12
13const PANE = 'session-radar'
14const TITLE = 'Claude Sessions'
15const POLL_MS = 3000
16const FRAME_MS = 400
17
18const peers = atom({ plugin: 'session-radar', key: 'peers' } as const, [])
19const selfId = atom({ plugin: 'session-radar', key: 'selfId' } as const, '')
20const checkedAt = atom({ plugin: 'session-radar', key: 'checkedAt' } as const, 0)
21const tiger = atom({ plugin: 'session-radar', key: 'tiger' } as const, {
22 isWorking: false,
23 idleSince: 0,
24 frame: 0,
25 x: 0,
26 facing: 1,
27} as Tiger)
28const themeName = atom({ plugin: 'session-radar', key: 'theme' } as const, DEFAULT_THEME as string)
29
30/** 跳到 peer 所在的終端機;失敗時用 toast 告訴使用者下一步,細節寫到 debug log。 */
31/** 跳到 peer 所在的終端機;回傳給使用者看的提示(成功時 undefined),錯誤細節寫到 debug log。 */
32async function jump($: EngineInterface, peer: Peer, inTmux: boolean): Promise<string | undefined> {
33 const run: Run = argv => $.process.run(argv)
34 const outcome = await jumpTo(peer.pid, run, { inTmux })
35 if (outcome.kind === 'error') $.ui.log(`session-radar 切換到 ${peer.name} 失敗:${outcome.detail}`, { to: 'debug' })
36 return messageFor(outcome)
37}
38
39/** 按下面板上的名稱:失敗時用 toast 提示。 */
40async function jumpFromPane($: EngineInterface, peer: Peer, inTmux: boolean) {
41 const message = await jump($, peer, inTmux)
42 if (message !== undefined) $.ui.toast(message)
43}
44
45/** 設定檔的內容;不存在或讀不到時 undefined。 */
46const readSettings = ($: EngineInterface, home: string) => $.fs.read(`${home}/${SETTINGS_PATH}`).catch(() => undefined)
47
48/** /sessions theme:切換當前面板的主題並寫進設定檔,回傳給使用者看的一行。 */
49async function switchTheme($: EngineInterface, home: string, name: string | undefined): Promise<string> {
50 const choice = chooseTheme(await read($, themeName), name)
51 if (choice.kind === 'unknown') return `沒有「${choice.name}」這個主題;可用的主題:${choice.names.join('、')}。`
52
53 await update($, themeName, () => choice.name)
54 const path = `${home}/${SETTINGS_PATH}`
55 try {
56 await $.fs.write(path, withThemeSetting(await readSettings($, home), choice.name))
57 } catch (err) {
58 $.ui.log(`session-radar 寫入 ${path} 失敗:${String(err)}`, { to: 'debug' })
59 return `主題已切換為 ${choice.name}(設定檔寫入失敗,只在這個 session 有效)。`
60 }
61 return `主題已切換為 ${choice.name}。`
62}
63
64async function scan($: EngineInterface, home: string) {
65 const dir = `${home}/.claude/sessions`
66 const entries = await $.fs.list(dir).catch(() => [])
67 const found: Peer[] = []
68
69 for (const entry of entries) {
70 if (entry.kind !== 'file' || !entry.name.endsWith('.json')) continue
71 const text = await $.fs.read(`${dir}/${entry.name}`).catch(() => '')
72 const peer = parsePeer(text)
73 if (peer !== undefined) found.push(peer)
74 }
75
76 const now = await $.clock.now()
77 await update($, peers, () => sortPeers(found))
78 await update($, checkedAt, () => now)
79}
80
81/** 動畫的一格:工作中往前走一步,其他狀態只推進動作的節奏。面板沒開就不動。 */
82async function tick($: EngineInterface, room: number, stride: number) {
83 const isOpen = (await $.ui.panes()).some(pane => pane.id === PANE)
84 if (!isOpen) return
85
86 await update($, tiger, t => {
87 const walked = t.isWorking ? step({ x: t.x, facing: t.facing }, room, stride) : t
88 return { ...t, x: walked.x, facing: walked.facing, frame: t.frame + 1 }
89 })
90}
91
92export const register: Register = on => {
93 let home = ''
94 let inTmux = false
95 let room = 0
96 let stride = 1
97
98 on('session.start', async ($, e, next) => {
99 home = (await $.env.get('HOME')) ?? ''
100 inTmux = ((await $.env.get('TMUX')) ?? '') !== ''
101 const id = await $.session.id()
102 const saved = parseThemeSetting(await readSettings($, home)) ?? DEFAULT_THEME
103 await update($, themeName, () => saved)
104 await update($, selfId, () => id)
105 await $.command.register({
106 name: 'sessions',
107 description: '開關側邊面板:本機所有 Claude session 的狀態;/sessions 1~9 跳到該編號的 session;/sessions theme [名稱] 切換主題',
108 })
109 await scan($, home)
110 $.clock.every(POLL_MS, () => scan($, home))
111 const { context } = await $.session.usage()
112 await update($, tiger, t => ({ ...t, isWorking: false, percent: context.percent }))
113 $.clock.every(FRAME_MS, () => tick($, room, stride))
114 return next(e)
115 })
116
117 on('prompt.submit', async ($, e, next) => {
118 await update($, tiger, t => ({ ...t, isWorking: true }))
119
120 return next(e)
121 })
122
123 on('turn.complete', async ($, e, next) => {
124 if (e.agentId === undefined) {
125 const t = await $.clock.now()
126 // 每次進入 resting 抽一次亂數,主題用它挑這段期間不變的內容(puma 的名言)
127 await update($, tiger, cat => ({ ...cat, isWorking: false, idleSince: t, seed: Math.random() }))
128 }
129
130 return next(e)
131 })
132
133 on('session.measure', async ($, e, next) => {
134 const { percent } = e.context
135 if (percent !== undefined) {
136 await update($, tiger, t => ({ ...t, percent }))
137 }
138
139 return next(e)
140 })
141
142 on('command.run', { command: 'sessions' }, async ($, e) => {
143 const cmd = parseSessionsArgs(e.args)
144
145 if (cmd.kind === 'usage') {
146 return { text: '用法:/sessions 開關面板;/sessions 1~9 跳到面板上該編號的 session;/sessions theme 輪流切換主題,/sessions theme <名稱> 指定主題。' }
147 }
148
149 if (cmd.kind === 'theme') return { text: await switchTheme($, home, cmd.name) }
150
151 if (cmd.kind === 'jump') {
152 await scan($, home)
153 const peer = peerForKey(await read($, peers), await read($, selfId), cmd.key)
154 if (peer === undefined) return { text: `沒有編號 ${cmd.key} 的 session。` }
155 return { text: (await jump($, peer, inTmux)) ?? `已切換到 ${peer.name}。` }
156 }
157
158 const isOpen = (await $.ui.panes()).some(pane => pane.id === PANE)
159
160 if (isOpen) {
161 await $.ui.close({ id: PANE })
162
163 return { text: 'Claude Sessions 面板已關閉。' }
164 }
165
166 await scan($, home)
167 await $.ui.open({ id: PANE, title: TITLE })
168
169 return { text: 'Claude Sessions 面板已開啟。' }
170 })
171
172 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
173 const { Box, Button, Text } = $.ui.resolve(e)
174 const list = await read($, peers)
175 const me = await read($, selfId)
176 const now = await read($, checkedAt)
177 const busy = list.filter(p => p.status === 'busy').length
178 const keys = hotkeys(list, me)
179 const cat = await read($, tiger)
180 const theme = themeFor(await read($, themeName))
181 const columns = e.props.bodyColumns
182 const factor = factorFor(cat.percent, columns, theme)
183 const state = stateFor(cat.isWorking, cat.idleSince, await $.clock.now())
184 const listRows = 1 + Math.max(1, list.length)
185 const frame = paneFrame({
186 theme,
187 state,
188 x: cat.x,
189 facing: cat.facing,
190 frame: cat.frame,
191 factor,
192 columns,
193 rows: e.props.scroll.bodyRows - listRows,
194 seed: cat.seed,
195 })
196 room = frame.room
197 stride = frame.stride
198 const cells = frame.marks.reduce(
199 (c, m) => stamp(c, frame.sprite, m.line, m.col, m.text, m.color, theme.palette),
200 toCells(frame.sprite, theme.palette),
201 )
202 const lines = joinCells(cells)
203 // 把場景推到面板最底:可見列數扣掉清單與場景佔的列數,剩下的補空行
204 const gap = Math.max(1, e.props.scroll.bodyRows - listRows - lines.length)
205
206 return (
207 <Box flexDirection="column">
208 <Text dimColor>
209 {list.length} 個 session · {busy} 個工作中 · 藍色為本視窗
210 </Text>
211 {list.length === 0 && <Text dimColor>找不到任何 session。</Text>}
212 {list.map(peer => {
213 const l = look(peer.status)
214 const isMe = peer.sessionId === me
215 const key = keys[peer.sessionId]
216 const detail = (
217 <Text dimColor wrap="truncate-end">
218 {' '}
219 {l.label} {ago(peer.since, now)}
220 {peer.kind === 'bg' ? ' 背景' : ''}
221 </Text>
222 )
223
224 // 本視窗那一行只是看的;其他 session 的名稱是按鈕,按了跳過去
225 return isMe ? (
226 <Text key={peer.sessionId} wrap="truncate-end">
227 <Text color={l.color}>{l.icon}</Text>{' '}
228 <Text bold color="blue">
229 {peer.name}
230 </Text>
231 {detail}
232 </Text>
233 ) : (
234 <Box key={peer.sessionId} flexDirection="row">
235 <Text color={l.color}>{l.icon} </Text>
236 <Button key={`jump-${peer.sessionId}`} plain hotkey={key} label={peer.name} onPress={() => jumpFromPane($, peer, inTmux)} />
237 {detail}
238 </Box>
239 )
240 })}
241 {Array.from({ length: gap }, (_, i) => (
242 <Text key={`gap${i}`}> </Text>
243 ))}
244 {lines.map((runs, row) => (
245 <Text key={`t${row}`} wrap="truncate-end">
246 {runs.map((r, i) => (
247 <Text key={`r${row}-${i}`} color={r.fg} backgroundColor={r.bg}>
248 {r.text}
249 </Text>
250 ))}
251 </Text>
252 ))}
253 </Box>
254 )
255 })
256}
257hooks/jump.ts 153 lines1/**
2 * 跳到某個 session 所在的終端機:依它的 tty 依序問 tmux、iTerm2、Terminal.app。
3 * 外部指令一律透過注入的 run 執行(argv,不經 shell),方便用假結果測試。
4 */
5
6export type RunResult = { exitCode: number; stdout: string; stderr: string }
7export type Run = (argv: readonly string[]) => Promise<RunResult>
8
9export type Outcome =
10 | { kind: 'done' }
11 | { kind: 'notFound' }
12 | { kind: 'attach'; session: string }
13 | { kind: 'denied'; app: string }
14 | { kind: 'error'; detail: string }
15
16/** 依序詢問的終端機 app(AppleScript 名稱)。 */
17const APPS = ['iTerm2', 'Terminal'] as const
18type App = (typeof APPS)[number]
19
20/** 在 app 裡找 tty 相符的分頁,選取後帶到最前面;找到回 "found"。tty 由 argv 傳入。 */
21const FOCUS: Record<App, string[]> = {
22 iTerm2: [
23 'on run argv',
24 'tell application "iTerm2"',
25 'repeat with w in windows',
26 'repeat with t in tabs of w',
27 'repeat with s in sessions of t',
28 'if tty of s is (item 1 of argv) then',
29 'select w',
30 'tell t to select',
31 'tell s to select',
32 'activate',
33 'return "found"',
34 'end if',
35 'end repeat',
36 'end repeat',
37 'end repeat',
38 'end tell',
39 'return ""',
40 'end run',
41 ],
42 Terminal: [
43 'on run argv',
44 'tell application "Terminal"',
45 'repeat with w in windows',
46 'repeat with t in tabs of w',
47 'if tty of t is (item 1 of argv) then',
48 'set selected of t to true',
49 'set index of w to 1',
50 'activate',
51 'return "found"',
52 'end if',
53 'end repeat',
54 'end repeat',
55 'end tell',
56 'return ""',
57 'end run',
58 ],
59}
60
61const script = (lines: readonly string[]): string[] => lines.flatMap(line => ['-e', line])
62
63/** macOS 拒絕 Apple events(使用者沒允許自動化)的錯誤碼。 */
64const NOT_AUTHORIZED = '-1743'
65
66/** ps 輸出的 tty(ttys001)轉成 /dev/ttys001;沒有終端機(??)回 undefined。 */
67export const ttyOf = (psOut: string): string | undefined => {
68 const name = psOut.trim()
69 if (name === '' || name.startsWith('?')) return undefined
70 return name.startsWith('/dev/') ? name : `/dev/${name}`
71}
72
73type Pane = { tty: string; session: string; window: string; pane: string }
74
75const PANE_FORMAT = '#{pane_tty} #{session_name} #{window_id} #{pane_id}'
76
77/** 解析 tmux list-panes -a 的輸出(欄位見 PANE_FORMAT)。session 名稱可能含空白。 */
78export const parsePanes = (out: string): Pane[] =>
79 out
80 .split('\n')
81 .map(line => line.trim().split(' '))
82 .filter(parts => parts.length >= 4)
83 .map(parts => ({
84 tty: parts[0] ?? '',
85 session: parts.slice(1, -2).join(' '),
86 window: parts.at(-2) ?? '',
87 pane: parts.at(-1) ?? '',
88 }))
89
90/** 依序問正在執行的終端機 app;沒找到回 undefined。 */
91const focusTty = async (run: Run, tty: string): Promise<Outcome | undefined> => {
92 for (const app of APPS) {
93 const running = await run(['osascript', '-e', `application "${app}" is running`])
94 if (running.stdout.trim() !== 'true') continue
95 const found = await run(['osascript', ...script(FOCUS[app]), tty])
96 if (found.stderr.includes(NOT_AUTHORIZED)) return { kind: 'denied', app }
97 if (found.exitCode !== 0) return { kind: 'error', detail: found.stderr.trim() }
98 if (found.stdout.trim() === 'found') return { kind: 'done' }
99 }
100 return undefined
101}
102
103/** 目標在 tmux 裡:選到它的 pane,再把看得到它的畫面帶到前面。 */
104const jumpInTmux = async (run: Run, target: Pane, inTmux: boolean): Promise<Outcome> => {
105 for (const argv of [
106 ['tmux', 'select-window', '-t', target.window],
107 ['tmux', 'select-pane', '-t', target.pane],
108 ]) {
109 const r = await run(argv)
110 if (r.exitCode !== 0) return { kind: 'error', detail: r.stderr.trim() }
111 }
112 const clients = await run(['tmux', 'list-clients', '-t', target.session, '-F', '#{client_tty}'])
113 const client = clients.exitCode === 0 ? ttyOf(clients.stdout.split('\n')[0] ?? '') : undefined
114 if (client !== undefined) return (await focusTty(run, client)) ?? { kind: 'notFound' }
115 if (!inTmux) return { kind: 'attach', session: target.session }
116 const r = await run(['tmux', 'switch-client', '-t', target.pane])
117 return r.exitCode === 0 ? { kind: 'done' } : { kind: 'error', detail: r.stderr.trim() }
118}
119
120/** 跳到 pid 這個 session 所在的終端機。inTmux:本視窗是否在 tmux 裡。 */
121export const jumpTo = async (pid: number, run: Run, env: { inTmux: boolean }): Promise<Outcome> => {
122 try {
123 const ps = await run(['ps', '-o', 'tty=', '-p', String(pid)])
124 const tty = ps.exitCode === 0 ? ttyOf(ps.stdout) : undefined
125 if (tty === undefined) return { kind: 'notFound' }
126
127 // 沒裝 tmux 時指令根本無法啟動,當成 tmux 裡找不到
128 const panes = await run(['tmux', 'list-panes', '-a', '-F', PANE_FORMAT]).catch(() => ({ exitCode: 1, stdout: '', stderr: '' }))
129 const target = panes.exitCode === 0 ? parsePanes(panes.stdout).find(p => p.tty === tty) : undefined
130 if (target !== undefined) return await jumpInTmux(run, target, env.inTmux)
131
132 return (await focusTty(run, tty)) ?? { kind: 'notFound' }
133 } catch (err) {
134 return { kind: 'error', detail: err instanceof Error ? err.message : String(err) }
135 }
136}
137
138/** 給使用者看的提示;成功時不提示。 */
139export const messageFor = (o: Outcome): string | undefined => {
140 switch (o.kind) {
141 case 'done':
142 return undefined
143 case 'notFound':
144 return '找不到這個 session 所在的終端機(支援 tmux、iTerm2、Terminal.app)'
145 case 'attach':
146 return `這個 session 在未連線的 tmux session「${o.session}」,請執行 tmux attach -t ${o.session}`
147 case 'denied':
148 return `沒有控制 ${o.app} 的權限,請到「系統設定 › 隱私權與安全性 › 自動化」允許`
149 case 'error':
150 return `切換失敗:${o.detail.split('\n')[0] ?? ''}`
151 }
152}
153hooks/sessions.ts 94 lines1import type { Peer } from '../types'
2
3export type Look = { icon: string; label: string; color: string }
4
5const LOOKS: Record<string, Look> = {
6 busy: { icon: '●', label: '工作中', color: 'yellow' },
7 idle: { icon: '○', label: '閒置', color: 'green' },
8 shell: { icon: '◆', label: 'shell', color: 'cyan' },
9 waiting: { icon: '◐', label: '等待回應', color: 'magenta' },
10}
11
12export const look = (status: string): Look =>
13 LOOKS[status] ?? { icon: '·', label: status || '未知', color: 'gray' }
14
15const asString = (v: unknown, fallback: string): string => (typeof v === 'string' ? v : fallback)
16const asNumber = (v: unknown, fallback: number): number => (typeof v === 'number' ? v : fallback)
17
18/** 解析 ~/.claude/sessions/<pid>.json;格式不符(例如寫入到一半)回 undefined。 */
19export const parsePeer = (text: string): Peer | undefined => {
20 let raw: unknown
21 try {
22 raw = JSON.parse(text)
23 } catch {
24 return undefined
25 }
26 if (typeof raw !== 'object' || raw === null) return undefined
27 const d = raw as Record<string, unknown>
28 if (typeof d.pid !== 'number' || typeof d.sessionId !== 'string') return undefined
29
30 return {
31 pid: d.pid,
32 sessionId: d.sessionId,
33 name: asString(d.name, `pid ${d.pid}`),
34 status: asString(d.status, ''),
35 kind: asString(d.kind, ''),
36 cwd: asString(d.cwd, ''),
37 since: asNumber(d.statusUpdatedAt, asNumber(d.updatedAt, asNumber(d.startedAt, 0))),
38 startedAt: asNumber(d.startedAt, 0),
39 }
40}
41
42/** 工作中的排最前面,其次依最近狀態變動時間。 */
43export const sortPeers = (peers: readonly Peer[]): Peer[] =>
44 [...peers].sort((a, b) => {
45 const busy = Number(b.status === 'busy') - Number(a.status === 'busy')
46 return busy !== 0 ? busy : b.since - a.since
47 })
48
49/**
50 * 本視窗以外的 session 依啟動時間由舊到新編號 1~9,當作 hotkey。
51 * 與清單排序無關,session 還在時編號就不變。
52 */
53export const hotkeys = (peers: readonly Peer[], selfId: string): Record<string, string> =>
54 Object.fromEntries(
55 peers
56 .filter(p => p.sessionId !== selfId)
57 .sort((a, b) => a.startedAt - b.startedAt || a.pid - b.pid)
58 .slice(0, 9)
59 .map((p, i) => [p.sessionId, String(i + 1)]),
60 )
61
62/** 依 hotkey 編號找回 session。 */
63export const peerForKey = (peers: readonly Peer[], selfId: string, key: string): Peer | undefined => {
64 const keys = hotkeys(peers, selfId)
65 return peers.find(p => keys[p.sessionId] === key)
66}
67
68export type SessionsCommand =
69 | { kind: 'toggle' }
70 | { kind: 'jump'; key: string }
71 | { kind: 'theme'; name?: string }
72 | { kind: 'usage' }
73
74/** /sessions 的參數:空的開關面板,1~9 跳到該編號的 session,theme [名稱] 切換主題(名稱由呼叫端檢查)。 */
75export const parseSessionsArgs = (args: string): SessionsCommand => {
76 const a = args.trim()
77 if (a === '') return { kind: 'toggle' }
78 if (/^[1-9]$/.test(a)) return { kind: 'jump', key: a }
79 const theme = /^theme(?:\s+(\S+))?$/.exec(a)
80 if (theme === null) return { kind: 'usage' }
81 return theme[1] === undefined ? { kind: 'theme' } : { kind: 'theme', name: theme[1] }
82}
83
84export const ago = (since: number, now: number): string => {
85 const s = Math.max(0, Math.round((now - since) / 1000))
86 if (s < 60) return `${s}秒`
87 const m = Math.floor(s / 60)
88 if (m < 60) return `${m}分`
89 const h = Math.floor(m / 60)
90 if (h < 24) return `${h}時`
91 return `${Math.floor(h / 24)}天`
92}
93
94hooks/frame.ts 55 lines1/**
2 * 面板底部一格畫面的計算:依狀態取主題的動作、決定角色位置、合成場景、算出文字符號的位置。
3 * 面板繪製與 README 動作圖的產生腳本共用這裡,兩邊看到的是同一套動畫。
4 */
5import { mirror, sized, spriteWidth } from './sprite'
6import type { Sprite } from './sprite'
7import type { State, Theme } from './theme'
8
9/** 蓋在場景上的文字符號:第 line 行(文字行)、第 col 欄。 */
10export type Mark = { line: number; col: number; text: string; color: string }
11
12export type FrameInput = {
13 theme: Theme
14 state: State
15 x: number
16 facing: 1 | -1
17 frame: number
18 /** 角色的倍數(factorFor 算出來的)。 */
19 factor: number
20 columns: number
21 /** 場景最多能佔幾行文字;放不下完整背景時由主題決定怎麼省略。 */
22 rows: number
23 /** 進入 resting 時抽的亂數(0 ≤ seed < 1),主題用來挑這段 resting 期間不變的內容(例如名言)。 */
24 seed?: number
25}
26
27export type Frame = {
28 sprite: Sprite
29 marks: Mark[]
30 /** 角色實際畫出的 x。 */
31 x: number
32 /** 角色最上面那一行文字。 */
33 tigerLine: number
34 /** 角色可走的範圍與每步欄數,給走路的計時器用。 */
35 room: number
36 stride: number
37}
38
39export const paneFrame = (input: FrameInput): Frame => {
40 const fitted = input.theme.fit?.(input.factor) ?? input.factor
41 const o = fitted === input.factor ? input : { ...input, factor: fitted }
42 const act = o.theme.acts[o.state]
43 const pose = sized(act.pose(o.frame), o.factor)
44 const width = spriteWidth(pose)
45 const room = Math.max(0, o.columns - width)
46 const stride = Math.max(1, Math.round(o.factor))
47 const x = act.place?.(o, width) ?? Math.min(o.x, room)
48 // 背景位移跟著角色實際畫出的 x
49 const scene = o.theme.compose({ pose: o.facing === 1 ? pose : mirror(pose), x, columns: o.columns, rows: o.rows })
50 const tigerLine = Math.floor(scene.top / 2)
51 const marks = act.marks?.(o, { x, width, top: scene.top, line: tigerLine }) ?? []
52
53 return { sprite: scene.sprite, marks, x, tigerLine, room, stride }
54}
55hooks/sprite.ts 127 lines1/** 像素圖的共用工具:縮放、翻轉、轉成半格字元、在畫面上寫字。與主題無關。 */
2import { charWidth } from './text'
3
4/** 像素圖:每個字元是一個像素,'.' 為透明;顏色由主題的調色盤決定。 */
5export type Sprite = readonly string[]
6
7export const spriteWidth = (s: Sprite): number => s[0]?.length ?? 0
8
9/** 整數倍放大(最近鄰),保持像素風。 */
10export const scale = (s: Sprite, k: number): Sprite =>
11 s.flatMap(row => {
12 const wide = [...row].map(px => px.repeat(k)).join('')
13 return Array.from({ length: k }, () => wide)
14 })
15
16/** 左右翻轉,讓角色往左走。 */
17export const mirror = (s: Sprite): Sprite => s.map(row => [...row].reverse().join(''))
18
19/** 最近鄰縮放到指定像素寬高,可放大也可縮小。 */
20export const resize = (s: Sprite, width: number, height: number): Sprite => {
21 const srcH = s.length
22 const srcW = spriteWidth(s)
23 return Array.from({ length: height }, (_, y) => {
24 const row = s[Math.min(srcH - 1, Math.floor((y * srcH) / height))] ?? ''
25 return Array.from({ length: width }, (_, x) => row[Math.min(srcW - 1, Math.floor((x * srcW) / width))] ?? '.').join('')
26 })
27}
28
29export const MAX_FACTOR = 3
30/** 整體縮小比例:依用量算出的大小再乘上這個值。 */
31export const SIZE_SCALE = 0.5
32
33/**
34 * 上下文 100% 時的倍數:面板能容納的整數倍(最多 3 倍)再乘上 SIZE_SCALE。
35 * 其他用量依比例縮小,最小為主題的最小倍數;還沒有用量時用最小值。
36 * width 是角色 1 倍時的像素寬。
37 */
38export const factorFor = (percent: number | undefined, columns: number, size: { width: number; minFactor: number }): number => {
39 const full = Math.max(1, Math.min(MAX_FACTOR, Math.floor(columns / size.width)))
40 if (percent === undefined) return size.minFactor
41 const ratio = Math.min(100, Math.max(0, percent)) / 100
42 return Math.max(size.minFactor, full * ratio * SIZE_SCALE)
43}
44
45/** 依倍數縮放姿勢,寬高至少 1 像素。 */
46export const sized = (s: Sprite, factor: number): Sprite =>
47 resize(s, Math.max(1, Math.round(spriteWidth(s) * factor)), Math.max(1, Math.round(s.length * factor)))
48
49export type Run = { text: string; fg?: string; bg?: string }
50
51/** 每兩列像素合成一列半格字元(▀ ▄),每格一個 Run;palette 是主題的調色盤。 */
52export const toCells = (s: Sprite, palette: Record<string, string>): Run[][] => {
53 const lines: Run[][] = []
54 for (let y = 0; y < s.length; y += 2) {
55 const top = s[y] ?? ''
56 const bottom = s[y + 1] ?? ''
57 lines.push(
58 [...top].map((px, x) => {
59 const t = palette[px]
60 const b = palette[bottom[x] ?? '.']
61 if (t === undefined && b === undefined) return { text: ' ' }
62 if (t === undefined) return { text: '▄', fg: b }
63 return b === undefined ? { text: '▀', fg: t } : { text: '▀', fg: t, bg: b }
64 }),
65 )
66 }
67 return lines
68}
69
70/** 相同顏色、相同字元的相鄰格併成一段。 */
71export const joinCells = (lines: readonly (readonly Run[])[]): Run[][] =>
72 lines.map(cells => {
73 const runs: Run[] = []
74 for (const cell of cells) {
75 const last = runs[runs.length - 1]
76 if (last !== undefined && last.fg === cell.fg && last.bg === cell.bg && last.text[0] === cell.text) {
77 last.text += cell.text
78 } else {
79 runs.push({ ...cell })
80 }
81 }
82 return runs
83 })
84
85export const toRuns = (s: Sprite, palette: Record<string, string>): Run[][] => joinCells(toCells(s, palette))
86
87/**
88 * 在第 line 行第 at 欄寫字;底色用該格下半像素的顏色,空白不蓋背景。
89 * 寬字元(中文)佔兩格:字放在第一格,第二格清成空字串;放不下整個字就不畫。
90 */
91export const stamp = (
92 cells: readonly (readonly Run[])[],
93 s: Sprite,
94 line: number,
95 at: number,
96 text: string,
97 color: string,
98 palette: Record<string, string>,
99): Run[][] =>
100 cells.map((row, l) => {
101 if (l !== line) return [...row]
102 const out = [...row]
103 let col = at
104 for (const ch of text) {
105 const width = charWidth(ch)
106 const fits = col >= 0 && col + width <= out.length
107 if (ch !== ' ' && fits) {
108 const bg = palette[s[line * 2 + 1]?.[col] ?? '.']
109 out[col] = bg === undefined ? { text: ch, fg: color } : { text: ch, fg: color, bg }
110 if (width === 2) out[col + 1] = { text: '' }
111 }
112 col += width
113 }
114 return out
115 })
116
117export type Walker = { x: number; facing: 1 | -1 }
118
119/** 往前走一步,碰到邊界就轉身。 */
120export const step = (w: Walker, room: number, stride: number): Walker => {
121 const max = Math.max(0, room)
122 const next = w.x + w.facing * stride
123 if (next > max) return { x: max, facing: -1 }
124 if (next < 0) return { x: 0, facing: 1 }
125 return { x: next, facing: w.facing }
126}
127hooks/theme.ts 64 lines1/**
2 * 面板底部的主題:一組角色、背景,與它們在各個狀態下的動作。
3 * 狀態與主題無關;主題決定每個狀態要演什麼。
4 */
5import type { FrameInput, Mark } from './frame'
6import type { Sprite } from './sprite'
7import { puma } from './puma'
8import { tiger } from './tiger'
9
10/** session 的活動程度:工作中、回覆結束後 30 秒內、閒置超過 30 秒。 */
11export type State = 'working' | 'resting' | 'idle'
12
13/** 閒置多久後從 resting 進入 idle。 */
14export const NAP_AFTER_MS = 30_000
15
16export const stateFor = (isWorking: boolean, idleSince: number, now: number): State => {
17 if (isWorking) return 'working'
18 return idleSince > 0 && now - idleSince < NAP_AFTER_MS ? 'resting' : 'idle'
19}
20
21/** 背景與角色合成後的整格畫面。 */
22export type Scene = {
23 sprite: Sprite
24 /** 角色最上面一列像素在場景裡的列數。 */
25 top: number
26}
27
28/** 角色在這一格實際的位置:x、寬度(欄)、最上面一列像素、最上面一行文字。 */
29export type Placed = { x: number; width: number; top: number; line: number }
30
31/** 主題在某個狀態下的動作。 */
32export type Act = {
33 /** 第 frame 格的姿勢:原始大小、面向右。 */
34 pose: (frame: number) => Sprite
35 /** 角色實際畫在哪一欄;沒有時夾在可走範圍內。width 是縮放後的寬度。 */
36 place?: (o: FrameInput, width: number) => number
37 /** 蓋在場景上的文字符號;沒有時不畫。 */
38 marks?: (o: FrameInput, at: Placed) => Mark[]
39}
40
41export type Theme = {
42 name: string
43 acts: Record<State, Act>
44 /** 背景合成:把擺好的角色疊到背景上;rows 是場景最多能佔幾行文字。 */
45 compose: (o: { pose: Sprite; x: number; columns: number; rows: number }) => Scene
46 /** 像素字元 → 色碼,涵蓋角色與背景。 */
47 palette: Record<string, string>
48 /** 角色 1 倍時的像素寬,給 factorFor 算體型。 */
49 width: number
50 /** 體型的最小倍數。 */
51 minFactor: number
52 /** 依面板算出的倍數再由主題微調(例如保證縮小後還看得到眼睛);沒有時照用。 */
53 fit?: (factor: number) => number
54}
55
56export const THEMES = { tiger, puma } satisfies Record<string, Theme>
57
58export type ThemeName = keyof typeof THEMES
59
60export const DEFAULT_THEME: ThemeName = 'tiger'
61
62/** 以名稱取得主題;不認得的名稱用預設主題。 */
63export const themeFor = (name: string): Theme => (Object.hasOwn(THEMES, name) ? THEMES[name as ThemeName] : THEMES[DEFAULT_THEME])
64hooks/choice.ts 43 lines1/**
2 * 使用者選的主題:/sessions theme 的切換規則,與 ~/.claude/session-radar.json 的讀寫。
3 * 主題名稱一律從登錄表推導,新增主題時這裡不用改。
4 */
5import { THEMES } from './theme'
6import type { ThemeName } from './theme'
7
8/** 依登錄表順序的主題名稱,/sessions theme 依這個順序輪流。 */
9export const THEME_NAMES = Object.keys(THEMES) as ThemeName[]
10
11/** 設定檔相對於家目錄的位置。 */
12export const SETTINGS_PATH = '.claude/session-radar.json'
13
14export type ThemeChoice = { kind: 'chosen'; name: string } | { kind: 'unknown'; name: string; names: string[] }
15
16/** 沒指定名稱就換到清單中的下一個(最後一個之後回到第一個);指定了就檢查名稱在不在清單裡。 */
17export const chooseTheme = (current: string, name: string | undefined, names: readonly string[] = THEME_NAMES): ThemeChoice => {
18 if (name === undefined) return { kind: 'chosen', name: names[(names.indexOf(current) + 1) % names.length] ?? current }
19 return names.includes(name) ? { kind: 'chosen', name } : { kind: 'unknown', name, names: [...names] }
20}
21
22/** 設定檔內容轉成物件;不存在、壞掉或不是物件時回 undefined。 */
23const parseObject = (text: string | undefined): Record<string, unknown> | undefined => {
24 if (text === undefined) return undefined
25 let raw: unknown
26 try {
27 raw = JSON.parse(text)
28 } catch {
29 return undefined
30 }
31 return typeof raw === 'object' && raw !== null && !Array.isArray(raw) ? (raw as Record<string, unknown>) : undefined
32}
33
34/** 從設定檔內容讀出主題名稱;檔案不存在(undefined)、壞掉、或名稱不在清單裡時回 undefined。 */
35export const parseThemeSetting = (text: string | undefined, names: readonly string[] = THEME_NAMES): string | undefined => {
36 const theme = parseObject(text)?.theme
37 return typeof theme === 'string' && names.includes(theme) ? theme : undefined
38}
39
40/** 把主題寫進設定檔內容,保留其他欄位;原內容不能用時只寫主題。 */
41export const withThemeSetting = (text: string | undefined, name: string): string =>
42 `${JSON.stringify({ ...parseObject(text), theme: name }, null, 2)}\n`
43hooks/text.ts 65 lines1/** 文字在終端機上的顯示寬度與換行:中日韓全形字佔 2 欄,其他佔 1 欄。與主題無關。 */
2
3/** 佔 2 欄的字元範圍(East Asian Wide / Fullwidth 的常用區段)。 */
4const WIDE: readonly (readonly [number, number])[] = [
5 [0x1100, 0x115f], // 韓文字母
6 [0x2e80, 0x303e], // 部首、CJK 標點(「」。、)
7 [0x3041, 0x33ff], // 假名、注音、CJK 相容字元
8 [0x3400, 0x4dbf], // CJK 擴充 A
9 [0x4e00, 0x9fff], // CJK 統一漢字
10 [0xa000, 0xa4cf], // 彝文
11 [0xac00, 0xd7a3], // 韓文音節
12 [0xf900, 0xfaff], // CJK 相容漢字
13 [0xfe30, 0xfe4f], // CJK 相容標點
14 [0xff00, 0xff60], // 全形英數與標點(,?!)
15 [0xffe0, 0xffe6], // 全形符號
16 [0x20000, 0x3fffd], // CJK 擴充 B 以後
17]
18
19/** 單一字元(code point)的顯示寬度。 */
20export const charWidth = (ch: string): 1 | 2 => {
21 const code = ch.codePointAt(0) ?? 0
22 return WIDE.some(([from, to]) => code >= from && code <= to) ? 2 : 1
23}
24
25/** 字串的顯示寬度。 */
26export const displayWidth = (text: string): number => [...text].reduce((sum, ch) => sum + charWidth(ch), 0)
27
28export const ELLIPSIS = '…'
29
30/** 不可放在行首的標點。 */
31const NO_LINE_START = ',。、?!;:」』)…'
32
33/**
34 * 以字為單位換行:每行顯示寬度不超過 width,寬字元不會被切成兩半。
35 * 超過 maxLines 行時只留前 maxLines 行,最後一行以 … 結尾(必要時再拿掉字讓出位置)。
36 * width 放不下任何一個字時回傳空陣列。
37 */
38export const wrapText = (text: string, width: number, maxLines: number): string[] => {
39 if (maxLines <= 0 || [...text].some(ch => charWidth(ch) > width)) return []
40 const lines: string[] = []
41 let line = ''
42 for (const ch of text) {
43 if (displayWidth(line + ch) > width) {
44 // 行首禁則:標點不能開頭,把前面的字一起帶到下一行
45 let carry = ch
46 while (NO_LINE_START.includes(carry[0] ?? '') && [...line].length > 1) {
47 carry = [...line].slice(-1).join('') + carry
48 line = [...line].slice(0, -1).join('')
49 }
50 lines.push(line)
51 line = carry
52 continue
53 }
54 line += ch
55 }
56 if (line !== '') lines.push(line)
57 if (lines.length <= maxLines) return lines
58
59 const kept = lines.slice(0, maxLines)
60 let last = kept[maxLines - 1] ?? ''
61 while (last !== '' && displayWidth(last + ELLIPSIS) > width) last = [...last].slice(0, -1).join('')
62 kept[maxLines - 1] = last + ELLIPSIS
63 return kept
64}
65hooks/puma.ts 171 lines1/**
2 * 綠雲與愛心主題(puma):工作中左右來回走,回覆結束後頭頂冒出 ♥ 與一句名言,閒置久了吃三色丸子。
3 * 背景是閃爍的星空,沒有地面;角色腳底貼著面板底部。
4 */
5import { DANGO } from './dango'
6import type { FrameInput, Mark } from './frame'
7import { PUMA_PALETTE, PUMA_WALK } from './puma-sprites'
8import { overlay } from './scene'
9import { mirror, sized, spriteWidth } from './sprite'
10import type { Sprite } from './sprite'
11import { starField, starMarks } from './stars'
12import { displayWidth, wrapText } from './text'
13import type { Act, Placed, Theme } from './theme'
14
15/** 名言(原文照用)。 */
16export const QUOTES: readonly string[] = [
17 '我們不要灰心,我們也不應該喪志,為什麼?因為我來了。',
18 '人類的讚歌就是勇氣的讚歌,我們會把勇氣繼續傳承下去。',
19 '如果有人說懷抱希望是一種錯誤,那麼我會每一次都反駁他。無論幾次,我都會堅定地說。',
20 '未來的事情無人知,但就是因為這樣,可能性才會如此強大。',
21 '我本來就不是以外表取勝。',
22 '洋流是溫暖的,可以帶來漁獲,不會燙傷人。',
23]
24
25/** 這段 resting 的名言:由進入 resting 時抽的亂數決定,期間不換;沒有亂數時用第一句。 */
26export const quoteFor = (seed: number | undefined): string =>
27 QUOTES[Math.min(QUOTES.length - 1, Math.max(0, Math.floor((seed ?? 0) * QUOTES.length)))] ?? ''
28
29/** 名言最多幾行。 */
30export const QUOTE_LINES = 3
31
32/** 原圖朝左;主題的姿勢一律面向右,所以先翻轉。 */
33const faceRight = (s: Sprite): Sprite => mirror(s)
34
35/** 走路圖(面向右)。 */
36export const WALK: readonly Sprite[] = PUMA_WALK.map(faceRight)
37
38/** 停下來時的站姿:固定用走路的第 1 格(朝左原圖)。 */
39const STAND = PUMA_WALK[0] ?? []
40
41/** 丸子疊在原圖(朝左)的哪個位置:竹籤尾端插在前面那隻黃色手的上緣。 */
42export const DANGO_AT = { x: 2, y: 7 }
43
44/** 丸子每個階段維持幾格。 */
45export const DANGO_FRAMES = 4
46
47/** 第 frame 格丸子吃到第幾階段:0~3 依序剩 3、2、1、0 顆,之後換新的一串。 */
48export const dangoStage = (frame: number): number => Math.floor(frame / DANGO_FRAMES) % DANGO.length
49
50/** 吃丸子的姿勢(面向右):站姿的手上疊著這個階段的丸子。 */
51export const eatPose = (frame: number): Sprite =>
52 faceRight(overlay(STAND, DANGO[dangoStage(frame)] ?? [], DANGO_AT.x, DANGO_AT.y))
53
54/**
55 * 白色眼睛區塊裡有黑色像素:某個黑色像素左右或上下都是眼白。
56 * 縮到最小時眼白整塊變白(眼珠被取樣掉)就不成立。
57 */
58export const hasPupil = (s: Sprite): boolean =>
59 s.some((row, y) =>
60 [...row].some(
61 (px, x) => px === 'X' && ((row[x - 1] === 'I' && row[x + 1] === 'I') || (s[y - 1]?.[x] === 'I' && s[y + 1]?.[x] === 'I')),
62 ),
63 )
64
65/** 最小倍數的起點;實際值會為了看得到黑眼珠往上調。 */
66export const BASE_MIN_FACTOR = 0.5
67
68/** 調高倍數時每次加多少。 */
69const FIT_STEP = 0.01
70
71const fitted = new Map<number, number>()
72
73/**
74 * 保底:縮放後 12 格走路圖都還看得到黑眼珠。縮放是最近鄰取樣,
75 * 某些倍數剛好會把眼珠取樣掉;這時往上加到 12 格都看得到為止。
76 * 檢查的是面板實際縮放的姿勢(面向右的版本),先翻轉再縮放取到的像素不一樣。
77 */
78export const eyeSafe = (factor: number): number => {
79 const cached = fitted.get(factor)
80 if (cached !== undefined) return cached
81 let f = factor
82 for (let i = 0; i < 100 && !WALK.every(p => hasPupil(sized(p, f))); i++) f = Math.round((f + FIT_STEP) * 1000) / 1000
83 fitted.set(factor, f)
84 return f
85}
86
87export const MIN_FACTOR = eyeSafe(BASE_MIN_FACTOR)
88
89/** 頭頂最多留幾行給對話框:名言 3 行加 ♥ 一行。 */
90export const HEAD_LINES = QUOTE_LINES + 1
91
92const evenUp = (n: number): number => n + (n % 2)
93
94/** 角色在場景裡佔的範圍(文字行):最上面有顏色的那一行到場景底部。 */
95type Figure = { head: number; bottom: number; drawn: boolean }
96
97const figureOf = (pose: Sprite, o: FrameInput, at: Placed): Figure => {
98 const sizedPose = sized(pose, o.factor)
99 const firstRow = Math.max(0, sizedPose.findIndex(row => /[^.]/.test(row)))
100 return {
101 head: Math.floor((at.top + firstRow) / 2),
102 bottom: Math.ceil((at.top + sizedPose.length) / 2),
103 drawn: at.width <= o.columns,
104 }
105}
106
107/** 頭頂的文字:一行一個 Mark,每行以角色為中心,碰到面板邊緣往內推。 */
108const centered = (o: FrameInput, at: Placed, line: number, text: string, color: string): Mark => {
109 const width = displayWidth(text)
110 const center = at.x + Math.floor(at.width / 2)
111 const col = Math.max(0, Math.min(center - Math.floor(width / 2), o.columns - width))
112 return { line, col, text, color }
113}
114
115/**
116 * 對話框:頭頂上一行是 ♥,名言在 ♥ 上面。
117 * 頭頂的行數不夠時依序讓出:名言 3 行 → 1 行 → 不顯示名言只留 ♥ → 什麼都不畫。
118 */
119export const bubble = (o: FrameInput, at: Placed, head: number): Mark[] => {
120 if (head < 1) return []
121 const heart = centered(o, at, head - 1, '♥', 'redBright')
122 const room = head - 1
123 const maxLines = room >= QUOTE_LINES ? QUOTE_LINES : room >= 1 ? 1 : 0
124 const lines = wrapText(`「${quoteFor(o.seed)}」`, o.columns, maxLines)
125 const top = head - 1 - lines.length
126 return [...lines.map((text, i) => centered(o, at, top + i, text, 'white')), heart]
127}
128
129/** 星空:避開角色與頭頂文字佔用的格子;文字左右各多留一格,星星不會黏在字旁邊。 */
130const sky = (o: FrameInput, at: Placed, fig: Figure, over: readonly Mark[]): Mark[] => {
131 const used = new Set(over.flatMap(m => Array.from({ length: displayWidth(m.text) + 2 }, (_, i) => `${m.line},${m.col - 1 + i}`)))
132 const onFigure = (line: number, col: number) => fig.drawn && line >= fig.head && col >= at.x && col < at.x + at.width
133 return starMarks(starField(o.columns, fig.bottom), o.frame, (line, col) => onFigure(line, col) || used.has(`${line},${col}`))
134}
135
136/** 動作:pose 之外,符號一律是「頭頂文字+避開它們的星空」。 */
137const act = (pose: (frame: number) => Sprite, overhead: (o: FrameInput, at: Placed, head: number) => Mark[]): Act => ({
138 pose,
139 marks: (o, at) => {
140 const fig = figureOf(pose(o.frame), o, at)
141 const over = fig.drawn ? overhead(o, at, fig.head) : []
142 return [...sky(o, at, fig, over), ...over]
143 },
144})
145
146/** 吃到剩 2 顆時頭頂冒出 nom。 */
147const nom = (o: FrameInput, at: Placed, head: number): Mark[] =>
148 dangoStage(o.frame) === 1 && head >= 1 ? [centered(o, at, head - 1, 'nom', 'yellow')] : []
149
150export const puma: Theme = {
151 name: 'puma',
152 acts: {
153 working: act(frame => WALK[frame % WALK.length] ?? [], () => []),
154 resting: act(() => faceRight(STAND), bubble),
155 idle: act(eatPose, nom),
156 },
157 // 星空不畫在像素圖上;場景只有角色,腳底貼底,頭頂最多留 HEAD_LINES 行。面板比角色窄時不畫角色。
158 compose: o => {
159 const body = evenUp(o.pose.length)
160 const head = Math.max(0, Math.min(HEAD_LINES, o.rows - body / 2))
161 const height = head * 2 + body
162 const top = height - o.pose.length
163 const empty: Sprite = Array.from({ length: height }, () => '.'.repeat(o.columns))
164 return { sprite: spriteWidth(o.pose) <= o.columns ? overlay(empty, o.pose, o.x, top) : empty, top }
165 },
166 palette: PUMA_PALETTE,
167 width: spriteWidth(STAND),
168 minFactor: MIN_FACTOR,
169 fit: factor => eyeSafe(Math.max(MIN_FACTOR, factor)),
170}
171hooks/tiger.ts 211 lines1/** 小老虎主題(預設):8-bit 老虎、樹林與草地背景,工作中巡邏、閒置時抓蝴蝶、久了睡覺。 */
2import type { FrameInput, Mark } from './frame'
3import { composeScene, fitsTrees } from './scene'
4import { spriteWidth } from './sprite'
5import type { Sprite } from './sprite'
6import type { Placed, Theme } from './theme'
7
8/** 老虎與樹林草地的顏色。 */
9export const PALETTE: Record<string, string> = {
10 O: '#f08a24', // 橘色毛
11 K: '#3b2410', // 深色條紋
12 W: '#f5efe2', // 白色肚子、嘴邊
13 E: '#111111', // 眼睛
14 N: '#ff8fa3', // 鼻子
15 G: '#2e7d32', // 樹冠
16 L: '#43a047', // 樹冠亮面
17 B: '#6d4c41', // 樹幹
18 g: '#7cc242', // 草
19 H: '#4e9a2f', // 草的暗面
20}
21
22const BODY = [
23 '............OO..OO',
24 '............OOKKOO',
25 'K...........OEOOEO',
26 '.O..OOOOOOOOOWNNWO',
27 '..OOKOOKOOKOOOWWW.',
28 '...OKOOKOOKOOOO...',
29 '...OWWWWWWWWWOO...',
30]
31
32export const WALK: readonly Sprite[] = [
33 [
34 ...BODY,
35 '...O.O......O.O...',
36 '..O...O....O...O..',
37 '..W...W....W...W..',
38 ],
39 [
40 ...BODY,
41 '....OO......OO....',
42 '....OO......OO....',
43 '....WW......WW....',
44 ],
45]
46
47/** 站著抓蝴蝶:0 四腳站好,1 舉起前掌。 */
48export const PLAY: readonly Sprite[] = [
49 WALK[1] ?? [],
50 [
51 ...BODY.slice(0, 4),
52 '..OOKOOKOOKOOOWWWW',
53 '...OKOOKOOKOOOO.OO',
54 '...OWWWWWWWWWOOO..',
55 '....OO......O.....',
56 '....OO......O.....',
57 '....WW......W.....',
58 ],
59]
60
61/** 尾巴往後平甩:把翹起的尾巴改成水平。 */
62const swish = (s: Sprite): Sprite =>
63 s.map((row, y) => (y === 2 ? '.' + row.slice(1) : y === 3 ? '..' + row.slice(2) : y === 4 ? 'KO' + row.slice(2) : row))
64
65const STAND = PLAY[0] ?? []
66
67/** 壓低身體準備撲:整隻往下一列,腿縮短。 */
68const CROUCH: Sprite = [
69 '..................',
70 ...BODY,
71 '....OO......OO....',
72 '....WW......WW....',
73]
74
75/** 跳起來拍:舉起前掌,四腳離地一列。 */
76const POUNCE: Sprite = [
77 ...(PLAY[1] ?? []).slice(0, 7),
78 '....OO......O.....',
79 '....WW......W.....',
80 '..................',
81]
82
83/**
84 * 抓蝴蝶時每一步的姿勢,與蝴蝶的飛行步驟一一對應:
85 * 盯著看甩尾 → 壓低身體扭屁股 → 跳起來拍 → 落地舉掌 → 甩尾看牠飛走。
86 */
87export const PLAY_STEPS: readonly Sprite[] = [
88 STAND,
89 swish(STAND),
90 CROUCH,
91 swish(CROUCH),
92 POUNCE,
93 PLAY[1] ?? STAND,
94 swish(STAND),
95 STAND,
96]
97
98export const SLEEP: Sprite = [
99 '............OO..OO',
100 '............OOKKOO',
101 '..OOOOOOOOOOOKOOKO',
102 '.OOKOOKOOKOOOWNNWO',
103 'OKOOOOOOOOOOOOWWW.',
104 '..OOOOOOOOOOWWWW..',
105]
106
107/** 把某一列換成新內容。 */
108const withRow = (s: Sprite, y: number, row: string): Sprite => s.map((r, i) => (i === y ? row : r))
109
110/** 吸氣:背部鼓起一列。 */
111const inhale = (s: Sprite): Sprite => withRow(s, 1, '....OOOOOOO.OOKKOO')
112
113/** 尾巴尖翹起來抖一下。 */
114const flick = (s: Sprite): Sprite =>
115 withRow(withRow(s, 3, 'KOOKOOKOOKOOOWNNWO'), 4, '.OOOOOOOOOOOOOWWW.')
116
117/** 後面那隻耳朵抖一下(折下去)。 */
118const twitch = (s: Sprite): Sprite => withRow(s, 0, '............OO....')
119
120/** 睡覺動作一輪的格數(每格 0.4 秒)。 */
121export const SLEEP_CYCLE = 16
122
123/**
124 * 第 frame 格的睡姿:每 1.2 秒一吸一吐,
125 * 每輪中段尾巴抖兩格,接近尾聲耳朵抖一格。
126 */
127export const sleepPose = (frame: number): Sprite => {
128 const step = frame % SLEEP_CYCLE
129 const breath = Math.floor(frame / 3) % 2 === 1 ? inhale(SLEEP) : SLEEP
130 const tail = step === 8 || step === 9 ? flick(breath) : breath
131 return step === 13 ? twitch(tail) : tail
132}
133
134/** 老虎的最小倍數。 */
135export const MIN_FACTOR = 0.5
136
137const ZZZ = ['z', 'z Z', 'z Z z', '']
138
139export const snore = (frame: number): string => ZZZ[Math.floor(frame / 2) % ZZZ.length] ?? ''
140
141/** 前掌在原始圖裡的像素列(第 6 列,從 0 起算第 5 列)。 */
142export const PAW_ROW = 5
143
144/**
145 * 蝴蝶一輪飛行的每一步(每步一格動畫):從頭頂飛下來、停到掌邊被拍到、再逃走。
146 * row 為 'above' 時在老虎頭頂那一行;否則是相對前掌那一行的列差。
147 * col 是離老虎身體前緣的欄數,hit 表示這一步被拍到、老虎舉起前掌。
148 */
149const FLIGHT: readonly { row: 'above' | number; col: number; hit?: boolean }[] = [
150 { row: 'above', col: 5 },
151 { row: 'above', col: 3 },
152 { row: -1, col: 3 },
153 { row: -1, col: 1 },
154 { row: 0, col: 0, hit: true },
155 { row: 0, col: 1, hit: true },
156 { row: -1, col: 4 },
157 { row: 'above', col: 6 },
158]
159
160export type Flight = { row: number; col: number; glyph: string; isHit: boolean }
161
162/**
163 * 第 frame 格時蝴蝶的位置。row 已換算成老虎圖的文字列(-1 為頭頂那行),
164 * pawLine 是前掌所在的文字列。
165 */
166export const flight = (frame: number, pawLine: number): Flight => {
167 const at = FLIGHT[frame % FLIGHT.length] ?? { row: 'above', col: 5 }
168 const row = at.row === 'above' ? -1 : Math.max(-1, pawLine + at.row)
169 const isHit = at.hit === true
170 return { row, col: at.col, isHit, glyph: isHit ? '*' : frame % 2 === 0 ? 'ʚɞ' : '><' }
171}
172
173/** 抓蝴蝶時,蝴蝶需要的前方空間(欄)。 */
174export const FLIGHT_ROOM = 8
175
176/** 抓蝴蝶時讓出前方空間給蝴蝶。 */
177const placeForPlay = (o: FrameInput, width: number): number =>
178 o.facing === 1
179 ? Math.max(0, Math.min(o.x, o.columns - width - FLIGHT_ROOM))
180 : Math.min(Math.max(0, o.columns - width), Math.max(o.x, FLIGHT_ROOM))
181
182/** 睡覺時打呼:靠老虎右緣、在頭頂那一行;符號直接蓋在背景上。 */
183const snoreMarks = (o: FrameInput, at: Placed): Mark[] => {
184 const text = snore(o.frame)
185 const col = Math.min(Math.max(0, at.x + at.width - text.length), o.columns - text.length)
186 return [{ line: at.line - 1, col, text, color: 'cyan' }]
187}
188
189/** 抓蝴蝶時蝴蝶在頭頂或掌邊。 */
190const butterflyMarks = (o: FrameInput, at: Placed): Mark[] => {
191 const pawLine = Math.floor((at.top + Math.floor(PAW_ROW * o.factor)) / 2)
192 const fly = flight(o.frame, pawLine - at.line)
193 // 蝴蝶的欄位:老虎面向哪邊,就在那一側的身體前緣外
194 const col = o.facing === 1 ? at.x + at.width + fly.col : at.x - fly.glyph.length - fly.col
195 return [{ line: at.line + fly.row, col, text: fly.glyph, color: fly.isHit ? 'yellow' : 'magenta' }]
196}
197
198export const tiger: Theme = {
199 name: 'tiger',
200 acts: {
201 working: { pose: frame => WALK[frame % WALK.length] ?? SLEEP },
202 resting: { pose: frame => PLAY_STEPS[frame % PLAY_STEPS.length] ?? SLEEP, place: placeForPlay, marks: butterflyMarks },
203 idle: { pose: sleepPose, marks: snoreMarks },
204 },
205 // 老虎疊在樹林前、站在草地上;放不下含樹的場景時只畫草
206 compose: o => composeScene({ pose: o.pose, x: o.x, columns: o.columns, withTrees: fitsTrees(o.rows, 0, o.pose.length) }),
207 palette: PALETTE,
208 width: spriteWidth(SLEEP),
209 minFactor: MIN_FACTOR,
210}
211hooks/dango.ts 17 lines1/**
2 * 三色丸子(手畫):粉、白、綠三顆串在竹籤上,綠雲在 idle 時從最上面一顆顆吃掉。
3 * 顏色字元用 puma 調色盤(見 puma-sprites.ts):P 粉、I 白、C 綠、D 竹籤。
4 */
5type Sprite = readonly string[]
6
7const STICK = '.D.'
8
9/** 竹籤由上往下:粉、白、綠各兩列,最下面一列是手拿的竹籤尾端。 */
10const BALLS = ['PPP', 'PPP', 'III', 'III', 'CCC', 'CCC']
11
12/** 吃掉最上面 eaten 顆,被吃掉的位置露出竹籤。 */
13const bite = (eaten: number): Sprite => [...BALLS.map((row, y) => (y < eaten * 2 ? STICK : row)), STICK]
14
15/** 4 個階段:剩 3、2、1、0 顆。 */
16export const DANGO: readonly Sprite[] = [bite(0), bite(1), bite(2), bite(3)]
17hooks/puma-sprites.ts 502 lines1/**
2 * 綠雲與愛心(puma 主題)的像素圖:每個字元是一個像素,'.' 為透明。
3 * 此檔由 scripts/gen-puma.ts 從 scripts/assets/green-cloud-and-heart.png 產生,請勿手改;
4 * 要調整請改腳本後執行 node scripts/gen-puma.ts。
5 */
6type Sprite = readonly string[]
7
8/** puma 調色盤:C 綠雲、Y 手腳、I 眼白、X 眉毛與輪廓、R 愛心、P 腮紅、D 愛心的腳。 */
9export const PUMA_PALETTE: Record<string, string> = {
10 C: '#5bb987',
11 Y: '#fad776',
12 I: '#fafafa',
13 X: '#111111',
14 R: '#f2466a',
15 P: '#fb8ea6',
16 D: '#8f5232',
17}
18
19/** 走路 12 格(原圖第 1 列左到右,再第 2 列),角色朝左;第 12 格沒有愛心。 */
20export const PUMA_WALK: readonly Sprite[] = [
21 [
22 '.....................................................',
23 '.....................................................',
24 '.....................................................',
25 '.....................................................',
26 '...............XXXX..................................',
27 '..............XCCCCXX................................',
28 '............XXCCCCCCCX.XXX...........................',
29 '.........XXXXXCCCCCCCXXCCXX..........................',
30 '........XCCCXXCCCCCCCXXCCCCX.........................',
31 '.......XXCCCCXXCCCCCCXCCCCCX.........................',
32 '.......XCCCCCCCCCCCCCCCCCCCX.........................',
33 '......XCCCCCCCCCCCCCCCCCCCCXX........................',
34 '...XX.XCCCCCCCCCCCCCCCCCCCCCCX.......................',
35 '..XYYXXCCCCCCCCCCCXXCCCCCCCCCCX......................',
36 '.XYYYYXXCCXXXCCXXXCCCCCCCCCCCCX......................',
37 'XYYYYXXXCXIIIXCCCXXXXCCCCCCCCCX......................',
38 'XYYYXXCCXIIIIIXXXIIIIXCCCCCCCCX......................',
39 'XYYYXCCCXIIIXIXXIIIIIIXCCCCCCX.......................',
40 'XYYYXCCCXIIIIIXXIXIIIIXCCCCCCX.......................',
41 'XYYYYXCCXXIIIXXXIIIIIIXCCCCCXXXX.....................',
42 '.XYYYYXCCXXXXXCXIIIIIIXCCCXXYYYYX....XXXX............',
43 '..XYYXXCCCXXXCCCXIIIIXCCCCXYYYYYXX..XRRRRX...........',
44 '..XXXXCCCCCCCCCCCXXXXCCCCXXXYYYYYX.XRRRRRRXXXXX......',
45 '...XXCCCCCCCCCCCCCCCCCCCCCCXXXYYYXXRRRRRRRXXRRRX.....',
46 '....XCCCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRRRRRRRRX....',
47 '.....XXCCCCCCCXCCCCCCCCCCCCXYYYYYXRRRRRRRXXXRRRRRX...',
48 '.....XCCCXXXXXXCCCCCCCCCCCCXYYYYXRRRRRXRRXXXRRRRRX...',
49 '......XCXYYYYYXXXCCCCCCCCCCCXXXX.XRRRPRXRXRRRRRRRX...',
50 '......XXYYYYYYYXCCCCCCCCCCCCX....XRRRRRRXRRRRRRRRX...',
51 '...XX.XYYYYYYYYXCCCCCCCCXCCX....XXXRRRRRRRRRRRRRX....',
52 '..XYYXYYYYYXXXXXXXCCCCCCXXX....XXRXRRRRRRRRRRRRX.....',
53 '..XYYYYYYYXXXCCCCXXCCCXXYYXX...XXRXXRRRRRRRRXXRRX....',
54 '...XYYYYYX..XXXXX.DXXXXYYYYX.....XX.XRRRRRRRXXRX.....',
55 '...XYYYYYX.......XXYYYYYYYYX......XXDXRRRRRXDXX......',
56 '....XYYYX..........XYYYYXYYX.....XDDDDXXXXXDDDX......',
57 '.....XXX............XXXXXYYX.....XDDDDX...XXDDX......',
58 '.........................XX.......XDDX....XDDX.......',
59 '...................................XX......XX........',
60 ],
61 [
62 '.....................................................',
63 '.....................................................',
64 '.....................................................',
65 '.....................................................',
66 '.....................................................',
67 '..................XXX................................',
68 '.................XCCCXX..............................',
69 '............XXXXXCCCCCCXXXX..........................',
70 '...........XCCCCXCCCCCXXCCCX.........................',
71 '..........XCCCCCXXCCCCXCCCCCX........................',
72 '.........XCCCCCCCCCCCCCCCCCCXX.......................',
73 '.........XCCCCCCCCCCCCCCCCCCCCX......................',
74 '.....XXXXCCCCCCCXCCCCCCCCCCCCCCX.....................',
75 '....XYYYXCCCXXXCXXXCCCCCCCCCCCCX.....................',
76 '...XYYYYXXXXIIIXCCXXXCCCCCCCCCCX.....................',
77 '...XYYYYXCXIIIIIXXIIIXCCCCCCXXCX.....................',
78 '...XYYYXCCXIIIXIXIIIIIXCCCCCCCX......................',
79 '...XYYYXCCXIIIIIXIXIIIXCCCCCCCX......................',
80 '...XYYYYXCXIIIIIXIXIIIXCCCCCCXXX.....................',
81 '...XYYYYYXCXIIIXXIIIIIXCCCCXXYYYX....................',
82 '....XYYYYXCCXXXCCXIIIXCCCCCXYYYYYX....XXX............',
83 '.....XYYXCCCCCCCCCXXXCCCCXXXXYYYYXX..XXXXXX..........',
84 '......XXCCCCCCCCCCCCCCCCCCCCCXXYYYXXXXRRRXX.XXX......',
85 '.......XCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRXXRRRX.....',
86 '.......XCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRRRRRRRX....',
87 '.......XCCCCCCCCCCCCCCCCCCCCXYYYYYXRRRRRRXXXRRRRRX...',
88 '........XCCXXXXXXCCCCCCCCCCCXYYYYXRRRRRXRXXXXRRRRX...',
89 '........XCXYYYYXXCCCCCCCCCCCCXXXX.XRRRRXXXXXRRRRRX...',
90 '.....XXX.XYYYYYYXCCCCCCCCCCCCX....XRRRRRXXRRRRRRRX...',
91 '....XYYYXYYYYYYYXCCCCCCCXCCCX....XXXRRRRRRRRRRRRX....',
92 '....XYYYYYYYYYXXCCCCCCCCXXXX....XRRXRRRRRRRRRRRX.....',
93 '....XYYYYYYYYXXCCCXCCCCXYYYYX...XXRXXRRRRRRRRXXRX....',
94 '.....XXYYYYYX..XXX.XXXXYYYYYX....XXX.XRRRRRRRXXXX....',
95 '.....XDXYYYYX......XYYYYYYYYX......XXDXRRRRRXDXX.....',
96 '.......XYYYX........XYYYYYYYX.....XDDDDXXXXXDDDX.....',
97 '........XXX..........XXXXXYYX.....XDDDDX...XXDDX.....',
98 '..........................XX.......XDDX....XDDX......',
99 '....................................XX......XX.......',
100 ],
101 [
102 '.................XXXX................................',
103 '................XCCCCX...............................',
104 '...............XXCCCCCX.XX...........................',
105 '............XXXXCCCCCCXXCCX..........................',
106 '..........XXCCCXCCCCCXXXCCXX.........................',
107 '.........XXCCCCCCCCCCCXCCCCX.........................',
108 '........XXXCCCCCCCCCCCCCCCCXXX.......................',
109 '....XXX.XCCCCCCCCCCCCCCCCCCCCCX......................',
110 '...XXYYXXCCCCCCCXCCXCCCCCCCCCCCX.....................',
111 '...XYYYXCCCCXXXCXCXXCCCCCCCCCCCX.....................',
112 '..XYYYYYCXCCXXXCXXXCCCCCCCCCCCCX.....................',
113 '..XYYYYYXXCXIIIXCCXXXCCCCCCCCCCX.....................',
114 '..XYYYXXCCXIIIIIXXIIIXCCCCCCCCCX.....................',
115 '..XYYYXCCCXIIIXIXIIIIIXCCCCCCXX......................',
116 '..XYYYXXCCXIIIIIXIXIIIXCCCCCCXX......................',
117 '..XYYYYXCCCXIIIXXIIIIIXCCCCCXXX......................',
118 '...XYYYYXCCXXXXXXIIIIIXCCCXXYYYX.....................',
119 '...XXYYXXCCCXXXCCXIIIXCCCCXXYYYXX....................',
120 '....XXXXCCCCCCCCCCXXXCCCCXXYYYYYX...XXXX.............',
121 '.....XXCCCCCCCCCCCCCCCCCCCCXXXYYYX.XRRRRX............',
122 '......XCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRX.XXX.......',
123 '......XCCCCCCCCCCCCCCCCCCCCCYYYYYXXRRRRRRXXRRRX......',
124 '......XCCCCCCCCCCCCCCCCCCCCXYYYYYXRRRRRRRRRRRRRX.....',
125 '.......XCCCXXXXXXCCCCCCCCCCXYYYYXRRRRRRRXXRRRRRRX....',
126 '.......XCCXYYYYXXCCCCCCCCCCCXYYYXRRRRXRRXXXXRRRRX....',
127 '........XXXYYYYXCCCCCCCCCCCXXXXXRRRRRXRPRXXRRRRRX....',
128 '........XXYYYYYXCCCCCCCCCCCXX...XRRRPRXRXXRRRRRRX....',
129 '........XYYYYYYXCCCCCCCXCCX.....XRRRRRXXXRRRRRRRX....',
130 '.....XX.YYYYYYXXXXCCCCCXCCX....XXRRRRRRRRRRRRRRX.....',
131 '....XYYXYYYYYXXCCXCCCXXYXX....XXRXRRRRRRRRRRRRXX.....',
132 '....XYYYYYYYYXXCCXXXXYYYYXX...XXRXXRRRRRRRRRRRX......',
133 '....XYYYYYYYXX.XXXXYYYYYYYX...XXRXXRRRRRRRRRXRXX.....',
134 '.....XYYYYYXX.....XYYYYYYYX....XX..XXRRRRRRXXXX......',
135 '......XYYYXX.......XXXXXYYX......XXDXXXXXXDXDDX......',
136 '.......XXXX.............XX......XDDDDXXXXXXDDDX......',
137 '.......XXX..............XX......XDDDDX...XXXDDX......',
138 '.................................XDDX....XXDDX.......',
139 '..................................XX.......XX........',
140 ],
141 [
142 '.....................................................',
143 '.....................................................',
144 '.....................................................',
145 '.....................................................',
146 '..................XXX................................',
147 '.................XCCCX...............................',
148 '............XXX.XCCCCCXXXXX..........................',
149 '...........XCCCXXCCCCCXXCCXX.........................',
150 '..........XCCCCCXXCCCCXCCCCXX........................',
151 '..........XCCCCCCCCCCCCCCCCXX........................',
152 '.........XCCCCCCCCCCCCCCCCCXXX.......................',
153 '.....XX..XCCCCCCCCCCCCCCCCCCCXX......................',
154 '....XYYXXXCCCCCCCCCCCCCCCCCCCCXX.....................',
155 '...XYYYXXCCCXXXCCXXCCCCCCCCCCCCX.....................',
156 '..XYYYYYXXCXIIIXCCXXXCCCCCCCCCCX.....................',
157 '..XYYYXXCCXIIIIIXXIIIXCCCCCCXCCX.....................',
158 '..XYYYXCCCXIIIXIXIIIIIXCCCCCXXX......................',
159 '..XYYYXXCCXIIIIIXIXIIIXCCCCCCXX......................',
160 '..XYYYYXCCCXIIIXXIIIIIXCCCCCXXX......................',
161 '...XYYYYXXCCXXXCXXIIIXXCCCXXYYYX.....................',
162 '....XYYXCCCCCCCCCXXXXXCCXXXYYYYYXX...XXXX............',
163 '.....XXCCCCCCCCCCCCCCCCXXCXXXXYYYXX.XRRRRX..XXX......',
164 '......XCCCCCCCCCCCCCCCCCCCCCXYYYYXXXRRRRRRXXRRRX.....',
165 '......XCCCCCCCCCCCCCCCCCCCCXYYYYYXXRRRRRRRXRRRRRX....',
166 '.......XCCCCCCCXXCCCCCCCCCCXYYYYYXRRRRRRRRRRRRRRXX...',
167 '........XCCXXXXXCCCCCCCCCCCXYYYYXXRRRRRRRXXXXRRRRX...',
168 '........XCXYYYYXXXCCCCCCCCCCXXXX.XRRRRXRRRXXXRRRRX...',
169 '.........XYYYYYYXCCCCCCCCCCCX....XRRRPRXRXXRRRRRRX...',
170 '.........XYYYYYYXCCCCCCXCCCXX....XRRRRRRXXRRRRRRRX...',
171 '.....XXXXYYYYYXXXXCCCCCXCCX....XXRXRRRRRRRRRRRRRX....',
172 '....XYYYXYYYYXXCCXXCCCXYXX.....XRRXXRRRRRRRRRRRX.....',
173 '....XYYYYYYYYXXXXXXXXXYYYXX....XRRRXRRRRRRRRRXRDX....',
174 '....XYYYYYYYYX.XXXXYYYYYYYX.....XXX.XRRRRRRRRXXRX....',
175 '.....XYYYYYYX.....XYYYYYYYX.........XXRRRRRRXDXX.....',
176 '......XXYYYX.......XXXXYYYX.......XXDDXXXXXXDDDX.....',
177 '........XXX............XYYX......XDDDDX....XXDDX.....',
178 '........................XX........XDDX....XXDDX......',
179 '...................................XX.......XX.......',
180 ],
181 [
182 '.....................................................',
183 '.....................................................',
184 '.................XXXX................................',
185 '................XXCCCX...............................',
186 '............XXXXXCCCCCXXXX...........................',
187 '...........XCCCXXCCCCXXXCCX..........................',
188 '..........XCCCCCXCCCCXXCCCCX.........................',
189 '.........XCCCCCCCCCCCCCCCCCXX........................',
190 '........XCCCCCCCCCCCCCCCCCCXXX.......................',
191 '........XCCCCCCCCCCCCCCCCCCCCCXX.....................',
192 '.......XCCCCCCCCCCCCCCCCCCCCCCCXX....................',
193 '....XXXXCCCCXXXCCXXCCCCCCCCCCCCX.....................',
194 '...XYYYXXXCXIIIXCCXXXCCCCCCCCCCX.....................',
195 '..XYYYXXCCXIIIIIXXIIIXCCCCCCCCCX.....................',
196 '.XYYYXXXCCXIIIXIXIIIIIXCCCCCCXX......................',
197 '.XYYYXXCCCXIIIIIXIXIIIXCCCCCCCX......................',
198 '.XYYYXXCCCCXIIIXXIIIIIXCCCCCXXXXX....................',
199 '.XYYYYXXCCCXXXXXXIIIIIXCCCXXYYYYYX...................',
200 '.XYYYYYYXCCCXXXCCXIIIXCCCXXYYYYYYXXX..XXXX...........',
201 '..XYYYYXXCCCCCCCCCXXXCCCCXXXYYYYYYXX.XRRRRX..........',
202 '...XYYXCCCCCCCCCCCCCCCCCCCCCXXYYYYXXXRRRRRRX.XXXX....',
203 '....XXCCCCCCCCCCCCCCCCCCCCCCXYYYYYXXXRRRRRRXXXRRXX...',
204 '.....XCCCCCCCCCCCCCCCCCCCCCXYYYYYYXRRRRRRRRRRRRRRRX..',
205 '.....XCCCCCCCCXCCCCCCCCCCCCXYYYYYYXRRRRRRRXXRRRRRRX..',
206 '......XCCCXXXXXXCCCCCCCCCCCXYYYYYXRRRRRXRRXXRXRRRRX..',
207 '.......XCXYYYYXXCCCCCCCCCCCCXXXXX.XRRRRXRRXXXXRRRRR..',
208 '........XYYYYYXCCCCCCCCCCCCCX.....XRRRPRXXXXRRRRRRR..',
209 '.......XYYYYYYXCCXXCCCCXCCXX.....XXRRRRRRXXRRRRRRRR..',
210 '.......XYYYYYXXXXCCCCCCXXXX.....XXRXRRRRRRRRRRRRRRX..',
211 '......XYYYYYXXCCCXCCCCXYYYX....XXRRXXRRRRRRRRRRRXX...',
212 '......XYYYYYX.XXXXXXXXYYYYYX....XRRXXRRRRRRRRRXRRXX..',
213 '....XXXYYYYYX.....XYYYYYYYYX.....XX.XXRRRRRRRRXXRXX..',
214 '...XXYYYYYYX.......XYYYXXYYX......XXXDXRRRRRRXDXXX...',
215 '....XYYYYYXX........XXXXXYYX.....XXDDDDXXXXXXDDDXX...',
216 '....XXYYYYX..............XX......XXDDDDX....XXDDXX...',
217 '......XXXX........................XXDDX....XXDDXX....',
218 '....................................XX......XDDX.....',
219 '....................................XX.......XX......',
220 ],
221 [
222 '.................XXXX................................',
223 '................XCCCCX...............................',
224 '............XXXXCCCCCCXXXX...........................',
225 '............XXXXCCCCCCXXCCX..........................',
226 '..........XXCCCXCCCCCXXCCCXX.........................',
227 '.........XXCCCCCCCCCCXXCCCCX.........................',
228 '.........XXCCCCCCCCCCCCCCCCXX........................',
229 '........XCCCCCCCCCCCCCCCCCCXCX.......................',
230 '.....XX.XCCCCCCCCCCCCCCCCCCCCCXX.....................',
231 '....XYYXCCCCCCCCXCCCCCCCCCCCCCXX.....................',
232 '...XXYYXCXCCXXXCXXXCCCCCCCCCCCXX.....................',
233 '...XYYYYXXCXIIIXCCCCCCCCCCCCCCXX.....................',
234 '.XXYYYYXXXXXIIIXXCXXXCCCCCCCCCXX.....................',
235 '.XXYYYYXCCXIIIIIXXIIIXCCCCCCCCXX.....................',
236 '.XXYYYXCCCXIIIXIXIIIIIXCCCCCCX.......................',
237 '.XXYYYXCCCXIIIIIXIXIIIXCCCCCCXXX.....................',
238 '.XXYYYYXCCCXIIIXXIIIIIXCCCCCXXXX.....................',
239 '.XXYYYYXXCCXXXXXXIIIIIXCCCXXYYYYX....................',
240 '...XYYYYXCCCXXXCCXIIIXCCCCXYYYYYYXX.XXXX.............',
241 '....XYXXCCCCCCCCCCXXXCCCCXXXYYYYYXXXRRRRX............',
242 '.....XCCCCCCCCCCCCCCCCCCCCCXXXYYYXXRRRRRRXXXX........',
243 '.....XCCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRXXRRX.......',
244 '.....XCCCCCCCCCCCCCCCCCCCCCXYYYYXRRRRRRRRRRRRRX......',
245 '......XCXCCCCCCXCCCCCCCCCCCXYYYYYRRRRRRRXRRRRRRX.....',
246 '.......XXCCCCCCXCCCCCCCCCCCXYYYYYRRRRRRRXXXRRRRX.....',
247 '.......XCCCXXXXXXCCCCCCCCCCXXYYYXRRRRXRRXXXRRRRX.....',
248 '.......XXCXXYYYXCCCCCCCCCCCXXXXXDRRRPRXRXRRRRRRX.....',
249 '........XXYYYYYYCCCCCCCXXCCCX...XRRRRRRXXRRRRRRX.....',
250 '.........XYYYYYXXXCCCCCXXCX....XRXRRRRRRRRRRRRX......',
251 '......XXXYYYYYXCCXXCCCXYYX....XRRXXRRRRRRRRRRX.......',
252 '.....XYYYYYYYYXXXXXXXXYYYYX...XXRXXRRRRRRRRXRRX......',
253 '.....XYYYYYYYYXXXXXYYYYYYYX....XX..XRRRRRRRXXXX......',
254 '.....XXYYYYYXX....XXYYYYYYX.....XXXDXRRRRRXDXX.......',
255 '......XYYYYYXX.....XYYYXYYX....XXDDDDXXXXXDDDX.......',
256 '.......XYYYXX.......XXXXYYX....XXDDDDX...XXDDX.......',
257 '........XXXX............XX......XXDDX....XDDX........',
258 '.........XX......................XXX.....XDDX........',
259 '..................................XX......XX.........',
260 ],
261 [
262 '.....................................................',
263 '.....................................................',
264 '...................XXX...............................',
265 '.................XXCCCX..............................',
266 '................XXCCCCCX.XXX.........................',
267 '............XXXXXXCCCCCXXCCCX........................',
268 '..........XXCCCCXXCCCCCXXCCCCX.......................',
269 '.........XXCCCCCXXXCCCCXCCCCCX.......................',
270 '.........XCCCCCCCCCCCCCCCCCCCXXX.....................',
271 '........XCCCCCCCCCCCCCCCCCCCCCCCX....................',
272 '....XX..XCCCCCCCCCCCCCCCCCCCCCCCCX...................',
273 '....XX..XCCCCCCCCCCCCCCCCCCCCCCCCX...................',
274 '...XYYXXCCCXXXXCCXXXCCCCCCCCCCCCCX...................',
275 '..XYYYYXXCXIIIIXCCCXXXCCCCCCCCCCCX...................',
276 '.XYYYYXCCXIIIIIIXCXIIIXCCCCCCCCCXX...................',
277 '.XYYYXCCCXIIIIXIXXIIIIIXCCCCCCCXX....................',
278 '.XYYYXCCCXIIIIIIXXIXIIIXCCCCCCCXX....................',
279 '.XYYYYXCCCXIIIIXXXIIIIIXCCCCCCXXXX...................',
280 '.XYYYYYXCCCXXXXCCXIIIIIXCCCCXXYYYX.....XXXX..........',
281 '..XYYYYXCCCCCCCCCCXIIIXCCCCCXYYYYYX...XXXXXX.........',
282 '...XYYXCCCCCCCCCCCCXXXCCCCXXYYYYYYX...XRRRRX.........',
283 '....XXCCCCCCCCCCCCCCCCCCCXXCCXXXYYYX.XRRRRRRX.XXXX...',
284 '....XXCCCCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRRXXRRRXX..',
285 '.....XCCCCCCCCCCCCCCCCCCCCCCCXYYYYYXRRRRRRRRRRRRRRRX.',
286 '.....XCCCCCCCCCCCCCCCCCCCCCCCXYYYYYXRRRRRRRXRRRRRRRXX',
287 '......XCCCCCCCCXXCCCCCCCCCCCCXYYYYXRRRRRRRRXXRRRRRRRX',
288 '......XCCCXXXXXXXXCCCCCCCCCCCCXXXXXXRRRRXRRXXXXRRRRRX',
289 '.......XCXYYYYYXXXCCCCCCCCCCCCXXXX.XRRRPRXRXXRRRRRRRX',
290 '.......XXYYYYYYYXCCCCCCCCCCCCCX....XRRRRRXXXRRRRRRRRX',
291 '.......XYYYYYYYYXCCCCCCCXXCCXX....XXRRRRRRRRRRRRRRRXX',
292 '......XYYYYYYYXXXXXCCCCCXXXXX.....XRXRRRRRRRRRRRRRXX.',
293 '..XX.XYYYYYYYXXCCCXXCCCXYYYYYX....XRRXRRRRRRRRRRRXX..',
294 '.XYYXYYYYYYXX.XXXX..XXXYYYYYYYX...XXX.XRRRRRRRRXXRRX.',
295 '.XYYYYYYYYX..........XYYYYYYYYX....XXXDXRRRRRXXDXXX..',
296 '..XYYYYYYX............XXXXXYYYX....XXXDDXXXXXDDDDX...',
297 '..XYYYYYX.................XXYYX...XDDDDDX...XXDDDX...',
298 '...XYYYX...................XXX.....XXDDX....XDDDX....',
299 '....XXX.............................XXX......XXX.....',
300 ],
301 [
302 '.....................................................',
303 '.....................................................',
304 '.................XXXX................................',
305 '................XCCCCXX..............................',
306 '...............XCCCCCCXXXX...........................',
307 '...........XXXXXCCCCCCXXCCX..........................',
308 '..........XCCCCXCCCCCCXXCCXX.........................',
309 '..........XCCCCXXCCCCCXCCCCX.........................',
310 '.........XCCCCCCCCCCCCCCCCCXXX.......................',
311 '........XCCCCCCCCCCCCCCCCCCXXXX......................',
312 '........XCCCCCCCCCCCCCCCCCCCCCX......................',
313 '.......XCCCCCCCCCCCCCCCCCCCCCCCX.....................',
314 '.....XXXCCCCXXXCCXXCCCCCCCCCCCCX.....................',
315 '....XYYXXCCXIIIXCCCCCCCCCCCCCCCX.....................',
316 '...XYYYYXCXIIIIIXCXXXXCCCCCCCCCX.....................',
317 '..XXYYYXCCXIIIIIXXIIIIXCCCCCCCXX.....................',
318 '..XYYYXXCCXIIIXIXIIIIIIXCCCCCXX......................',
319 '..XYYYXCCCXIIIIIXIXIIIIXCCCCXXXX.....................',
320 '..XYYYYXCCCXIIIXXIIIIIIXCCCXYXXXX....................',
321 '..XYYYYXXCCCXXXCXXIIIIXXCCXXYYYYX....XXXX............',
322 '...XYYYYXCCCCCCCCXXXXXXCCXXYYYYYYX..XRXXRX...........',
323 '....XYYXCCCCCCCCCCCCCCCCCCCXXXYYYX.XRRRRRXXXXXX......',
324 '.....XXCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRRXXRRRXX....',
325 '......XCCCCCCCCCCCCCCCCCCCCXYYYYYXDRRRRRRRRRRRRRX....',
326 '......XCCCCCCCCCXCCCCCCCCCCXYYYYYXRRRRRRRXXRRRRRR....',
327 '.......XXCCCXXXXXCCCCCCCCCCCXYYYXRRRRRXRRXXXRRRRR....',
328 '.......XCCCXYYYYXXCCCCCCCCCCXXXX.XRRRRXXRXXRRRRRR....',
329 '........XCXYYYYYYCCCCCCCCCCCX....XRRRPRXXXRRRRRRR....',
330 '.........XYYYYYYYCCCCCCCXCCX....XXRRRRRRRRRRRRRRX....',
331 '.....XXX.XYYYYYYXXXCCCCCXXX....XXRXRRRRRRRRRRRRRX....',
332 '....XXXXXYYYYYYXCCXXCCCXYYYX...XXRXRRRRRRRRRRRXX.....',
333 '....XYYYYYYYYYXXCXXXXXXYYYYYX...XRXXRRRRRRRRXXRRX....',
334 '....XYYYYYYYYXXXXX....XYYYYYX....X..XRRRRRRRXXXXX....',
335 '.....XYYYYYYX..........XXYYYX.....XXDXRRRRRXDDX......',
336 '......XXYYYXX...........XYYYX...XXDDDDXXXXXXDDX......',
337 '.......XYYYX.............XXX.....XDDDXX...XXDDX......',
338 '........XXX.......................XDDX....XDDX.......',
339 '...................................XX......XX........',
340 ],
341 [
342 '.....................................................',
343 '.....................................................',
344 '.....................................................',
345 '................XXXX.................................',
346 '...............XCCCCXX...............................',
347 '..............XCCCCCCXXXX............................',
348 '...........XXXXCCCCCCXXCCX...........................',
349 '.........XXCCCXCCCCCCXXCCXX..........................',
350 '.........XXCCCXCCCCCCXCCCCX..........................',
351 '........XXCCCCCCCCCCCCCCCCXX.........................',
352 '.......XCCCCCCCCCCCCCCCCCCCCX........................',
353 '.......XCCCCCCCCCCCCCCCCCCCCCX.......................',
354 '....XXXXCCCCCCCXCCCCCCCCCCCCCX.......................',
355 '...XYYXCCCCXXXCXXXCCCCCCCCCCCX.......................',
356 '..XYYYYXXCXIIIXCCXXXXCCCCCCCCX.......................',
357 '.XYYYYXCCXIIIIIXXIIIIXCCCCCXX........................',
358 '.XYYYXCCCXIIIXIXIIIIIIXCCCCCX........................',
359 '.XYYYXCCCXIIIIIXIXIIIIXCCCCCX........................',
360 '.XYYYYXCCCXIIIXXIIIIIIXCCCCCXX.......................',
361 '.XYYYYYXCCCXXXCXIIIIIIXCCCXXYYX......................',
362 '..XYYYYXXCCCCCCCXIIIIXCCCCXXYYX.....XXXX.............',
363 '...XYYXCCCCCCCCCCXXXXCCCXXYYYYYX...XXXXXX............',
364 '....XXCCCCCCCCCCCCCCCCCCXCXXXYYYXXXXRRRRXXXXXX.......',
365 '.....XCCCCCCCCCCCCCCCCCCCCCCXYYYXXXRRRRRRXXRRXX......',
366 '.....XCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRRRRRRRRX.....',
367 '......XXCCCCCCXCCCCCCCCCCCCXYYYYXRRRRRRRXRRRRRRX.....',
368 '......XCCCCCXXXCCCCCCCCCCCCXYYYXRRRRRXRRXXXRRRRR.....',
369 '.......XCCXXYYXXCCCCCCCCCCCCXXX.XRRRRXRRXXXRRRRR.....',
370 '.......XCXYYYYDCCCCCCCCCCCCCX...XRRRPRXXXRRRRRRR.....',
371 '........XXYYYYYCCXXCCCCCXCCX...XXXRRRRRXRRRRRRRX.....',
372 '........XYYYYYYXXCCCCCCCXXX...XXRXRRRRRRRRRRRRXX.....',
373 '.....XXXXYYYYYXCCXCCCCCXYYX...XXRXXRRRRRRRRXRRXX.....',
374 '.....XYYYYYYYXXXX.XXXXXYYYYX...XXX.XRRRRRRRXXRXX.....',
375 '......XYYYYYYX...XXYYYYYYYYX.....XXDXRRRRRXDDXX......',
376 '......XYYYYYXX....XXYYYYYYYX....XDDDDXXXXXDDDX.......',
377 '.......XYYYYX.......XXXXXYYX....XDDDDX...XXDDX.......',
378 '........XXXX............XYYX.....XDDX....XDDX........',
379 '.........................XX.......XX......XX.........',
380 ],
381 [
382 '.................XXX.................................',
383 '................XCCCXX...............................',
384 '...............XCCCCCXXXX............................',
385 '............XXXXCCCCCXXCCX...........................',
386 '...........XCCCXCCCCCXXCCCX..........................',
387 '..........XCCCCXXCCCCXCCCCX..........................',
388 '.........XXCCCCCCCCCCCCCCCXX.........................',
389 '.........XCCCCCCCCCCCCCCCCXCX........................',
390 '.....XX..XCCCCCCCCCCCCCCCCCCCX.......................',
391 '....XYYXXXCCCCCCXCXCCCCCCCCCCX.......................',
392 '...XYYYXXCCCXXXCXXXCCCCCCCCCCX.......................',
393 '..XYYYYXXXCXIIIXCCXXXCCCCCCCCX.......................',
394 '..XYYYXXCCXIIIIIXXIIIXCCCCCXX........................',
395 '..XYYYXCCCXIIIXIXIIIIIXCCCCCX........................',
396 '..XYYYXCCCXIIIIIXIIIIIXCCCCCX........................',
397 '..XYYYYXCCCXIIIXXIIIIIXCCCCCXXX......................',
398 '...XYYYYXCCCXXXCXIIIIIXCCCXXYYYX.....................',
399 '....XYYXCCCCCCCCCXXXXXCCXXYYYYYYX....XXXX............',
400 '.....XXCCCCCCCCCCCXXXCCCXCXXXYYYX...XRRRRXX.XXXX.....',
401 '......XCCCCCCCCCCCCCCCCCXCCCXYYYX..XRRRRRRXXXXXX.....',
402 '......XCCCCCCCCCCCCCCCCCCCCCXYYYX.XXRRRRRRXXXRRXX....',
403 '......XXCCCCCCCCCCCCCCCCCCCXYYYYX.XRRRRRRRRRRRRRRX...',
404 '.......XXCCCCCCCCCCCCCCCCCCXYYYYXRRRRRRRRXXXRRRRRX...',
405 '.......XXCCCXXXXXCCCCCCCCCCCXYYX.XRRRRXRRRXXXRRRRX...',
406 '........XCCXYYYYXCCCCCCCCCCCXXX..XRRRRXXRXXXRRRRRX...',
407 '........XCXYYYYYXCCCCCCCCCCCX....XRRRPRXXXXRRRRRRX...',
408 '.........XXYYYYYXCXCCCCCXCCX....XXRRRRRRXXRRRRRRRR...',
409 '..........XYYYYYXXXCCCCCXCCX...XXRXRRRRRRRRRRRRRRX...',
410 '..........XYYYYYXXCCCCCCXXXX...XRRXRRRRRRRRRRRRXXX...',
411 '.......XXXYYYYYXCCXCCCCXYYYXX..XRRRXRRRRRRRRRRRXX....',
412 '.......XXXYYYYYXCCXCXXXXYYYYX...XRXXRRRRRRRRRXXRRX...',
413 '......XYYXYYYYXDXX.XXXXYYYYYX...XXX.XRRRRRRRRXXRRX...',
414 '......XYYYYYYYX....XXYYYYXYYX.....XXDXRRRRRRXDXXX....',
415 '......XYYYYYYX......XXXXXXYYX...XXDDDDXXXXXXDDDX.....',
416 '.......XYYYYY.............XX....XXDDDDX....XXDDX.....',
417 '........XYYXX.....................XDDX.....XDDXX.....',
418 '........XXXX.......................XX......XDDX......',
419 '.........XX.................................XX.......',
420 ],
421 [
422 '.....................................................',
423 '.....................................................',
424 '................XXXX.................................',
425 '...............XCCCCXX...............................',
426 '..............XCCCCCCCXXX............................',
427 '...........XXXXCCCCCCXXCCX...........................',
428 '..........XCCCXXCCCCCXCCCCX..........................',
429 '.........XCCCCXXCCCCCXCCCCXX.........................',
430 '........XCCCCCCCCCCCCCCCCCXXX........................',
431 '.......XCCCCCCCCCCCCCCCCCCCCX........................',
432 '.......XCCCCCCCCCCCCCCCCCCCCCX.......................',
433 '.....XXXCCCCCCCCCCCCCCCCCCCCCX.......................',
434 '....XYXCCCCXXXCCXXCCCCCCCCCCCX.......................',
435 '...XYYYXXCXIIIXCCXXXXCCCCCCCCX.......................',
436 '..XYYYYXCXIIIIIXXIIIIXCCCCCXX........................',
437 '..XYYYXCCXIIIXIXIIIIIIXCCCCCCX.......................',
438 '..XYYYXCCXIIIIIXIXIIIIXCCCCCXXXX.....................',
439 '..XYYYYXCCXIIIXXIIIIIIXCCCXXXXXXX....................',
440 '..XYYYYXXCCXXXCCXIIIIXCCCCXXYYYYX....................',
441 '...XYYXCCCCCCCCCXXXXXXCCCXYYYYYYYX..XXXX.............',
442 '....XYXCCCCCCCCCCXXXXCCCXXXXXYYYYX.XRRRRX..XX........',
443 '....XXCCCCCCCCCCCCCCCCCCXCCXXYYYYXXXRRRRXXXXXX.......',
444 '.....XCCCCCCCCCCCCCCCCCCCCCCXYYYYXXRRRRRRXXRRXX......',
445 '.....XCCCCCCCCCCCCCCCCCCCCCXYYYYYXRRRRRRRRRRRRRX.....',
446 '......XXCCCCCCXXCCCCCCCCCCCXYYYYXRRRRRRRXXRRRRRRX....',
447 '......XXCCCXXXXXCCCCCCCCCCCCXYYXDRRRRXRRXXXRRRRRX....',
448 '.......XXCXYYYXXCCCCCCCCCCCCXXX.XRRRRXXRRXRRRRRRX....',
449 '.......XXXYYYYYXCCCCCCCCCCCCX...XRRRPRXXXRRRRRRRX....',
450 '........XXYYYYYXCXXCCCCCXCCX...XXRRRRRRXXRRRRRRXX....',
451 '........XYYYYYYXXXCCCCCCXXX...XXRXRRRRRRRRRRRRXX.....',
452 '........XYYYYXXCCXXCCCCXYYYX..XXRRXRRRRRRRRRRXX......',
453 '........XYYYYYYXX.XXXXXYYYYX...XXXXXRRRRRRRXXRRX.....',
454 '.........XYYYYYYX..XXYYYYYYX.......XRRRRRRRXXDX......',
455 '.........XXYYYYYX...XXXXXYYX.....XXDXRRRRRXDDXX......',
456 '..........XXYYYYX....XXXXYYX....XDDDDXXXXXDDDX.......',
457 '..........XYYYYYX........XX.....XDDDDX...XXDDX.......',
458 '..........XYYYYX.................XDDX....XDDX........',
459 '...........XXXX...................XX......XX.........',
460 ],
461 [
462 '.....................................................',
463 '..................XXX................................',
464 '.................XCCCX...............................',
465 '.............XXXXCCCCCXXXXX..........................',
466 '............XXXXXCCCCCXXCCCX.........................',
467 '..........XXCCCXXCCCCCXXCCCCX........................',
468 '..........XXCCCCXXCCCCXCCCCCX........................',
469 '.........XCCCCCCCCCCCCCCCCCCXXX......................',
470 '........XCCCCCCCCCCCCCCCCCCCCCCX.....................',
471 '.....XX.XCCCCCCCCXCCCCCCCCCCCCCCX....................',
472 '....XYYXCCCCXXXXCXCCCCCCCCCCCCCCX....................',
473 '...XYYYXXCCXIIIIXXCCCCCCCCCCCCCCX....................',
474 '..XYYYYYXXXIIIIIIXXXXXCCCCCCCCCCX....................',
475 '..XYYYYXCCXIIIIIIXXIIIXCCCCCCCCCX....................',
476 '..XYYYXCCCXIIIIXIXIIIIIXCCCCCCXX.....................',
477 '..XYYYXCCCXIIIIIIXIXIIIXCCCCCCCX.....................',
478 '..XYYYYXCCCXIIIIXXIIIIIXCCCCCCXXXX...................',
479 '..XXYYYYXCCCXXXXCXIIIIIXCCCCXXYYYYX..................',
480 '...XYYYYXCCCCCCCCCXIIIXCCCXCXYYYYYYX.................',
481 '....XYYXCCCCCCCCCCCXXXCCCCXXXXYYYYYX.................',
482 '.....XXCCCCCCCCCCCCCCCCCCXXCXXXXYYYYX................',
483 '......XCCCCCCCCCCCCCCCCCCCCCCCXXYYYYX................',
484 '......XCCCCCCCCCCCCCCCCCCCCCCCXYYYYYX................',
485 '.......XXCCCCCCCCCCCCCCCCCCCCXYYYYYYX................',
486 '.......XCCCCCCCCCCCCCCCCCCCCCXYYYYYX.................',
487 '.......XCCCCXXXXXCCCCCCCCCCCCCXYYYYX.................',
488 '........XCXXYYYYYXCCCCCCCCCCCCXXXXX..................',
489 '.........XXYYYYYYXCCCCCCCXCCXX.......................',
490 '.........XYYYYYYYDCCCCCCXXCCX........................',
491 '........XYYYYYYYYXCCCCCCXXXXX........................',
492 '.....XXXYYYYYYYXXXCCCCCXXYYYYX.......................',
493 '....XXXXYYYYYYXXCCXCCCXYYYYYYXX......................',
494 '....XYYYYYYYYX..XX.XXXYYYYYYYYX......................',
495 '....XYYYYYYYX.........XXXXXYYYX......................',
496 '.....XYYYYYXX..............XYYX......................',
497 '.....XXYYYXXX...............XX.......................',
498 '......XYYYX.................X........................',
499 '.......XXX...........................................',
500 ],
501]
502