หลักคิดง่าย ๆ คือ เราคำนวณ ช่องถัดไปของหัว ไว้ก่อน แล้วค่อยถามว่าช่องนั้นปลอดภัยไหม ถ้าไม่ปลอดภัยค่อยตัดสินว่าแพ้ — โดยที่ยังไม่ได้ขยับงูจริง
ตำแหน่งหัว = ช่อง ทิศที่เดิน =
ตัวอย่าง เดินขวา จากหัวที่ :

next_head = [snake_body[0][0] + step_col, snake_body[0][1] + step_row]
snake_body[0]คือหัวงู ·step_col/step_rowคือทิศที่กำลังเดิน
ช่องที่ถูกต้องต้องอยู่ในกริด — ถ้าหลุดออกแม้ช่องเดียว = ตาย
จุดที่พลาดกันบ่อย: ต้องใช้
>=ไม่ใช่>เพราะช่องสุดท้ายในกริดคือGRID_COLS - 1
สมการด้านบนอ่านยาก ลองดูเป็นภาพกริดจริง — กล่องเส้นประคือ next_head (ช่องที่หัวกำลังจะไป แต่ ยังไม่ขยับ) แล้วเราถามว่าช่องนั้นปลอดภัยไหม

ซ้าย = ปลอดภัยเดินต่อ · กลาง =
next_headหลุดขอบขวา (col ≥ GRID_COLS) · ขวา =next_headไปตรงกับช่องที่ลำตัวอยู่แล้ว (next_head in snake_body) สองกรณีหลังคือ "แพ้"
ตำแหน่งสำคัญมาก ให้ใส่บล็อกนี้ หลัง next_head และ ก่อน snake_body.insert(...):

# ----- เติมส่วนนี้เอง: เงื่อนไข "แพ้" (ตรวจก่อนขยับงูจริง) -----
if (next_head[0] < 0 or next_head[0] >= GRID_COLS or # ชนกำแพงซ้าย/ขวา
next_head[1] < 0 or next_head[1] >= GRID_ROWS or # ชนกำแพงบน/ล่าง
next_head in snake_body): # หัวซ้อนทับตัวเอง
game.sfx("die")
game.Text("GAME OVER", 320, 180, game.RED)
return False # ออกจาก game loop
ลองอ่านออกเสียงตามตรรกะ: "ถ้าหัวใหม่หลุดออกซ้าย หรือขวา หรือบน หรือล่าง หรือไปทับตัวเอง ก็เล่นเสียงตาย ขึ้นป้าย แล้วจบเกม"
next_head in snake_bodyเป็นความสะดวกของ Python ที่ตอบ True/False ให้เราได้เลย โดยไม่ต้องเขียน loop เช็คเอง
70% core (แค่เรียก):
game.start · game.Box · game.Text.set
game.keys · game.sfx · game.run
30% logic (เขียนวันนี้):
บล็อก if ตัดสินว่า "ตายเมื่อไหร่"
ตรงกับ full_games/snake_full.py:43–47
ไฟล์ฝึก practise_codes/snake_step4.py↗ เปิดช่องไว้ตรงนี้ พร้อมคำใบ้กำกับ:
next_head = [snake_body[0][0] + step_col, snake_body[0][1] + step_row] # ช่องถัดไปของหัว
# ----- เติมส่วนนี้เอง (งานของคุณ): เงื่อนไข "แพ้" (ตรวจก่อนขยับงูจริง) -----
# next_head คือ [col, row] ของหัวงูช่องถัดไป (คำนวณให้แล้วบรรทัดบน)
# 1) เขียน if ตรวจ "แพ้" 2 กรณี รวมกันด้วย or:
# (ก) หัวหลุดขอบกริด — col หรือ row ติดลบ หรือ เกินจำนวนช่อง (GRID_COLS / GRID_ROWS)
# (ข) หัวไปทับตัวเอง — next_head อยู่ใน snake_body แล้ว (ใช้ตัวดำเนินการ in)
# 2) ถ้าเข้าเงื่อนไขแพ้ ภายใน if ให้:
# - เล่นเสียงตายด้วย game.sfx() (ชื่อเสียง "die")
# - ขึ้นป้ายด้วย game.Text(ข้อความ, x, y, สี) — เลือกตำแหน่งกลางจอเอง, ใช้สี game.RED
# - return False เพื่อออกจาก game loop
pass # <- ลบ pass ออกเมื่อเริ่มเขียน

