diff --git a/.sync-state b/.sync-state
index 7d2f0ad..07eb368 100644
--- a/.sync-state
+++ b/.sync-state
@@ -1 +1 @@
-2026-06-16T17:01:11.214068+10:00
\ No newline at end of file
+2026-06-17T17:01:07.811986+10:00
\ No newline at end of file
diff --git a/links-posts/2776-2026-06-17-technology.md b/links-posts/2776-2026-06-17-technology.md
new file mode 100644
index 0000000..34d6959
--- /dev/null
+++ b/links-posts/2776-2026-06-17-technology.md
@@ -0,0 +1,26 @@
+---
+title: "2026-06-17-technology"
+created: 2026-06-17
+updated: 2026-06-17
+type: summary
+tags: [newsletter, technology]
+external: http://go/ui/posts/2776/
+---
+
+** Tech news**
+- [Nvidia 计划首次 AI 热潮后发行至少 200 亿美元债券](https://www.cnbc.com/2026/06/15/nvidia-plans-to-raise-about-20-billion-first-debt-sale-in-ai-boom.html)
+自2021年以来首次大规模债务融资,芯片巨头市值已远非昔日可比。
+- [Anthropic 与特朗普政府协商恢复强大 AI 模型访问权限](https://www.wsj.com/tech/ai/anthropic-trump-officials-seek-deal-on-restoring-powerful-model-access-d9c4ffee?mod=rss_Technology)
+双方正快速推进解决,这是 Anthropic 撤回“Fable”和“Mythos”模型后的首次公开表态。
+- [高通 CEO 称 AI 代理将取代应用程序,正研发 40 款新 AI 设备](https://www.cnbc.com/2026/06/16/qualcomm-ceo-ai-devices-agents.html)
+高通看好智能眼镜等设备,其潜力或堪比智能手机;同时被视作稀有的 AI 芯片价值股。
+- [DeepSeek 融资超 74 亿美元,成中国最具价值 AI 初创](https://www.wsj.com/tech/ai/deepseek-becomes-chinas-most-valuable-ai-startup-after-over-7-4-billion-fundraise-78ef64c0?mod=rss_Technology)
+DeepSeek 首轮融资规模巨大,支持其高昂的 AI 研发成本。
+- [SpaceX 以 600 亿美元收购 AI 编程初创公司 Cursor](https://www.cnbc.com/2026/06/16/spacex-spcx-cursor-acquisition-ipo.html)
+交易旨在增强其与 Anthropic、OpenAI 等对手在 AI 编程工具领域的竞争力,距其历史性 IPO 仅数日。
+- [SpaceX 二把手 Gwynne Shotwell 如何助其成为 IPO 巨头](https://www.cnbc.com/2026/06/16/gwynne-shotwell-spacex-elon-musk-ipo.html)
+CNBC 专访多位与 SpaceX 总裁兼 COO 共事过的人士,揭示其关键作用。
+- [Mobileye 计划 2027 年在美国推出机器人出租车服务](https://www.cnbc.com/2026/06/16/self-driving-tech-supplier-mobileye-targets-us-robotaxi-launch-in-2027.html)
+自动驾驶技术供应商 Mobileye 宣布其自动驾驶出租车服务上线时间表。
+- [强生 CEO 称借助 AI 攻克特定癌症是现实目标](https://www.wsj.com/health/healthcare/johnson-johnson-ceo-says-cure-for-certain-cancers-is-realistic-goal-234d2767?mod=rss_Technology)
+AI 正加速药物研发,但其对利润的具体提升程度尚待观察。
\ No newline at end of file
diff --git a/links-posts/2777-2026-06-17-victoria.md b/links-posts/2777-2026-06-17-victoria.md
new file mode 100644
index 0000000..f8f7703
--- /dev/null
+++ b/links-posts/2777-2026-06-17-victoria.md
@@ -0,0 +1,26 @@
+---
+title: "2026-06-17-victoria"
+created: 2026-06-17
+updated: 2026-06-17
+type: summary
+tags: [local_news, newsletter]
+external: http://go/ui/posts/2777/
+---
+
+# 焦点新闻摘要
+- [澳洲联储暂停加息,但未排除未来继续上调;楼市价格下跌引关注](https://www.sbs.com.au/news/live-blog/rba-june-2026-interest-rates-decision-live/ntpgs9xc6)
+澳洲储备银行(RBA)今年首次暂停加息,但表示通胀仍然过高。同时,市场正密切关注房价走势,部分人士期待在市场底部购入,引发了关于房价未来跌幅的讨论。
+- [美国军方计划在澳洲境内建立大型战备物资储备库](https://www.sbs.com.au/news/article/us-military-plans-major-war-stockpile-on-australian-soil/rbg8qyb0q)
+作为应对中国崛起的一项亚洲地区防御战略,美国军方计划在澳大利亚建立大规模武器储备。
+- [墨尔本市中心一高层托儿所儿童面临12层高坠落风险,险酿灾难](https://www.theage.com.au/national/victoria/could-have-been-catastrophic-children-at-risk-of-12-storey-plunge-from-high-rise-childcare-centre-20260616-p60781.html?ref=rss&utm_medium=rss&utm_source=rss_national_victoria)
+墨尔本市中心一间高层托儿中心的儿童曾面临从12层屋顶边缘坠落的危险,情况“可能造成灾难性后果”。
+- [致命“自由分娩”案:助产师为自己的行为辩护,称未考虑呼叫救护车](https://www.theage.com.au/national/victoria/no-way-i-was-going-to-call-an-ambulance-doula-at-fatal-free-birth-defends-actions-20260616-p6073x.html?ref=rss&utm_medium=rss&utm_source=rss_national_victoria)
+维州一名助产师在死因裁判官法庭上表示,她起初认为临终的孕妇Stacey Warnecke只是恐慌发作,从未想过呼叫救护车。
+- [被判46年徒刑的“掠夺性”教师,此前已被揭露对学生进行性侵](https://www.theage.com.au/national/victoria/predatory-teacher-serving-46-years-for-sadistic-abuse-also-assaulted-his-students-20260616-p607e5.html?ref=rss&utm_medium=rss&utm_source=rss_national_victoria)
+这名前教师因儿童性虐待案已被判处澳洲最长刑期,如今更多针对其学生的袭击事件被揭露。
+- [澳洲记录上“最致命的一年”揭示隐藏危机正在恶化](https://www.sbs.com.au/news/article/australias-deadliest-year-on-record- exposes-a-worsening-hidden-crisis/c4gnrk0xd)
+最新数据显示,澳大利亚经历了一个悲剧性的里程碑,预示着一个日益恶化的隐藏危机。
+- [警方相信Dezi Freeman已前往新州,更多逮捕行动正在进行](https://www.theage.com.au/national/victoria/police-believe-dezi-freeman-travelled-to-nsw-more-arrests-made-20260616-p607c5.html?ref=rss&utm_medium=rss&utm_source=rss_national_victoria)
+在涉嫌杀害警员的凶手Dezi Freeman在边境枪战中身亡近三个月后,警方仍在深入调查其216天逃亡期间的行踪,并逮捕了更多涉案人员。
+- [世界杯裁判肖恩·埃文斯被澄清“白人至上”手势指控](https://www.sbs.com.au/news/article/shaun-evans-world-cup-referee-white-power-gesture-cleared/hdrhammtw)
+国际足联(FIFA)在肖恩·埃文斯否认故意做出此手势后,将其澄清,埃文斯称那只是“不自主的抽搐”。
\ No newline at end of file
diff --git a/links-posts/2778-glm-52-pacman.md b/links-posts/2778-glm-52-pacman.md
new file mode 100644
index 0000000..888be1c
--- /dev/null
+++ b/links-posts/2778-glm-52-pacman.md
@@ -0,0 +1,589 @@
+---
+title: "GLM 5.2 pacman"
+created: 2026-06-17
+updated: 2026-06-17
+type: summary
+tags: [AI]
+external: http://go/ui/posts/2778/
+---
+
+Here's the complete `pacman.py` implementation:
+
+```python
+#!/usr/bin/env python3
+"""Pac-Man — Terminal Edition. Run: python3 pacman.py"""
+
+import curses
+import time
+import random
+
+# ─── Maze ───────────────────────────────────────────────────────────
+# 31 rows × 28 cols. '#' wall, '.' dot, 'o' power pellet,
+# ' ' empty, 'G' ghost-house door.
+MAZE_TEMPLATE = [
+ "############################",
+ "#............##............#",
+ "#.####.#####.##.#####.####.#",
+ "#o####.#####.##.#####.####o#",
+ "#.####.#####.##.#####.####.#",
+ "#..........................#",
+ "#.####.##.########.##.####.#",
+ "#.####.##.########.##.####.#",
+ "#......##....##....##......#",
+ "######.##### ## #####.######",
+ " #.##### ## #####.# ",
+ " #.## ##.# ",
+ " #.## ###--### ##.# ",
+ "######.## # # ##.######",
+ " # # ",
+ "######.## # # ##.######",
+ " #.## ######## ##.# ",
+ " #.## ##.# ",
+ " #.## ######## ##.# ",
+ "######.## ######## ##.######",
+ "#............##............#",
+ "#.####.#####.##.#####.####.#",
+ "#.####.#####.##.#####.####.#",
+ "#o..##.......##.......##..o#",
+ "###.##.##.########.##.##.###",
+ "###.##.##.########.##.##.###",
+ "#......##....##....##......#",
+ "#.##########.##.##########.#",
+ "#.##########.##.##########.#",
+ "#..........................#",
+ "############################",
+]
+
+ROWS = 31
+COLS = 28
+
+# Ghost-house door position (row 12, col 14 → the two '-' cells become 'G')
+# We'll patch the template after copying.
+
+# Directions: (dr, dc)
+UP = (-1, 0)
+DOWN = (1, 0)
+LEFT = (0, -1)
+RIGHT = (0, 1)
+DIRS = [UP, DOWN, LEFT, RIGHT]
+OPPOSITE = {UP: DOWN, DOWN: UP, LEFT: RIGHT, RIGHT: UP}
+
+# ─── Ghost ──────────────────────────────────────────────────────────
+class Ghost:
+ COLORS = {0: 1, 1: 2, 2: 3, 3: 4} # Blinky, Pinky, Inky, Clyde
+ GLYPH = 'ᗣ'
+ EATEN = '◌'
+
+ def __init__(self, idx, home_r, home_c, start_r, start_c):
+ self.idx = idx
+ self.color = self.COLORS[idx]
+ self.home_r = home_r
+ self.home_c = home_c
+ self.start_r = start_r
+ self.start_c = start_c
+ self.reset()
+
+ def reset(self):
+ self.r = self.start_r
+ self.c = self.start_c
+ self.dir = UP
+ self.frightened = False
+ self.fright_timer = 0.0
+ self.eaten = False
+ self.spawn_delay = float(self.idx * 2) # 0,2,4,6 s
+
+ # ── helpers ────────────────────────────────────────────────────
+ @staticmethod
+ def _walkable(maze, r, c):
+ """Ghost-walkable: not a wall."""
+ return maze[r][c] != '#'
+
+ def _blink(self):
+ """True when frightened & should show white (last 2 s)."""
+ return (self.frightened
+ and self.fright_timer < 2.0
+ and int(self.fright_timer * 3) % 2 == 0)
+
+ # ── movement ───────────────────────────────────────────────────
+ def move(self, maze, pac_r, pac_c, dt):
+ if self.spawn_delay > 0:
+ self.spawn_delay -= dt
+ return
+
+ # Frightened timer
+ if self.frightened:
+ self.fright_timer -= dt
+ if self.fright_timer <= 0:
+ self.frightened = False
+
+ # Target cell
+ if self.eaten:
+ tr, tc = self.home_r, self.home_c
+ # arrived home?
+ if self.r == self.home_r and self.c == self.home_c:
+ self.eaten = False
+ self.frightened = False
+ self.fright_timer = 0
+ elif self.frightened:
+ tr, tc = random.randint(0, ROWS-1), random.randint(0, COLS-1)
+ else:
+ offsets = [(0, 0), (2, 2), (-2, 2), (2, -2)]
+ dr, dc = offsets[self.idx]
+ tr, tc = pac_r + dr, pac_c + dc
+
+ # Candidate directions (no reverse), shuffled for tie-break
+ cand = [d for d in DIRS if d != OPPOSITE.get(self.dir)]
+ random.shuffle(cand)
+
+ best, best_d = None, None
+ for d in cand:
+ nr, nc = self.r + d[0], (self.c + d[1]) % COLS
+ if not self._walkable(maze, nr, nc):
+ continue
+ dist = (nr - tr) ** 2 + (nc - tc) ** 2
+ if best_d is None or dist < best_d:
+ best_d = dist
+ best = d
+
+ if best is None: # dead end → allow reverse
+ for d in DIRS:
+ nr, nc = self.r + d[0], (self.c + d[1]) % COLS
+ if self._walkable(maze, nr, nc):
+ best = d
+ break
+
+ if best is not None:
+ self.r += best[0]
+ self.c = (self.c + best[1]) % COLS
+ self.dir = best
+
+
+# ─── PacmanGame ─────────────────────────────────────────────────────
+class PacmanGame:
+ PAC_INTERVAL = 0.14
+ GHOST_INTERVAL = 0.22
+ BLINK_INTERVAL = 0.35
+ FRIGHT_TIME = 7.0
+
+ PAC_ARROWS = {UP: 'ᐱ', DOWN: 'ᐯ', LEFT: 'ᐸ', RIGHT: 'ᐳ'}
+
+ def __init__(self, stdscr):
+ self.stdscr = stdscr
+ self._setup_colors()
+ self.score = 0
+ self.lives = 3
+ self.level = 1
+ self.paused = False
+ self.running = True
+
+ # ── colour setup ───────────────────────────────────────────────
+ def _setup_colors(self):
+ curses.use_default_colors()
+ curses.init_pair(1, curses.COLOR_RED, -1)
+ curses.init_pair(2, curses.COLOR_MAGENTA, -1)
+ curses.init_pair(3, curses.COLOR_CYAN, -1)
+ curses.init_pair(4, curses.COLOR_YELLOW, -1)
+ curses.init_pair(5, curses.COLOR_YELLOW, -1)
+ curses.init_pair(6, curses.COLOR_BLUE, -1)
+ curses.init_pair(7, curses.COLOR_WHITE, -1)
+ curses.init_pair(8, curses.COLOR_GREEN, -1)
+
+ # ── game init ──────────────────────────────────────────────────
+ def _init_game(self):
+ self.maze = [list(row) for row in MAZE_TEMPLATE]
+
+ # Mark ghost-house door cells (row 12, cols 13-14) as 'G'
+ for c in (13, 14):
+ if 0 <= 12 < ROWS and 0 <= c < COLS:
+ self.maze[12][c] = 'G'
+
+ self.score = 0
+ self.lives = 3
+ self.level = 1
+ self.dots_eaten = 0
+ self.total_dots = sum(
+ 1 for r in range(ROWS) for c in range(COLS)
+ if self.maze[r][c] in ('.', 'o')
+ )
+
+ # Pac-Man
+ self.pac_r = 23
+ self.pac_c = 14
+ self.pac_dir = LEFT
+ self.next_dir = LEFT
+
+ # Ghosts — all start inside the house
+ self.ghosts = [
+ Ghost(0, 14, 13, 14, 13), # Blinky (door cell-ish)
+ Ghost(1, 14, 13, 14, 12), # Pinky
+ Ghost(2, 14, 14, 15, 12), # Inky
+ Ghost(3, 14, 14, 15, 14), # Clyde
+ ]
+
+ self.last_pac = time.time()
+ self.last_ghost = time.time()
+ self.last_blink = time.time()
+ self.blink_on = True
+ self.pac_mouth = True
+
+ # ── public entry ───────────────────────────────────────────────
+ def run(self):
+ self._show_splash()
+ while self.running:
+ self._init_game()
+ self._play()
+ if not self._game_over_screen():
+ break
+
+ # ── splash ─────────────────────────────────────────────────────
+ def _show_splash(self):
+ s = self.stdscr
+ s.nodelay(False)
+ s.clear()
+ sh, sw = s.getmaxyx()
+
+ title_lines = [
+ "██████╗ █████╗ ██████╗ █████╗ ██╗ ██╗",
+ "██╔══██╗██╔══██╗██╔══██╗██╔══██╗██║ ██║",
+ "██████╔╝███████║██████╔╝███████║██║ ██║",
+ "██╔═══╝ ██╔══██║██╔══██╗██╔══██║██║ ██║",
+ "██║ ██║ ██║██████╔╝██║ ██║╚██████╔╝",
+ "╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ",
+ ]
+ top = max(0, (sh - 12) // 2)
+ for i, line in enumerate(title_lines):
+ col = max(0, (sw - len(line)) // 2)
+ try:
+ s.addstr(top + i, col, line, curses.color_pair(5) | curses.A_BOLD)
+ except curses.error:
+ pass
+
+ info_top = top + len(title_lines) + 2
+ ghosts_line = (
+ "Blinky ᗣ Pinky ᗣ Inky ᗣ Clyde ᗣ"
+ )
+ ctrl_line = "Arrow keys / WASD: move P: pause Q: quit"
+ start_line = "Press any key to start!"
+
+ for i, (line, pair) in enumerate([
+ (ghosts_line, 7),
+ (ctrl_line, 7),
+ (start_line, 8),
+ ]):
+ col = max(0, (sw - len(line)) // 2)
+ try:
+ s.addstr(info_top + i, col, line,
+ curses.color_pair(pair) | curses.A_BOLD)
+ except curses.error:
+ pass
+
+ s.refresh()
+ s.getch()
+
+ # ── main play loop ─────────────────────────────────────────────
+ def _play(self):
+ s = self.stdscr
+ s.nodelay(True)
+ curses.curs_set(0)
+ now = time.time()
+ self.last_pac = now
+ self.last_ghost = now
+ self.last_blink = now
+
+ playing = True
+ while playing:
+ now = time.time()
+ dt = now - self._last_frame if hasattr(self, '_last_frame') else 0.016
+ self._last_frame = now
+
+ if self._handle_input() == 'QUIT':
+ self.running = False
+ return
+
+ if not self.paused:
+ # Pac-Man
+ if now - self.last_pac >= self.PAC_INTERVAL:
+ self._move_pacman()
+ self.last_pac = now
+ self.pac_mouth = not self.pac_mouth
+
+ # Ghosts
+ if now - self.last_ghost >= self.GHOST_INTERVAL:
+ for g in self.ghosts:
+ g.move(self.maze, self.pac_r, self.pac_c,
+ self.GHOST_INTERVAL)
+ self.last_ghost = now
+
+ # Blink timer
+ if now - self.last_blink >= self.BLINK_INTERVAL:
+ self.blink_on = not self.blink_on
+ self.last_blink = now
+
+ result = self._check_collisions()
+ if result == 'LOSE':
+ playing = False
+ continue
+ if self._check_win():
+ playing = False
+ self.won = True
+ continue
+
+ self._draw()
+ time.sleep(0.02)
+
+ # ── input ──────────────────────────────────────────────────────
+ def _handle_input(self):
+ s = self.stdscr
+ while True:
+ ch = s.getch()
+ if ch == -1:
+ break
+ if ch in (curses.KEY_UP, ord('w'), ord('W')):
+ self.next_dir = UP
+ elif ch in (curses.KEY_DOWN, ord('s'), ord('S')):
+ self.next_dir = DOWN
+ elif ch in (curses.KEY_LEFT, ord('a'), ord('A')):
+ self.next_dir = LEFT
+ elif ch in (curses.KEY_RIGHT, ord('d'), ord('D')):
+ self.next_dir = RIGHT
+ elif ch in (ord('p'), ord('P')):
+ self.paused = not self.paused
+ elif ch in (ord('q'), ord('Q')):
+ return 'QUIT'
+ return None
+
+ # ── Pac-Man movement ───────────────────────────────────────────
+ def _move_pacman(self):
+ maze = self.maze
+
+ def try_dir(d):
+ nr = self.pac_r + d[0]
+ nc = (self.pac_c + d[1]) % COLS
+ cell = maze[nr][nc]
+ if cell == '#' or cell == 'G':
+ return False
+ return True
+
+ d = self.next_dir if try_dir(self.next_dir) else (
+ self.pac_dir if try_dir(self.pac_dir) else None
+ )
+ if d is None:
+ return
+
+ self.pac_r += d[0]
+ self.pac_c = (self.pac_c + d[1]) % COLS
+ self.pac_dir = d
+
+ cell = self.maze[self.pac_r][self.pac_c]
+ if cell == '.':
+ self.maze[self.pac_r][self.pac_c] = ' '
+ self.score += 10
+ self.dots_eaten += 1
+ elif cell == 'o':
+ self.maze[self.pac_r][self.pac_c] = ' '
+ self.score += 50
+ self.dots_eaten += 1
+ for g in self.ghosts:
+ if not g.eaten:
+ g.frightened = True
+ g.fright_timer = self.FRIGHT_TIME
+
+ # ── collisions ─────────────────────────────────────────────────
+ def _check_collisions(self):
+ for g in self.ghosts:
+ if g.r == self.pac_r and g.c == self.pac_c:
+ if g.eaten:
+ continue
+ if g.frightened:
+ g.eaten = True
+ g.frightened = False
+ self.score += 200
+ else:
+ self._lose_life()
+ return 'LOSE'
+ return None
+
+ def _lose_life(self):
+ self.lives -= 1
+ if self.lives <= 0:
+ self.won = False
+ return
+ # reset positions
+ self.pac_r, self.pac_c = 23, 14
+ self.pac_dir = LEFT
+ self.next_dir = LEFT
+ for g in self.ghosts:
+ g.reset()
+ self._draw()
+ time.sleep(1.0)
+
+ def _check_win(self):
+ if self.dots_eaten >= self.total_dots:
+ self.won = True
+ return True
+ return False
+
+ # ── rendering ──────────────────────────────────────────────────
+ def _addch(self, r, c, ch, attr=0):
+ try:
+ self.stdscr.addch(r, c, ch, attr)
+ except curses.error:
+ pass
+
+ def _draw(self):
+ s = self.stdscr
+ sh, sw = s.getmaxyx()
+ offset_r = max(0, (sh - ROWS - 3) // 2)
+ offset_c = max(0, (sw - COLS) // 2)
+
+ s.clear()
+
+ # maze
+ for r in range(ROWS):
+ for c in range(COLS):
+ cell = self.maze[r][c]
+ R, C = offset_r + r, offset_c + c
+ if cell == '#':
+ self._addch(R, C, '█', curses.color_pair(7) | curses.A_DIM)
+ elif cell == '.':
+ self._addch(R, C, '·', curses.color_pair(7))
+ elif cell == 'o':
+ self._addch(R, C, '●', curses.color_pair(7) | curses.A_BOLD)
+ elif cell == 'G':
+ self._addch(R, C, '─', curses.color_pair(7) | curses.A_DIM)
+ # ' ' → nothing
+
+ # ghosts
+ for g in self.ghosts:
+ R, C = offset_r + g.r, offset_c + g.c
+ if g.eaten:
+ self._addch(R, C, Ghost.EATEN, curses.color_pair(7) | curses.A_BOLD)
+ elif g.frightened:
+ if g._blink():
+ attr = curses.color_pair(7) | curses.A_BOLD
+ else:
+ attr = curses.color_pair(6) | curses.A_BOLD
+ self._addch(R, C, Ghost.GLYPH, attr)
+ else:
+ self._addch(R, C, Ghost.GLYPH,
+ curses.color_pair(g.color) | curses.A_BOLD)
+
+ # Pac-Man
+ R, C = offset_r + self.pac_r, offset_c + self.pac_c
+ if self.pac_mouth:
+ glyph = self.PAC_ARROWS.get(self.pac_dir, 'O')
+ else:
+ glyph = 'O'
+ self._addch(R, C, glyph, curses.color_pair(5) | curses.A_BOLD)
+
+ # HUD
+ hud_r = offset_r + ROWS + 1
+ lives_str = ' '.join(['●'] * max(0, self.lives))
+ hud = f"SCORE: {self.score:06d} LIVES: {lives_str:<5} LEVEL: {self.level}"
+ if self.paused:
+ hud += " [PAUSED]"
+ hud_c = max(0, (sw - len(hud)) // 2)
+ try:
+ s.addstr(hud_r, hud_c, hud, curses.color_pair(8) | curses.A_BOLD)
+ except curses.error:
+ pass
+
+ s.refresh()
+
+ # ── game-over screen ───────────────────────────────────────────
+ def _center_msg(self, r, msg, attr=0):
+ s = self.stdscr
+ sh, sw = s.getmaxyx()
+ c = max(0, (sw - len(msg)) // 2)
+ try:
+ s.addstr(r, c, msg, attr)
+ except curses.error:
+ pass
+
+ def _game_over_screen(self):
+ """Return True to play again, False to exit."""
+ s = self.stdscr
+ s.nodelay(False)
+ s.clear()
+ sh, sw = s.getmaxyx()
+ mid = sh // 2
+
+ if getattr(self, 'won', False):
+ self._center_msg(mid - 2, "YOU WIN! 🎉",
+ curses.color_pair(8) | curses.A_BOLD)
+ else:
+ self._center_msg(mid - 2, "GAME OVER",
+ curses.color_pair(1) | curses.A_BOLD)
+
+ self._center_msg(mid, f"Final Score: {self.score}",
+ curses.color_pair(7) | curses.A_BOLD)
+ self._center_msg(mid + 2, "Play again? (Y / N)",
+ curses.color_pair(7))
+
+ s.refresh()
+ while True:
+ ch = s.getch()
+ if ch in (ord('y'), ord('Y')):
+ return True
+ if ch in (ord('n'), ord('N'), ord('q'), ord('Q')):
+ return False
+
+
+# ─── entry point ────────────────────────────────────────────────────
+def main(stdscr):
+ game = PacmanGame(stdscr)
+ game.run()
+
+
+if __name__ == "__main__":
+ curses.wrapper(main)
+```
+
+## Quick reference
+
+| Item | Detail |
+|------|--------|
+| **Run** | `python3 pacman.py` |
+| **Move** | Arrow keys or WASD |
+| **Pause** | `P` |
+| **Quit** | `Q` |
+| **Min terminal** | 80 × 34 |
+
+## How it works
+
+
+Maze & cells
+
+`MAZE_TEMPLATE` is a 31×28 grid of strings. At `_init_game()` it's deep-copied to a list-of-lists so dots/pellets can be consumed. The two `-` cells at row 12, cols 13–14 are patched to `'G'` (ghost-house door). Row 14's left/right edges are spaces — horizontal tunnel wrap uses `col % COLS`.
+
+
+
+Pac-Man movement & cornering
+
+Both `pac_dir` (current) and `next_dir` (buffered) are kept. On each 0.14 s tick the code tries `next_dir` first; if the cell ahead is a wall or door it falls back to `pac_dir`. The mouth glyph alternates between the directional arrow and `'O'` every tick.
+
+
+
+Ghost AI
+
+Each ghost picks the non-reverse, walkable neighbour with the smallest squared-Euclidean distance to its target. Targets:
+- **Eaten** → home cell (returns to house, resets).
+- **Frightened** → random cell (scatter).
+- **Normal** → Pac-Man position + per-ghost offset `[(0,0),(2,2),(-2,2),(2,-2)]`.
+
+Directions are shuffled so ties break randomly. The door `'G'` is walkable for ghosts but not Pac-Man.
+
+
+
+Frightened & eaten states
+
+Eating a power pellet (`'o'`) sets `frightened=True` for 7 s on all non-eaten ghosts. During the last 2 s they blink white↔blue on the 0.35 s timer. Colliding with a frightened ghost awards 200 pts and marks it `eaten` (glyph `◌`, navigates home). Colliding with a normal ghost costs a life.
+
+
+
+Rendering
+
+The maze is centred via `offset_r / offset_c`. Every `addstr`/`addch` is wrapped in `try/except curses.error` so small terminals degrade gracefully. The HUD is green/bold one row below the maze. The splash screen shows a big block-letter title; the game-over screen reports win/lose and prompts Y/N.
+
+
+Save as `pacman.py` and run — enjoy! 🟡
\ No newline at end of file
diff --git a/links-posts/2779-2026-06-17-world_news.md b/links-posts/2779-2026-06-17-world_news.md
new file mode 100644
index 0000000..0902811
--- /dev/null
+++ b/links-posts/2779-2026-06-17-world_news.md
@@ -0,0 +1,26 @@
+---
+title: "2026-06-17-world_news"
+created: 2026-06-17
+updated: 2026-06-17
+type: summary
+tags: [newsletter, world_news]
+external: http://go/ui/posts/2779/
+---
+
+# 世界重大新闻速览
+
+- [印度教科书恢复“舞女”裸体雕像原图,此前遭涂黑引发争议](https://www.bbc.com/news/articles/c7vyzgl2142o)
+- [印度因考试泄题担忧临时封禁Telegram](https://www.bbc.com/news/articles/cn074j04l3eo)
+- [特朗普转向乌克兰问题:表态愿推动俄乌和谈](https://www.cnbc.com/2026/06/16/trump-zelenskyy-ukraine-war-iran-g7-summit.html)
+- [美国加州野火肆虐,已蔓延超2000英亩](https://www.bbc.com/news/videos/cn8q909jld2o)
+- [美国施压见效,古巴旅游业游客数暴跌58%](https://www.bbc.com/news/articles/ckg8zrm20jjo)
+- [SpaceX以600亿美元收购AI编程公司Cursor,助力IPO及AI竞争](https://www.cnbc.com/2026/06/16/spacex-spcx-cursor-acquisition-ipo.html)
+- [Spacex总裁肖特维尔被曝主导公司IPO进程](https://www.cnbc.com/2026/06/16/gwynne-shotwell-spacex-elon-musk-ipo.html)
+- [匈牙利议会通过法案,限制欧尔班未来八年内再任总理](https://www.bbc.com/news/articles/c621zplqrrno)
+- [索马里兰在以色列承认其独立后,于耶路撒冷开设使馆](https://www.bbc.com/news/articles/cj4gn4d54y0o)
+- [瑞典61岁男子因强迫妻子与120名男子发生性关系被判刑四年](https://www.bbc.com/news/articles/crlwxg721eeo)
+- [法国最年长女囚犯(79岁)因1995年肢解案出庭受审](https://www.bbc.com/news/articles/cy498llene1o)
+- [委内瑞拉与美国通用电气达成协议,重建国家电网](https://www.bbc.com/news/articles/c0jyzpv52yyo)
+- [伊朗官方宣扬对美谈判为胜利,民众却关注物价与战争恐惧](https://www.bbc.com/news/articles/cn4rwzxvl8ko)
+- [霍尔木兹海峡航运恢复面临三大障碍:安全、水雷与通行费](https://www.bbc.com/news/articles/cn4rw784nj2o)
+- [西雅图上空首次出现无人机组成的FIFA实时比分显示](https://www.bbc.com/news/videos/ced4y9ljye9o)
\ No newline at end of file