SLOPSHOPPER

nokia-games-th

เกมงูและ Tetris จอเขียวแบบ Nokia 3310 เล่นในแผงข้าง Claude Code ด้วยปุ่ม w a s d พร้อม High Score

newpanecommandtoasttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · nokia-games-th
│ ┃ nokia-games-th ✕ › fix the failing auth test and add an audit log call │ ┃ เริ่มเกมด้วย /game snake หรือ /game tetris │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /game │ ⎿ nokia-games-th: 📱 Nokia Games TH (Mod 13) │ ⎿ nokia-games-th: ======================================== │ ⎿ nokia-games-th: /game snake [กว้าง] [สูง] เกมงู (ค่าเริ่มต้น 2 │ ⎿ nokia-games-th: /game tetris Tetris │ ⎿ nokia-games-th: /game pause | resume | quit │ ⎿ nokia-games-th: /game scores [clear] High Score │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · nokia-games-th
เริ่มเกมด้วย /game snake หรือ /game tetris
README

Mod 13: Nokia Games TH

เกมงูและ Tetris จอสีเขียวแบบ Nokia 3310 เล่นในแผงข้าง Claude Code เกมขยับเองตามเวลา และกด w a s d บนคีย์บอร์ดเพื่อบังคับได้เลย High Score จะจำไว้ข้าม session

คำสั่ง

คำสั่งทำอะไร
/game snake [กว้าง] [สูง]เริ่มเกมงู (ค่าเริ่มต้น 20x12)
/game tetrisเริ่ม Tetris
/game pause / resume / quitหยุด / เล่นต่อ / ออก
/game scoresดู High Score (/game scores clear ล้าง)
/game demoดูหน้าจอตัวอย่างแบบข้อความ
/game up / down / left / right / dropบังคับด้วยคำสั่ง (ใช้ตอนกดปุ่มในแผงไม่ได้)

ปุ่มในแผง (กดบนคีย์บอร์ดได้ทันที):

ปุ่มเกมงูTetris
wขึ้นหมุน
aซ้ายเลื่อนซ้าย
sลงลงเร็ว
dขวาเลื่อนขวา
x-ทิ้งลงสุด
pหยุด / เล่นต่อหยุด / เล่นต่อ
rเริ่มใหม่เริ่มใหม่

กด Esc เพื่อปิดแผง (เกมจะหยุดไว้ พิมพ์ /game resume เพื่อเล่นต่อ)

ติดตั้ง

/plugin install nokia-games-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. แยก game state (ตำแหน่งงู, อาหาร, คะแนน) ออกจากการวาด
  2. ขยับงูทีละก้าว: เช็คชนขอบ, ชนตัวเอง, กินอาหาร และกันการหันกลับ 180 องศา
  3. Tetris: เช็คว่าชิ้นวางได้ไหม และหมุนชิ้น 90 องศา
  4. Tetris: ตก, วางชิ้นลงกระดาน, ลบแถวที่เต็ม และนับคะแนน
  5. Game loop ด้วย $.clock.every และเก็บ High Score ใน $.store
  6. วาดจอ Nokia ด้วย Box (สีพื้น + ขอบ) และทำปุ่มคีย์ลัดด้วย Button hotkey
  7. คำสั่ง /game และการบังคับเกมด้วยคำสั่ง

หมายเหตุ

  • ปุ่ม w a s d ใช้ได้เมื่อแผงเกมถูกโฟกัสอยู่ (แผงจะถูกโฟกัสเองตอนเริ่มเกม) ถ้ากลับไปพิมพ์ที่ช่องข้อความ ให้คลิกที่แผงหรือกด ctrl+x แล้ว tab เพื่อกลับไปที่แผง
  • เกมวาดด้วยตัวอักษร ไม่ใช่พิกเซลจริง ความเร็วเกมขึ้นกับการวาดของหน้าจอ
Source 1 files
hooks/register.tsx 433 lines
1/**
2 * ============================================================================
3 * MOD 13: Nokia Games TH - เกมงูและ Tetris จอเขียวแบบ Nokia 3310 (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: เกม Snake บนมือถือ Nokia 3310 และ Tetris
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /game snake เล่นเกมงู, /game tetris เล่น Tetris ในแผงข้าง Claude Code
9 * 2. จอสีเขียวแบบ Nokia (#9bbc0f) ตัวหนังสือเขียวเข้ม (#0f380f)
10 * 3. เกมขยับเองด้วย timer และควบคุมด้วยปุ่ม w a s d บนคีย์บอร์ด
11 * 4. เก็บ High Score ข้าม session ด้วย $.store
12 *
13 * แนวคิดสำคัญ (สอนทำเกม):
14 * - Game state = ข้อมูลล้วนๆ (ตำแหน่งงู, กระดาน) ไม่ผูกกับการวาด
15 * - Game loop = $.clock.every เรียก tick() ทุกช่วงเวลา แล้วสั่งวาดใหม่
16 * - Input = ปุ่ม (Button) ที่มี hotkey -> กดตัวอักษรบนคีย์บอร์ดได้เลยเมื่อแผงถูกโฟกัส
17 */
18
19import type { EngineInterface, Register, Timer } from 'claude-code'
20
21const PANE = 'nokia-games-th'
22const NOKIA_SCREEN = '#9bbc0f' // สีพื้นจอ Nokia 3310
23const NOKIA_INK = '#0f380f' // สีตัวหนังสือ/พิกเซล
24
25type Point = { x: number; y: number }
26type Direction = 'up' | 'down' | 'left' | 'right'
27
28// ==========================================================================
29// บทที่ 1: Game state ของเกมงู
30// ==========================================================================
31interface SnakeGame {
32  kind: 'snake'
33  width: number
34  height: number
35  snake: Point[] // ตัวแรก = หัว
36  direction: Direction
37  nextDirection: Direction // ทิศที่กดไว้ ใช้ตอน tick ถัดไป (กันกดสองทีแล้วหันกลับชนตัวเอง)
38  food: Point
39  score: number
40  isOver: boolean
41}
42
43function randomCell(width: number, height: number, avoid: Point[]): Point {
44  // สุ่มจนกว่าจะได้ช่องที่ไม่ทับตัวงู
45  for (;;) {
46    const p = { x: Math.floor(Math.random() * width), y: Math.floor(Math.random() * height) }
47    if (!avoid.some(a => a.x === p.x && a.y === p.y)) return p
48  }
49}
50
51function createSnake(width = 20, height = 12): SnakeGame {
52  const cx = Math.floor(width / 2)
53  const cy = Math.floor(height / 2)
54  const snake = [{ x: cx, y: cy }, { x: cx - 1, y: cy }, { x: cx - 2, y: cy }]
55  return { kind: 'snake', width, height, snake, direction: 'right', nextDirection: 'right', food: randomCell(width, height, snake), score: 0, isOver: false }
56}
57
58const OPPOSITE: Record<Direction, Direction> = { up: 'down', down: 'up', left: 'right', right: 'left' }
59
60// ==========================================================================
61// บทที่ 2: ขยับงูหนึ่งก้าว (เรียกทุก tick)
62// ==========================================================================
63function tickSnake(g: SnakeGame): void {
64  // ห้ามหันกลับ 180 องศา
65  if (g.nextDirection !== OPPOSITE[g.direction]) g.direction = g.nextDirection
66
67  const head = g.snake[0] ?? { x: 0, y: 0 }
68  const next = { ...head }
69  if (g.direction === 'up') next.y -= 1
70  if (g.direction === 'down') next.y += 1
71  if (g.direction === 'left') next.x -= 1
72  if (g.direction === 'right') next.x += 1
73
74  // ชนขอบ หรือชนตัวเอง (ไม่นับหางที่กำลังจะขยับออก) = จบเกม
75  const body = g.snake.slice(0, -1)
76  const hitWall = next.x < 0 || next.x >= g.width || next.y < 0 || next.y >= g.height
77  if (hitWall || body.some(s => s.x === next.x && s.y === next.y)) {
78    g.isOver = true
79    return
80  }
81
82  g.snake.unshift(next)
83  if (next.x === g.food.x && next.y === g.food.y) {
84    g.score += 10 // กินอาหาร: ไม่ตัดหาง งูจึงยาวขึ้น
85    g.food = randomCell(g.width, g.height, g.snake)
86  } else {
87    g.snake.pop()
88  }
89}
90
91// วาดเป็นบรรทัดข้อความ ช่องละ 2 ตัวอักษรให้ดูเป็นสี่เหลี่ยมจัตุรัส
92function drawSnake(g: SnakeGame): string[] {
93  const rows: string[] = []
94  for (let y = 0; y < g.height; y++) {
95    let row = ''
96    for (let x = 0; x < g.width; x++) {
97      const at = g.snake.findIndex(s => s.x === x && s.y === y)
98      row += at === 0 ? '██' : at > 0 ? '▓▓' : g.food.x === x && g.food.y === y ? '<>' : '  '
99    }
100    rows.push(row)
101  }
102  return rows
103}
104
105// ==========================================================================
106// บทที่ 3: Game state ของ Tetris
107// ==========================================================================
108const SHAPES: number[][][] = [
109  [[1, 1, 1, 1]], // I
110  [[1, 1], [1, 1]], // O
111  [[1, 1, 1], [0, 1, 0]], // T
112  [[1, 1, 0], [0, 1, 1]], // S
113  [[0, 1, 1], [1, 1, 0]], // Z
114  [[1, 1, 1], [1, 0, 0]], // J
115  [[1, 1, 1], [0, 0, 1]], // L
116]
117
118interface Piece {
119  shape: number[][]
120  x: number
121  y: number
122}
123
124interface TetrisGame {
125  kind: 'tetris'
126  width: number
127  height: number
128  board: number[][] // 1 = มีบล็อก
129  piece: Piece
130  score: number
131  lines: number
132  isOver: boolean
133}
134
135function randomPiece(width: number): Piece {
136  const shape = SHAPES[Math.floor(Math.random() * SHAPES.length)] ?? [[1]]
137  return { shape, x: Math.floor((width - (shape[0]?.length ?? 1)) / 2), y: 0 }
138}
139
140function createTetris(): TetrisGame {
141  const width = 10
142  const height = 16
143  return { kind: 'tetris', width, height, board: Array.from({ length: height }, () => Array<number>(width).fill(0)), piece: randomPiece(width), score: 0, lines: 0, isOver: false }
144}
145
146// ชิ้นนี้วางที่ตำแหน่งนี้ได้ไหม (ไม่ทะลุขอบ ไม่ทับบล็อกเดิม)
147function fits(g: TetrisGame, p: Piece): boolean {
148  return p.shape.every((row, dy) =>
149    row.every((cell, dx) => {
150      if (!cell) return true
151      const x = p.x + dx
152      const y = p.y + dy
153      return x >= 0 && x < g.width && y < g.height && (y < 0 || !g.board[y]?.[x])
154    }),
155  )
156}
157
158// หมุน 90 องศาตามเข็มนาฬิกา: แถวกลายเป็นคอลัมน์
159function rotate(shape: number[][]): number[][] {
160  const first = shape[0] ?? []
161  return first.map((_, x) => shape.map(row => row[x] ?? 0).reverse())
162}
163
164// ==========================================================================
165// บทที่ 4: Tetris - ตกหนึ่งช่อง ถ้าตกไม่ได้ให้วางลงกระดานแล้วลบแถวที่เต็ม
166// ==========================================================================
167function tickTetris(g: TetrisGame): void {
168  const down = { ...g.piece, y: g.piece.y + 1 }
169  if (fits(g, down)) {
170    g.piece = down
171    return
172  }
173
174  // วางชิ้นลงกระดาน
175  g.piece.shape.forEach((row, dy) =>
176    row.forEach((cell, dx) => {
177      const boardRow = g.board[g.piece.y + dy]
178      if (cell && boardRow) boardRow[g.piece.x + dx] = 1
179    }),
180  )
181
182  // ลบแถวที่เต็ม แล้วเติมแถวว่างด้านบน
183  const kept = g.board.filter(row => row.some(cell => !cell))
184  const cleared = g.height - kept.length
185  g.board = [...Array.from({ length: cleared }, () => Array<number>(g.width).fill(0)), ...kept]
186  g.lines += cleared
187  g.score += [0, 40, 100, 300, 1200][cleared] ?? 0
188
189  // ชิ้นใหม่วางไม่ได้ตั้งแต่บนสุด = จบเกม
190  g.piece = randomPiece(g.width)
191  if (!fits(g, g.piece)) g.isOver = true
192}
193
194function moveTetris(g: TetrisGame, action: 'left' | 'right' | 'rotate' | 'down' | 'drop'): void {
195  if (action === 'drop') {
196    while (fits(g, { ...g.piece, y: g.piece.y + 1 })) g.piece = { ...g.piece, y: g.piece.y + 1 }
197    tickTetris(g)
198    return
199  }
200  if (action === 'down') return tickTetris(g)
201  const moved: Piece =
202    action === 'rotate' ? { ...g.piece, shape: rotate(g.piece.shape) } : { ...g.piece, x: g.piece.x + (action === 'left' ? -1 : 1) }
203  if (fits(g, moved)) g.piece = moved
204}
205
206function drawTetris(g: TetrisGame): string[] {
207  return g.board.map((row, y) =>
208    row
209      .map((cell, x) => {
210        const py = y - g.piece.y
211        const px = x - g.piece.x
212        const isPiece = g.piece.shape[py]?.[px] === 1
213        return cell || isPiece ? '[]' : ' .'
214      })
215      .join(''),
216  )
217}
218
219// ==========================================================================
220// บทที่ 5: Game loop + High Score
221// ==========================================================================
222type Game = SnakeGame | TetrisGame
223type Score = { game: string; score: number; date: number }
224
225let game: Game | undefined
226let isPaused = false
227let loop: Timer | undefined
228let scores: Score[] = []
229
230const SPEED_MS = { snake: 220, tetris: 600 }
231
232function stopLoop(): void {
233  loop?.cancel()
234  loop = undefined
235}
236
237function startLoop($: EngineInterface): void {
238  stopLoop()
239  if (!game) return
240  loop = $.clock.every(SPEED_MS[game.kind], () => {
241    if (!game || isPaused || game.isOver) return
242    if (game.kind === 'snake') tickSnake(game)
243    else tickTetris(game)
244    if (game.isOver) void finish($, game)
245    $.ui.invalidate('ui.render')
246  })
247}
248
249async function finish($: EngineInterface, g: Game): Promise<void> {
250  stopLoop()
251  const best = scores.filter(s => s.game === g.kind).reduce((m, s) => Math.max(m, s.score), 0)
252  scores = [...scores, { game: g.kind, score: g.score, date: await $.clock.now() }].sort((a, b) => b.score - a.score).slice(0, 30)
253  await $.store.set('scores', scores)
254  $.ui.toast(g.score > best && g.score > 0 ? `🏆 สถิติใหม่! ${g.kind} ${g.score} คะแนน` : `💀 GAME OVER - ${g.score} คะแนน`)
255}
256
257async function startGame($: EngineInterface, next: Game): Promise<string> {
258  game = next
259  isPaused = false
260  // focus: true = แผงรับคีย์บอร์ด ปุ่ม hotkey w a s d จึงใช้ได้ทันที
261  const opened = await $.ui.open({ id: PANE, title: '📱 NOKIA 3310', focus: true, closeOnEscape: true })
262  startLoop($)
263  $.ui.invalidate('ui.render')
264  return opened.isPlaced ? '' : `\n(เปิดแผงไม่ได้ในหน้าจอนี้: ${opened.reason})`
265}
266
267// รับคำสั่งควบคุม ใช้ทั้งจากปุ่มในแผงและจากคำสั่ง /game up
268function control(action: string): void {
269  if (!game || game.isOver) return
270  if (game.kind === 'snake') {
271    const map: Record<string, Direction> = { up: 'up', w: 'up', down: 'down', s: 'down', left: 'left', a: 'left', right: 'right', d: 'right' }
272    const dir = map[action]
273    if (dir) game.nextDirection = dir
274    return
275  }
276  const map: Record<string, 'left' | 'right' | 'rotate' | 'down' | 'drop'> = { left: 'left', a: 'left', right: 'right', d: 'right', up: 'rotate', w: 'rotate', rotate: 'rotate', down: 'down', s: 'down', drop: 'drop', x: 'drop' }
277  const move = map[action]
278  if (move && !isPaused) moveTetris(game, move)
279  if (game.isOver) stopLoop()
280}
281
282function scoreBoard(): string {
283  if (scores.length === 0) return '🏆 ยังไม่มี High Score เล่นด้วย /game snake หรือ /game tetris'
284  const lines = ['🏆 High Scores']
285  for (const kind of ['snake', 'tetris']) {
286    const top = scores.filter(s => s.game === kind).slice(0, 5)
287    if (top.length === 0) continue
288    lines.push(`\n🎮 ${kind}:`)
289    top.forEach((s, i) => lines.push(`  ${i + 1}. ${s.score} คะแนน - ${new Date(s.date).toLocaleDateString('th-TH')}`))
290  }
291  return lines.join('\n')
292}
293
294const HELP = `📱 Nokia Games TH (Mod 13)
295========================================
296/game snake [กว้าง] [สูง]   เกมงู (ค่าเริ่มต้น 20x12)
297/game tetris                Tetris
298/game pause | resume | quit
299/game scores [clear]        High Score
300/game demo                  ดูหน้าจอตัวอย่างแบบข้อความ
301
302ในแผง (กดบนคีย์บอร์ดได้เลย):
303  งู:     w ขึ้น  a ซ้าย  s ลง  d ขวา
304  Tetris: a ซ้าย  d ขวา  w หมุน  s ลงเร็ว  x ทิ้งลงสุด
305  p หยุด/เล่นต่อ   r เริ่มใหม่   Esc ปิดแผง
306หรือใช้คำสั่ง /game up | down | left | right | drop`
307
308export const register: Register = on => {
309  on('session.start', async ($, e, next) => {
310    const saved = await $.store.get('scores')
311    scores = Array.isArray(saved) ? (saved as Score[]) : []
312    await $.command.register({ name: 'game', description: 'เกม Nokia: /game snake, /game tetris (/game help)', argumentHint: 'snake|tetris|scores|help' })
313    return next(e)
314  })
315
316  // ปิดแผง (Esc) = หยุดเกม จะได้ไม่มี timer วิ่งค้าง
317  on('ui.close', ($, e, next) => {
318    if (e.id === PANE && game && !game.isOver) {
319      isPaused = true
320      stopLoop()
321    }
322    return next(e)
323  })
324
325  // ==========================================================================
326  // บทที่ 6: วาดจอ Nokia + ปุ่มควบคุมที่มี hotkey
327  // ==========================================================================
328  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
329    const { Box, Text, Button } = $.ui.resolve(e)
330    if (!game) return <Text dimColor>เริ่มเกมด้วย /game snake หรือ /game tetris</Text>
331
332    const g = game
333    const rows = g.kind === 'snake' ? drawSnake(g) : drawTetris(g)
334    const status = g.isOver ? '💀 GAME OVER (r = เล่นใหม่)' : isPaused ? '⏸ หยุดอยู่ (p = เล่นต่อ)' : g.kind === 'snake' ? `ยาว ${g.snake.length}` : `${g.lines} แถว`
335    const press = (action: string) => () => {
336      control(action)
337      $.ui.invalidate('ui.render')
338    }
339    const restart = () => void startGame($, g.kind === 'snake' ? createSnake(g.width, g.height) : createTetris())
340    const togglePause = () => {
341      if (g.isOver) return
342      isPaused = !isPaused
343      $.ui.invalidate('ui.render')
344    }
345
346    return (
347      <Box flexDirection="column">
348        <Box flexDirection="column" backgroundColor={NOKIA_SCREEN} borderStyle="bold" borderColor={NOKIA_INK} paddingX={1}>
349          <Text color={NOKIA_INK} bold>
350            {`NOKIA  ${g.kind.toUpperCase()}  SCORE ${g.score}`}
351          </Text>
352          {rows.map(row => (
353            <Text color={NOKIA_INK}>{row}</Text>
354          ))}
355          <Text color={NOKIA_INK}>{status}</Text>
356        </Box>
357        <Box gap={1} flexWrap="wrap">
358          <Button key="w" hotkey="w" label={g.kind === 'snake' ? '↑' : '⟳'} onPress={press('w')} />
359          <Button key="a" hotkey="a" label="←" onPress={press('a')} />
360          <Button key="s" hotkey="s" label="↓" onPress={press('s')} />
361          <Button key="d" hotkey="d" label="→" onPress={press('d')} />
362          {g.kind === 'tetris' && <Button key="x" hotkey="x" label="ทิ้ง" onPress={press('x')} />}
363          <Button key="p" hotkey="p" label={isPaused ? 'เล่นต่อ' : 'หยุด'} onPress={togglePause} />
364          <Button key="r" hotkey="r" label="ใหม่" onPress={restart} />
365        </Box>
366      </Box>
367    )
368  })
369
370  // ==========================================================================
371  // บทที่ 7: คำสั่ง /game
372  // ==========================================================================
373  on('command.run', { command: 'game' }, async ($, e) => {
374    const [sub = 'help', a1, a2] = e.args.trim().split(/\s+/).filter(Boolean)
375
376    switch (sub) {
377      case 'snake': {
378        const width = Math.min(30, Math.max(8, Number(a1) || 20))
379        const height = Math.min(20, Math.max(6, Number(a2) || 12))
380        return { text: `🐍 เริ่มเกมงู ${width}x${height} กด w a s d ในแผงเพื่อบังคับ${await startGame($, createSnake(width, height))}` }
381      }
382      case 'tetris':
383        return { text: `🧱 เริ่ม Tetris กด a d เลื่อน, w หมุน, x ทิ้งลงสุด${await startGame($, createTetris())}` }
384      case 'pause':
385        isPaused = true
386        $.ui.invalidate('ui.render')
387        return { text: '⏸️ หยุดเกมแล้ว' }
388      case 'resume':
389        if (!game || game.isOver) return { text: 'ไม่มีเกมที่เล่นค้างอยู่' }
390        isPaused = false
391        if (!loop) startLoop($)
392        await $.ui.open({ id: PANE, title: '📱 NOKIA 3310', focus: true, closeOnEscape: true })
393        return { text: '▶️ เล่นต่อ' }
394      case 'quit':
395        stopLoop()
396        game = undefined
397        await $.ui.close({ id: PANE })
398        return { text: '👋 ออกจากเกมแล้ว' }
399      case 'scores':
400        if (a1 === 'clear') {
401          scores = []
402          await $.store.delete('scores')
403          return { text: '🧹 ล้าง High Score แล้ว' }
404        }
405        return { text: scoreBoard() }
406      case 'demo': {
407        const demo = createSnake(16, 6)
408        for (let i = 0; i < 3; i++) tickSnake(demo)
409        return { text: ['📱 NOKIA 3310 - ตัวอย่างจอเกมงู', ...drawSnake(demo).map(r => `|${r}|`), '/game snake เพื่อเล่นจริง'].join('\n') }
410      }
411      case 'up':
412      case 'down':
413      case 'left':
414      case 'right':
415      case 'drop':
416        control(sub)
417        $.ui.invalidate('ui.render')
418        return { text: `🎮 ${sub}` }
419      default:
420        return { text: HELP }
421    }
422  })
423}
424
425/**
426 * ========================================================================
427 * การบ้าน / ต่อยอด
428 * ========================================================================
429 * 1. ทำให้งูเร็วขึ้นทุก 50 คะแนน: เรียก startLoop ใหม่ด้วยค่า SPEED_MS ที่น้อยลง
430 * 2. วาดด้วย Raster (terminal) แทนข้อความ จะได้พิกเซลสีแบบจอจริง
431 * 3. ให้ Claude เล่นงูเอง: หาทางไปอาหารด้วย BFS แล้วเรียก control() ทุก tick
432 */
433