ลองเขียนเองให้สุดก่อนนะ — เดี๋ยวเฉลยพร้อมกันในห้องเรียน
เราไม่ได้สั่งรันด้วย exec(open(...)) ที่ REPL — แต่ใช้เครื่องมือของ BENTO IDE ที่เตรียมไว้ให้แล้ว:
snake_step4.py ใน BENTO IDEProgram to Device จะจัดการส่งไฟล์ + รันให้เอง น้อง ๆ ไม่ต้องพิมพ์คำสั่งรันเองที่ REPL
เปิดไฟล์ → Program to Device → เดินงูชนกำแพงซ้าย
→ "GAME OVER" สีแดง + เสียงตาย → งูหยุดนิ่ง
ลองอีกแบบหนึ่ง: กินอาหารสัก 3–4 ครั้งให้งูยาวก่อน แล้ว เลี้ยวกลับมาทับหางตัวเอง คราวนี้ก็ต้อง GAME OVER เหมือนกัน

สังเกตจังหวะในภาพ: หัวเดินไปจนช่อง
next_head(เส้นประ) หลุดออกนอกขอบ — บล็อกifจับได้ก่อนงูขยับจริง จึงreturn Falseแล้วงูหยุดนิ่งตรงนั้น
เป้าหมายถัดไป (คาบ 6) คือ step5 ที่จะมี sprite พิกเซลเป๊ะ พร้อมเมนูเลือกความยาก
die จริงbentogame.py มีตัวกัน _HAS_SOUND ไว้ ถ้าเฟิร์มแวร์ไม่มีเสียง game.sfx(...) จะไม่ทำอะไรเลย แต่จะไม่ทำให้โปรแกรมพังเสียงในคาบนี้ — Snake ใช้
snake_dieตอนชน/แพ้ (บล็อกifวันนี้) คู่กับsnake_eatตอนกินอาหารจากคาบก่อน และgame_overเป็น stretch ต่อท้ายตอนจบเกม (กดเล่นฟังได้จริง):
รูป spectrogram สร้างจากไฟล์เสียง .mp3 ชุดเดียวกันด้านบน (สี = ความเข้มพลังงานตามความถี่–เวลา) — สังเกต snake_die ทอดเสียงยาว–ตกต่ำ ส่วน snake_eat สั้น–สูง คม
ทาง "die" ทำ
return Falseออกจากgame.run(...)— เกมหยุดทันที นี่คือ state machine เล็ก ๆ: เล่นอยู่ → ตาย → จบ
on_each_frame)ผังฝั่ง state-machine ก่อนหน้าเป็นเสี้ยวหนึ่ง — นี่คือ ภาพเต็มทั้งเฟรม ตามลำดับจริงใน snake_step4.py:46-77 · สัญกรณ์ที่ใช้ทั้งคอร์ส: กล่องมนเทา = เริ่ม/จบ · เขียว = อ่าน/วาด · น้ำเงิน = คำนวณ · ส้ม = จบเกม · ข้าวหลามตัดม่วง = ตัดสินใจ
theory → code: boundary 0 ≤ c < COLS → :58-60 · next_head in snake_body (linear search) → :60 · เช็ก next_head ก่อน insert → :57 เทียบ :65 · grow-invariant (กิน = ไม่ pop) → :66-73
ทางเดินหลักคือ อ่าน → คำนวณ next_head → ตัดสินแพ้ → กิน/เดิน → วาด → วน เส้นประคือ
game.run()เรียกon_each_frame()ซ้ำทุกเฟรม (9 fps) มีเพียงกิ่ง "แพ้" ที่return Falseออกจากลูป
insert) → หัวถูกขยับแล้วก่อนเช็ค ทำให้เช็คชนตัวเองเพี้ยน → เช็ค next_head ก่อน insert เสมอreturn False → ป้าย GAME OVER ขึ้นแต่งูยังเดินทับป้ายnext_head in snake_body ไม่เจอ → snake_body ต้องเก็บ [col, row] (list) ไม่ใช่ tuple (col, row)>= ไม่ใช่ > กับขอบเกณฑ์ผ่าน
die (บนบอร์ดจริง)if ของตัวเองได้ส่งงาน (commit)
snake_step4.py (เวอร์ชันแพ้ได้) ขึ้น branch กลุ่มStretch (ทีมเร็ว)
frames_alive → โชว์ "รอดได้ N วิ" ตอนจบgame.sfx("gameover") ต่อจาก diereturn Falsewhile not game.keys().start: passผู้สอน
if เดียว — อย่าให้น้องเขียนเกมใหม่ทั้งไฟล์full_games/snake_full.py:43–47 หลังลองเองnext_head ก่อนขยับ" ครอบทั้ง wall + self collisionบล็อกเช็คแพ้สั้น ๆ วันนี้ จับแนวคิดที่วิศวกรซอฟต์แวร์และระบบฝังตัวใช้จริงทุกวัน:
ฝั่ง Algorithm
return False คือ transition ออกจากสถานะ "กำลังเล่น"ฝั่ง Python
in กับ list — next_head in snake_body คือการค้นสมาชิกในลิสต์ ภายในคือ linear search ที่ Python ทำให้เราในบรรทัดเดียวsnake_body ต้องเก็บ [col, row] (list) ให้ตรงชนิดกับ next_head การเทียบจึงจะเจอฝั่ง Embedded
0 ≤ index < SIZE คือกฎเดียวกับที่กันการอ่านนอกขอบ buffer/array บน MCU ที่ไม่มี OS คอยจับ ถ้าพลาดคือ HardFault ของจริงฝั่ง Graphics
How Collisions Work in Games — TheHappieCat
อธิบายแนวคิด hitbox และการตรวจชนแบบภาพรวม คนละภาษา/เอนจิน แต่หลักคิด "เช็คก่อนชน" เหมือนที่เราทำกับ
next_headวันนี้
next_head ก่อนขยับ ครอบคลุมทั้งชนกำแพงและชนตัวเองทำเอง 30%: เปิด practise_codes/snake_step4.py↗ เติมบล็อก if เช็คแพ้ด้วยตัวเอง (ติดตรงไหนยกมือถามได้เลย — เดี๋ยวเฉลยในห้อง)
คาบหน้า: คาบ 6 — Snake #5: เปลี่ยน Box เป็น Sprite จริง + เมนูเลือกความยาก (MVP-1)
snake_step4.py — อ่านให้เข้าใจแล้วพิมพ์เองเฉลยนี้มีไว้ให้ เทียบ ไม่ใช่ให้ลอก คะแนนของคาบนี้อยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง ไม่ได้อยู่ที่โค้ดตรงกับเฉลยเป๊ะ วิธีใช้ให้ได้ผลจริงมีขั้นเดียว: อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนั่นแหละที่สมองจำ pattern ได้ ต่อจากนี้เราจะแกะทีละส่วน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ส่วนแรก — คำนวณช่องถัดไปของหัวก่อน ยังไม่ขยับ:
next_head = [snake_body[0][0] + step_col, snake_body[0][1] + step_row] # ช่องถัดไปของหัว
next_head แทนที่จะขยับหัวลงไปเลย? เพราะนี่คือหัวใจที่เราคุยกันในสไลด์ "เช็คก่อนขยับ" — เราต้อง ถามก่อนว่าช่องนั้นปลอดภัยไหม ถ้าขยับไปแล้วค่อยเช็ค งูจะทับตัวเองหรือหลุดขอบไปแล้วหนึ่งเฟรม แก้คืนยาก (:55)snake_body[0] คือหัวงูเสมอ (เราออกแบบให้หัวอยู่ต้นลิสต์ตั้งแต่คาบก่อน) [0][0] คือ col ของหัว [0][1] คือ row · step_col/step_row คือทิศที่กำลังเดิน มาจากการอ่านปุ่มด้านบน :48-52[col, row] แบบ list ไม่ใช่ tuple โดยตั้งใจ เพราะอีกสองบรรทัดถัดไปเราจะเอามันไปเทียบกับ snake_body ที่เก็บ list เหมือนกัน ชนิดต้องตรงกัน in ถึงจะเจอsnake_body เลย มันแค่ "จินตนาการ" ว่าหัวจะไปอยู่ไหน นี่คือนิสัยที่ดี: แยกการ คิด ออกจากการ ลงมือ
next_headคือช่องในอนาคตที่เรายังไม่เชื่อว่าปลอดภัย บรรทัดเดียวนี้เปิดทางให้เราตัดสินแพ้ได้ก่อนที่ความเสียหายจะเกิด
if เช็คแพ้ (30% ของวันนี้) # ----- เติมส่วนนี้เอง: เงื่อนไข "แพ้" (ตรวจก่อนขยับงูจริง) -----
if (next_head[0] < 0 or next_head[0] >= GRID_COLS or # ชนกำแพงซ้าย/ขวา
next_head[1] < 0 or next_head[1] >= GRID_ROWS or # ชนกำแพงบน/ล่าง
next_head in snake_body): # หัวซ้อนทับตัวเอง
game.sfx("die")
game.Text("GAME OVER", 320, 180, game.RED)
return False # ออกจาก game loop
if เดียว แต่รวมเงื่อนไขแพ้ สามอย่าง ด้วย or ต่อกัน ถ้าอย่างใดอย่างหนึ่งจริง = แพ้ทันที นี่คือ collision detection ที่เราวาดเป็นภาพสามกรณีไปแล้ว (:57-63)>= GRID_COLS ต้องเป็น >= ไม่ใช่ > เพราะช่องขวาสุดที่ถูกต้องคือ GRID_COLS - 1 พอ col เท่ากับ GRID_COLS พอดีก็หลุดขอบแล้ว จุดนี้คือ bounds-checking 0 ≤ index < SIZE อันเดียวกับที่กัน buffer overflow บน MCU ถ้าพลาดบนของจริงคือ HardFault ไม่ใช่แค่งูตายnext_head in snake_body ให้ Python ค้นทั้งลิสต์ให้เราในบรรทัดเดียว (ข้างในคือ linear search) เราไม่ต้องเขียน for วนเทียบเอง — และมันทำงานได้เพราะ next_head กับสมาชิกใน snake_body เป็น list ชนิดเดียวกัน ถ้าเผลอเก็บเป็น tuple จะเทียบไม่เจอif เรียงตามลำดับที่อ่านออกเสียงได้: ส่งเสียงตาย → ขึ้นป้ายกลางจอ → return False ตัวสุดท้ายสำคัญที่สุด มันคือ transition ของ state machine "กำลังเล่น → จบ" ที่ทำให้ game.run() หยุดเรียกเฟรมถัดไป ถ้าลืมมันไป ป้ายขึ้นแต่งูยังเดินทับป้ายต่ออ่านออกเสียงตามตรรกะ: "ถ้าหัวใหม่หลุดซ้าย หรือขวา หรือบน หรือล่าง หรือไปทับตัวเอง ก็เล่นเสียงตาย ขึ้นป้าย แล้วออกจากลูป" — โค้ดที่อ่านออกเสียงแล้วเข้าใจ คือโค้ดที่ออกแบบมาดี
snake_body.insert(0, next_head)
if next_head == [food_square.x // CELL_PX, food_square.y // CELL_PX]: # กินอาหาร -> โต + คะแนน
score += 1
game.sfx("eat")
score_text.set("Score: %d" % score)
body_squares.append(game.Box(0, 0, CELL_PX - 2, CELL_PX - 2, game.GB_DARK))
place_food_at_random_empty_cell()
else:
snake_body.pop() # ไม่กิน -> เดินปกติ
snake_body.insert(0, next_head) อยู่ หลัง บล็อกเช็คแพ้เสมอ นี่คือเหตุผลที่เราคำนวณ next_head แยกไว้ก่อน: กว่าจะมาถึงบรรทัดนี้ได้ แปลว่าช่องนั้นผ่านด่านปลอดภัยมาแล้ว การขยับจริงจึงเกิดทีหลังการตัดสินใจ (:65) ถ้าสลับลำดับเอาบล็อก if ไปไว้หลัง insert การเช็คชนตัวเองจะเพี้ยน เพราะหัวถูกใส่เข้าลิสต์ไปแล้ว:66-73)game.sfx("eat") คู่กับ game.sfx("die") ในบล็อกก่อน เฟิร์มแวร์ไหนไม่มีเสียงก็กลายเป็น no-op ไม่พัง เพราะ bentogame.py มีตัวกัน _HAS_SOUND ให้ ตรรกะเลยเหมือนกันทุกบอร์ดplace_food_at_random_empty_cell() ที่วน while True จนสุ่มได้ช่องที่ not in snake_body — กันไม่ให้อาหารไปโผล่ทับตัวงู หลักคิด in/not in อันเดียวกับที่เราใช้เช็คชนตัวเอง แค่กลับด้านจับจุดนี้ไว้: โครงสร้างที่ดีบังคับให้ "ตัดสินใจก่อน ลงมือทีหลัง" การวาง insert ไว้หลังบล็อกเช็คแพ้ ไม่ใช่เรื่องบังเอิญ แต่คือการออกแบบลำดับให้ถูก
ทั้งเฟรมของ on_each_frame ถูกเรียงให้แต่ละก้าวต่อยอดจากก้าวก่อน โดยยังใช้ของเดิมที่คาบก่อนวางไว้ นี่คือวิธีเดียวกับที่เราจะค่อย ๆ ต่อเกมทั้งเครื่องขึ้นมา:
| ส่วน | บรรทัด | แนวคิดใหม่ที่เพิ่มเข้ามา | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| อ่านปุ่ม + กันเลี้ยวกลับ | :48-52 |
แปลง input เป็นทิศ step_col/step_row |
โครง on_each_frame จากคาบก่อน |
คำนวณ next_head |
:55 |
คิดช่องอนาคตแยกจากการขยับจริง | พิกัดกริด (col, row) |
| บล็อกเช็คแพ้ (วันนี้) | :57-63 |
collision detection + return False |
next_head + in/>= |
| insert + กิน/ไม่กิน | :65-73 |
grow-invariant (pop หรือไม่ pop) | next_head ที่ผ่านด่านแล้ว |
| วาดงูใหม่ | :75-77 |
sync ลิสต์ข้อมูล → กล่องบนจอ | game.Box.move_to จากคาบก่อน |
next_head: คำนวณครั้งเดียว (:55) แล้วถูกใช้ซ้ำทั้งการเช็คแพ้ (:60) และการเทียบกินอาหาร (:66) คิดของสำคัญครั้งเดียว ใช้หลายที่:55 → :57-63 → :65 — คิด → ตัดสิน → ลงมือ ตามลำดับ ถ้าจับจังหวะนี้ได้ น้องจะเห็นว่าเกมทั้งเกมก็สร้างแบบเดียวกัน: เริ่มจากของเล็กที่รันได้ แล้วเติมทีละแนวคิดอย่ากระโดดไปเขียนทั้งเกมรวดเดียว เริ่มจากก้าวที่เล็กที่สุดที่ยังรันได้ แล้วเติมทีละชิ้น — นี่คือวิธีที่วิศวกรจริงสร้างของใหญ่
if นี้มาจากไหน แล้วพาเราไปถึงไหนหยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องพิมพ์โค้ด แต่เรื่องมองให้ทะลุว่าบล็อกเช็คแพ้สั้น ๆ วันนี้ จริง ๆ แล้วมันคือชิ้นส่วนของอะไร
ที่มา — บล็อกนี้ยืนอยู่บนอะไร ย้อนไปสไลด์ "หัวใจของวันนี้: เช็คก่อนขยับ (next_head)" เราตั้งกติกาไว้ว่าจะ คิดช่องอนาคตก่อน แล้วค่อยถามว่าปลอดภัยไหม บล็อก if วันนี้คือการเอากติกานั้นมาใช้จริงครั้งแรก ปัญหาที่มันแก้เก่าแก่กว่าที่คิด: ตั้งแต่สไลด์ "คณิตของชนกำแพง" เราพิสูจน์ว่า "อยู่ในจอไหม" คือคณิต 0 ≤ index < SIZE ล้วน ๆ อันเดียวกับที่ MCU ใช้กันอ่านนอกขอบ array มาหลายสิบปี วันนี้เราแค่เอามาห่อเป็นเงื่อนไขแพ้ของเกม
ที่ไป — บล็อกนี้จะโตเป็นอะไร ในสไลด์ "flow จบเกมอย่างสุภาพ" อาจารย์เรียก return False ว่า transition ของ state machine "เล่นอยู่ → ตาย → จบ" นั่นคือเมล็ดของ game state machine ที่จะโตต่อในคาบ 6 (เมนูเลือกความยาก = อีกสถานะหนึ่ง) และในโปรเจกต์จริงมันคือหัวใจของทุกระบบที่ต้องรู้ว่า "ตอนนี้อยู่โหมดไหน" ส่วนการเทียบ next_head กับสิ่งกีดขวาง ก็คือ collision detection ที่เกมทุกเกมและหุ่นยนต์ทุกตัวใช้
ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
if แบบเดียวกันนี้หรือเปล่า?return False พาเกมออกจากลูปได้ พรุ่งนี้เราจะทำให้มัน ไปหน้าเมนูเลือกความยาก แทนที่จะจบเลย ได้ไหม? (ใบ้: นั่นคือการเพิ่มอีกหนึ่งสถานะให้ state machine)next_head in snake_body (ชนตัวเอง) กับ not in snake_body ใน place_food (หาช่องว่างวางอาหาร) มันคือ เรื่องเดียวกัน — ถามว่า "ช่องนี้มีลำตัวอยู่ไหม" แค่กลับด้านคำตอบ?ถ้าตอบสามคำถามข้างบนได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด — บล็อก
ifเดียววันนี้ไม่ได้เล็ก มันคือเมล็ดของ collision detection กับ state machine ทั้งวงการ
เทคนิควันนี้ไม่ใช่แค่แบบฝึกหัด มันคือสิ่งที่ระบบจริงใช้กันทุกวัน:
next_head ก่อนขยับ" เป๊ะ ต่างแค่กริดคือแผนที่ห้อง ไม่ใช่ช่อง Snake0 ≤ index < SIZE เองทุกครั้ง เหมือน >= GRID_COLS วันนี้ พลาดเมื่อไรคือ HardFault ของจริง ไม่ใช่แค่ป้าย GAME OVERreturn False ที่พาเราออกจาก "กำลังเล่น" คือ transition เดียวกับปุ่ม cancel ที่พาเครื่องกลับ idlenext_head in snake_body คือ broad-phase collision บนกริดฉบับจิ๋วสุดห้าบรรทัดในบล็อก
ifวันนี้ ไม่มีบรรทัดไหนเป็นของสมมติเลย ทุกบรรทัดมีเครื่องจริงในโลกที่ทำงานด้วยหลักการเดียวกันอยู่
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:
walls ของ [col, row]) เงื่อนไขแพ้จะเพิ่มอีกกี่ตัวใน or และหน้าตาเหมือน next_head in snake_body แค่ไหน?if ให้คำนวณ col/row ใหม่ด้วย % อย่างไร แล้วเงื่อนไขแพ้จะเหลือแค่ "ชนตัวเอง" อย่างเดียวใช่ไหม? เกมจะยากขึ้นหรือง่ายลงframes_alive ไว้ตรงไหน แปลงเป็นวินาทีด้วย fps=9 ยังไง แล้วโชว์บนป้าย GAME OVER คู่กับ Score?return False ทีเดียว น้องจะเพิ่ม "สถานะ" อะไรให้เกมบ้าง? นี่คือสิ่งที่คาบหน้า (Snake #5: Sprite + เมนูเลือกความยาก) ต้องใช้ — ลองร่างสถานะของเกมทั้งหมดไว้ก่อน แล้วคาบหน้าต่อจากตรงนั้นเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากบล็อก
ifที่พิมพ์เองวันนี้ — ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นวิศวกร ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css
Marp deck — คาบ 5 (Snake ขั้นที่ 4/5 — เล่นแพ้ได้ครบ ระดับผู้เริ่มต้น). Render: npx @marp-team/marp-cli session-05.md -o session-05.html สอนแบบเดโมก่อน เพิ่มทีละก้าว: โชว์เฉพาะโค้ดของก้าวนี้ (บล็อกเช็คแพ้).