ปี 1997 Nokia ใส่เกม Snake ลงในมือถือ และ Nokia 3310 (ปี 2000) ทำให้มันดังไปทั้งโลก
จอขาวดำ ปุ่มไม่กี่ปุ่ม หน่วยความจำนิดเดียว แต่สนุกได้เพราะกติกาเรียบง่าย
หัวใจของเกมนั้นคือสิ่งที่เรากำลังเขียนเป๊ะ ๆ:
เครื่องเล็กแรงน้อยทำเกมที่คนจำได้ทั้งรุ่น เพราะตรรกะคิดมาดี ไม่ใช่เพราะฮาร์ดแวร์แรง — บอร์ดในมือเราวันนี้แรงกว่ามันหลายร้อยเท่า

ที่มา: "Nokia 3310 blue" — Michael Brandtner, Public domain, Wikimedia Commons
[col,row] แล้วคูณตอนวาด
while True สุ่มช่องใหม่ถ้าทับตัวงู
game.sfx("eat") + score_text.set()
| สถานการณ์ | ทำกับ list | ผลบนจอ |
|---|---|---|
| เดินปกติ | insert(0,หัว) + pop() |
ยาวเท่าเดิม |
| กินอาหาร | insert(0,หัว) ไม่ pop() |
ยาว +1 ปล้อง |
สมการให้น้อง ๆ จำ:
คาบนี้เราเขียนแต่ตรรกะของเกม ส่วนการวาดและเสียงเป็นหน้าที่ของ engine ที่เราเรียกใช้ผ่าน bentogame
ไฟล์ที่เราแก้คาบนี้:
practise_codes/snake_step3.py↗ (โครงเว้นช่อง 30% ให้เติมเอง)
ถ้าติดจริง ๆ ยกมือถามได้เลย — เดี๋ยวเฉลยพร้อมกันในห้อง
import bentogame as game
import random # ใหม่: ไว้สุ่มช่องอาหาร
food_square = game.Box(0, 0, CELL_PX - 2, CELL_PX - 2, game.GB_LIGHTEST) # อาหาร = ช่องสว่างสุด
score_text = game.Text("Score: 0", 10, 8, game.WHITE)
step_col, step_row, score = 1, 0, 0 # +score = state ใหม่ของคาบนี้
def place_food_at_random_empty_cell():
while True:
col = random.randint(0, GRID_COLS - 1) # สุ่ม "ช่อง"
row = random.randint(0, GRID_ROWS - 1)
if [col, row] not in snake_body: # ห้ามทับตัวงู -> สุ่มใหม่
food_square.move_to(col * CELL_PX, row * CELL_PX) # ช่อง -> พิกเซล
return
แปลงหน่วยกริด → พิกเซลตอนวาด:
![]()
เราคิดและเก็บทุกอย่างเป็น "ช่อง" [col,row] (ตัวเลขเล็ก ๆ นับง่าย) แล้ว คูณ CELL_PX ตอนวาด เพื่อบอก engine ว่าจะวางกล่องที่พิกเซลไหนบนจอ — แยกสองโลกนี้ออกจากกันคือหัวใจของ coordinate transform ในเอนจินเกมทุกตัว
while Trueคู่กับreturnคือสำนวน "ลองสุ่มไปเรื่อย ๆ จนกว่าจะได้ช่องที่ว่างจริง"
วิธีนี้มีชื่อในวิชา algorithm ว่า rejection sampling: สุ่มมั่ว ๆ แล้ว "ทิ้ง" ตัวที่ไม่ผ่านเงื่อนไข

ทุกครั้งที่สุ่มได้ช่องที่ทับตัวงู เงื่อนไข if [col,row] not in snake_body: เป็นเท็จ เราก็วน while True สุ่มใหม่ จนกว่าจะเจอช่องว่างจริงแล้วค่อย return
ถ้า f = สัดส่วนช่องที่ไม่ว่าง (งูยาวขึ้น f ก็โตขึ้น) จำนวนครั้งที่คาดว่าต้องสุ่มคือ

อ่านกราฟง่าย ๆ:
ในเกมจริง Snake ของเรากระดานไม่เคยเต็มขนาดนั้น วิธีนี้จึง เร็วพอและเขียนง่าย เหมาะกับงานบนบอร์ด
บทเรียน algorithm: วิธีที่ "ไม่สมบูรณ์แบบทางทฤษฎี" แต่ เข้าใจง่ายและเร็วพอกับงานจริง มักเป็นทางเลือกที่ดีที่สุดบน MCU
next_head = [snake_body[0][0] + step_col, snake_body[0][1] + step_row]
snake_body.insert(0, next_head) # เติมหัวใหม่เสมอ
ate_food = (next_head == [food_square.x // CELL_PX, food_square.y // CELL_PX])
if ate_food: # งูกินอาหารเข้า
score += 1
game.sfx("eat") # เสียงงับ มาจาก engine จริง
score_text.set("Score: %d" % score)
body_squares.append(game.Box(0, 0, CELL_PX - 2, CELL_PX - 2, game.GB_DARK)) # โต 1 ปล้อง
game.Text("Nice bite!", 320, 180, game.GB_LIGHT)
place_food_at_random_empty_cell()
else:
snake_body.pop() # ไม่กิน -> ตัดหาง = เดินปกติ
ผลต่อความยาวงูในแต่ละเฟรม สรุปเป็นเงื่อนไขเดียว:

จุดสำคัญคือ ตอนกินเราไม่เรียก
pop()งูจึงยาวขึ้น 1 ปล้อง
เทียบ "ช่อง" ตรง ๆ — หัวงูกับอาหารต้องอยู่ช่องเดียวกัน:
next_head เก็บเป็น "ช่อง"food_square.x // CELL_PX game.run(on_each_frame, fps=9) # 9 เฟรม/วิ = จังหวะ Snake คลาสสิก
ลองบังคับงูไปงับช่องสว่างดู งูจะยาวขึ้น Score เพิ่ม มีเสียงงับ แล้วอาหารก็โผล่ช่องใหม่

ภาพจริงจากบอร์ด PSoC Edge Eva Kit รันผ่าน BENTO IDE — Copyright by BDH & TESAIoT
นี่คือผลตอนรันจริงบนบอร์ดหลัง game.run(...): งูยาวขึ้นหลายปล้องแล้ว Score: 9 มุมซ้ายบน และมีอาหาร (จุดสว่าง) รอให้งับช่องถัดไป — ตรงกับ acceptance ของคาบนี้ทุกข้อ
snake_step3.py — 1 tick เต็มแผนผัง TODO ก่อนหน้าโฟกัสแค่ 2 แขนงตอนกิน/ไม่กิน ผังนี้ประกอบมันกลับเข้าไปใน 1 รอบเต็มของ on_each_frame(): อ่านจอย → เติมหัว → ◇ ถามว่า "หัวอยู่ช่องเดียวกับอาหารไหม" (ate_food) → กิน (ไม่ตัดหาง → ยาว +1) หรือ เดินปกติ (ตัดหาง) → แล้วสองแขนงมาบรรจบที่การวาดใหม่
ทั้งไฟล์เติมหัว (
insert(0)) เสมอ ก่อนถาม ate_food — ความต่างอยู่ที่ ตัดหางหรือไม่: กิน = ไม่pop()(ยาว +1), ไม่กิน =pop()(ยาวเท่าเดิม) นี่คือสมการlen' = len + 1เฉพาะตอนกิน ที่เห็นบนสไลด์ก่อนหน้า แปลเป็น control flow ตรง ๆ
snake_step3.py ใน BENTO IDEเราไม่ใช้
exec(open(...))ในการรัน — บนบอร์ดให้กด Program to Device เสมอ
เสียงgame.sfx("eat")จะ "ดังจริง" เฉพาะบนบอร์ดเท่านั้น

game.sfx("eat") หนึ่งบรรทัด ถูกแปลงเป็นเสียงจริงด้วยฮาร์ดแวร์หลายชั้น:

เราสั่งแค่ "ชื่อเสียง" หนึ่งคำ แต่ engine ไปผสม ตารางตัวอย่างคลื่นเสียง เป็นตัวเลข PCM แล้วส่งออกทาง บัส I2S ไปยังชิป codec (TLV320DAC3100) ที่ทำหน้าที่ DAC แปลงเลขเป็นแรงดัน พร้อม amplifier Class-D ในตัวขับลำโพง — บนหน้าจอจำลองไม่มี codec กับลำโพงจริง เสียงจึงเงียบ
เสียงดิจิทัล "วัด" สองแกน — เวลา (sample rate) กับ ระดับ (จำนวนบิต) — แล้ว DAC แปลงเลขแต่ละตัวเป็นแรงดันออกมาเป็นเส้นตรง:
แปลว่าเครื่องยิงตัวเลขออกมา 48,000 ตัวต่อวินาที และ DAC เปลี่ยน "เลข code" (ลึก บิต) ให้เป็นแรงดันเทียบสัดส่วนกับช่วงเต็มสเกล ของ codec — นี่คือ digital-to-analog ที่จับต้องได้จริง
นี่คือ digital-to-analog ของจริง: ตัวเลขในหน่วยความจำกลายเป็นคลื่นแรงดันที่หูเราได้ยิน
game.sfx("eat") + ลากซูมรูปคลื่นgame.sfx("eat") ที่เราเรียกในคาบนี้ = เสียง snake_eat ด้านล่าง (square 659 Hz) สังเคราะห์ตรงจากตารางใน firmware — กดเลือก → เล่น → ลากแถบ ซูม ดูรูปคลื่นราย cycle → เทียบ spectrogram
เสียงเกมอื่นก็มาจากตารางเดียวกัน — square ของ snake ฟังกระด้าง, saw ของยิงเลเซอร์ฟังคม · เครื่องมือเต็ม: sound_video_demo
E = 1/(1-f) · เลือกวิธีที่เร็วพอกับงานจริงบน MCU
sfx("eat") -> PCM samples -> บัส I2S -> codec (DAC+amp) -> ลำโพง คือสาย digital-to-analog ของจริงบน PSoC Edge E84
[col,row] แล้วคูณ CELL_PX ตอนวาด คือ coordinate transform grid -> pixel ที่เอนจินเกมทุกตัวใช้
เกมเล็ก ๆ เกมเดียวร้อยทั้ง 4 รากฐานเข้าด้วยกัน เมื่อน้องเข้าใจว่าทำไมมันถึงทำงาน น้องก็เอาแนวคิดเดียวกันนี้ไปสร้างผลิตภัณฑ์จริงได้
ดูคนอื่นวางตรรกะ Snake แบบสั้น ๆ แล้วเทียบกับของเรา — เขาใช้คนละ engine แต่หัวใจ list / อาหาร / โต เหมือนกันเป๊ะ:
Creating a Snake game with Python in under 5 minutes — Engineer Man
| อาการ | สาเหตุ | แก้ |
|---|---|---|
| กินแล้วไม่โต / งูสั้นลง | ลืม — ตอนกินต้อง ไม่ pop() |
grow = insert หัว ไม่ตัดหาง |
name 'score' is not defined |
ลืม global ..., score |
เติมใน global ต้นฟังก์ชัน |
| กินไม่ติดทั้งที่กล่องทับ | เทียบผิดหน่วย (พิกเซล vs ช่อง) | เทียบช่อง x // CELL_PX |
| อาหารโผล่นอกจอ | สุ่มพิกเซลแทนช่อง | สุ่ม 0..GRID-1 แล้วคูณ |
| อาหารทับตัวงู | ลืม if [c,r] not in snake_body |
ใส่เงื่อนไขใน while True |
| ไม่ได้ยินเสียง | sfx ดังจริงเฉพาะ บนบอร์ด |
กด Program to Device แล้วฟังที่บอร์ด |
ถ้ารันแล้วงูค้าง ให้กด Program to Device ใหม่อีกครั้ง
เช็คผ่าน (Acceptance)
"eat"งาน 30% ของน้อง ๆ คาบนี้
ใน practise_codes/snake_step3.py↗ มีช่อง TODO ให้เติมเอง:
ate_food (เทียบช่อง ไม่ใช่พิกเซล)score += 1 · game.sfx("eat") · score_text.set(...) · ต่อปล้อง · วางอาหารใหม่snake_body.pop()ลองต่อยอด (ทำได้ก็ดี)
fps ให้เร็วขึ้น"Nice bite!" เป็นคำของตัวเองคาบหน้า: เราจะเพิ่มการชนกำแพงและชนตัวเองให้เป็น GAME OVER เกมจะเริ่ม "แพ้ได้" จริง
snake_step3.py — อ่านให้เข้าใจ แล้วปิดไฟล์ พิมพ์เองเฉลยนี้มีไว้ให้เทียบกับของน้อง ไม่ได้มีไว้ให้ลอกวางส่ง คะแนนของคาบอยู่ที่ใบงานกับการอธิบายด้วยคำพูดตัวเอง วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือ ตอนพิมพ์เองนั่นแหละที่สมองจำ pattern ได้ ต่อจากนี้เราจะแกะเฉลยทีละก้อน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ก้อนแรก — ตั้งของให้พร้อมก่อน แล้วค่อยเข้าลูป:
import bentogame as game
import random
CELL_PX = 26
GRID_COLS = game.WIDTH // CELL_PX
GRID_ROWS = game.HEIGHT // CELL_PX
game.title("SNAKE") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
snake_body = [[6, 8], [5, 8], [4, 8]]
body_squares = [game.Box(col * CELL_PX, row * CELL_PX, CELL_PX - 2, CELL_PX - 2, game.GB_DARK)
for col, row in snake_body]
body_squares[0].set_color(game.GB_LIGHT)
food_square = game.Box(0, 0, CELL_PX - 2, CELL_PX - 2, game.GB_LIGHTEST) # อาหาร = ช่องสว่างสุด
score_text = game.Text("Score: 0", 10, 8, game.WHITE)
step_col, step_row, score = 1, 0, 0
GRID_COLS = game.WIDTH // CELL_PX (:11-12) ให้ บอร์ดบอกขนาดจอเอง แล้วหารด้วยขนาดช่อง ไม่ hard-code จำนวนช่องลงไปตรง ๆ ถ้าจอคนละรุ่นกริดจะปรับตามอัตโนมัติ ตรงกับหลัก "คิดเป็นช่อง แล้วคูณตอนวาด" ที่เราคุยในสไลด์ สุ่มอาหาร (สุ่มช่องไม่ใช่พิกเซล)game.title("SNAKE") (:14) ยกงานหน้า Start/Back ให้ engine ทำให้ในตัว เราไม่ต้องเขียน state machine ของหน้าเมนูเอง นี่คือ เส้นแบ่งหน้าที่ (API) จากสไลด์ ทีมเราเขียนแค่ตรรกะfood_square = game.Box(...) (:20) สร้างกล่องอาหาร ครั้งเดียว แล้วค่อย move_to ย้ายตำแหน่งทีหลัง ไม่สร้าง object ใหม่ทุกเฟรม บน MCU ที่หน่วยความจำน้อย นิสัย "สร้างครั้งเดียว ใช้ซ้ำ" สำคัญมากstep_col, step_row, score = 1, 0, 0 (:23) รวม state ทั้งหมดของเกมไว้บรรทัดเดียว เห็นครบในที่เดียว score คือ state ใหม่ของคาบนี้ที่ Snake #2 ยังไม่มีก่อนงูขยับสักก้าว เราวางของให้ครบและรู้ตำแหน่งทุกชิ้นก่อน โปรแกรมที่ดีเริ่มจาก "สถานะที่รู้แน่" เสมอ
def place_food_at_random_empty_cell(): # หาช่องว่าง (ไม่ทับตัวงู) แล้ววางอาหารตรงนั้น
while True:
col = random.randint(0, GRID_COLS - 1) # สุ่ม "ช่อง"
row = random.randint(0, GRID_ROWS - 1)
if [col, row] not in snake_body: # ห้ามทับตัวงู
food_square.move_to(col * CELL_PX, row * CELL_PX) # ช่อง -> พิกเซล
return
while True: คู่กับ return (:27, :32) คือสำนวน rejection sampling เป๊ะ ๆ ที่เราวิเคราะห์ต้นทุนไว้ในสไลด์ ถ้ากระดานเริ่มแน่น จะสุ่มกี่ครั้ง — สุ่มมั่ว แล้ว "ทิ้ง" ตัวที่ไม่ผ่าน จนเจอช่องที่ใช้ได้ ต้นทุนเฉลี่ย E = 1/(1-f)if [col, row] not in snake_body: (:30) คือเงื่อนไข "ไม่ทับตัวงู" ถ้าขาดบรรทัดนี้ อาหารจะไปโผล่ทับตัวงูได้ (ตรงกับกับดัก อาหารทับตัวงู ในตารางก่อนหน้า)randint(0, GRID_COLS - 1)) แล้วค่อย move_to(col * CELL_PX, ...) (:31) แปลงเป็นพิกเซลตอนวาง นี่คือ coordinate transform grid → pixel เส้นเดียวกับตอนวาดตัวงูplace_food_at_random_empty_cell() หนึ่งครั้งที่ :35 ก่อน เข้าลูป เพื่อให้เฟรมแรกมีอาหารรออยู่แล้ว ไม่ใช่จอว่างสังเกตว่าเราแยก "ตรรกะเลือกช่อง" ออกเป็นฟังก์ชันชื่อยาว ๆ อ่านรู้เรื่อง พอตอนกินอาหารเราแค่เรียกซ้ำ ไม่ต้องเขียนใหม่
def on_each_frame():
global step_col, step_row, score
keys = game.keys()
if keys.left and step_col == 0: step_col, step_row = -1, 0
if keys.right and step_col == 0: step_col, step_row = 1, 0
if keys.up and step_row == 0: step_col, step_row = 0, -1
if keys.down and step_row == 0: step_col, step_row = 0, 1
# (Back=ออก / Start=เริ่มใหม่ — game.run() จัดการให้)
next_head = [snake_body[0][0] + step_col, snake_body[0][1] + step_row]
snake_body.insert(0, next_head)
# ----- เติมส่วนนี้เอง: เช็คว่าหัวงูไปอยู่ช่องเดียวกับอาหารไหม -----
ate_food = (next_head == [food_square.x // CELL_PX, food_square.y // CELL_PX])
if ate_food: # งูกินอาหารเข้า
score += 1
game.sfx("eat") # เสียงงับ มาจาก C engine จริง
score_text.set("Score: %d" % score)
body_squares.append(game.Box(0, 0, CELL_PX - 2, CELL_PX - 2, game.GB_DARK)) # โต 1 ปล้อง
game.Text("Nice bite!", 320, 180, game.GB_LIGHT)
place_food_at_random_empty_cell()
else:
snake_body.pop() # ไม่กิน -> ตัดหาง = เดินปกติ
global step_col, step_row, score (:39) บอก Python ว่าสามตัวนี้จะถูก เขียนทับของเดิม ไม่ใช่สร้างตัวใหม่ในฟังก์ชัน ขาดบรรทัดนี้แล้ว score += 1 จะพัง name 'score' is not defined (กับดักที่เราลิสต์ไว้)and step_col == 0 (:41-44) กันเลี้ยวกลับหลัง 180° ทันที — ของเดิมจาก Snake #2 ที่เอากลับมาใช้ต่อsnake_body.insert(0, next_head) (:48) เติมหัว เสมอ ก่อนถามว่ากินไหม นี่คือครึ่งซ้ายของสมการ grow = walk - cut_tail จากสไลด์ หัวใจของคาบนี้ate_food เทียบ "ช่อง" ไม่ใช่พิกเซล (:51): แปลง food_square.x // CELL_PX กลับเป็นช่องก่อนเทียบกับ next_head — unit-safety ที่เน้นในสไลด์ เช็คกิน = อยู่ช่องเดียวกันpop() (:52-58) งูจึงยาว +1 · append กล่องใหม่ให้ body_squares ยาวตามsnake_body · game.sfx("eat") (:54) คือ event → feedback ที่ engine เล่นเสียงจริงผ่านสาย I2S → codecelse: snake_body.pop() (:60) เดินปกติ ตัดหาง ยาวเท่าเดิม — สองแขนงต่างกันแค่บรรทัดเดียวนี้ทั้งฟังก์ชันเติมหัวเหมือนกันหมด ความฉลาดอยู่ที่คำถามเดียว: "หัวใหม่ไปนั่งช่องเดียวกับอาหารหรือเปล่า" ตอบ ใช่/ไม่ใช่ แล้วเลือกว่าจะตัดหางไหม
for index, cell in enumerate(snake_body):
body_squares[index].set_color(game.GB_LIGHT if index == 0 else game.GB_DARK)
body_squares[index].move_to(cell[0] * CELL_PX, cell[1] * CELL_PX)
game.run(on_each_frame, fps=9)
enumerate(snake_body) (:62-64) sync สองโลก ทุกเฟรม: snake_body (ความจริงเชิงตรรกะเป็นช่อง) กับ body_squares (กล่องที่ engine วาด) — เราแก้ list ให้ถูกก่อน แล้วค่อยไล่บอก engine ว่าแต่ละกล่องไปอยู่พิกเซลไหนgame.GB_LIGHT if index == 0 else game.GB_DARK (:63) ระบายหัว (index 0) ให้สว่างกว่าลำตัว คนเล่นจะได้รู้ว่าหัวอยู่ไหนgame.run(on_each_frame, fps=9) (:67) มอบ "การหมุนลูป" ให้ engine — เราเขียนแค่ "หนึ่งเฟรมทำอะไร" ที่ 9 fps เท่าจังหวะ Snake คลาสสิกทั้งไฟล์ไต่ระดับทีละก้อน แต่ละก้อนเพิ่มแนวคิดใหม่ หนึ่งอย่าง โดยยังยืนบนของเดิมจาก Snake #1–#2:
| ก้อนของโค้ด | บรรทัด | แนวคิดใหม่ของคาบนี้ | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| ตั้งของครั้งเดียว | :10-23 |
food_square + score state + title() |
snake_body = list ของช่อง, game.Box |
| สุ่มอาหาร | :26-35 |
rejection sampling + grid → pixel | random, ฟังก์ชันแยกงาน |
| เติมหัว | :47-48 |
next_head + insert(0) เสมอ |
list สองด้าน (deque) |
| ตัดสินใจกิน | :50-60 |
ate_food + โต (ไม่ pop) + sfx + score |
insert/pop = deque, if/else |
| วาด + รัน | :62-67 |
sync list → engine ทุกเฟรม + fps |
game.run loop |
จับจังหวะการไต่ระดับนี้ไว้ เกมทั้งเกมสร้างแบบเดียวกัน: เริ่มจากของเล็กที่สุดที่รันได้ แล้วเติมทีละแนวคิด อย่ากระโดดเขียนทั้งเกมรวดเดียว
ที่มา — ก้อน if ate_food: ... else: ... ที่เราเพิ่งแกะ ไม่ใช่ของใหม่ถอดด้ามเลย มันคือรูปพิเศษของ pattern เก่าแก่: ตรวจเงื่อนไข → เปลี่ยน state → ให้ feedback ตัวงูเองก็ยืนบน Snake #1–#2 ที่สอนว่า "งู = list ของช่อง เติมหัว/ตัดหางสองด้าน" (ก็คือ deque) ส่วน game.sfx("eat") ต่อยอดจากสไลด์ ทำไมเสียงดังจริงได้เฉพาะบนบอร์ด ที่เราไล่สาย PCM → I2S → codec ไปแล้ว วันนี้เราแค่เอาสามชิ้นนี้มาต่อกันในเฟรมเดียว
ที่ไป — ทักษะเล็ก ๆ "ตรวจว่าหัวชนอะไร แล้วตอบสนอง" นี้แหละคือเมล็ดของคาบหน้า: ate_food (เทียบว่าหัวอยู่ช่องเดียวกับเป้า) จริง ๆ แล้วคือ collision detection เปลี่ยน "อาหาร" เป็น "กำแพง" หรือ "ตัวงูเอง" มันก็กลายเป็นเงื่อนไข GAME OVER ทันที และคู่ score + sfx-on-event นี้จะโผล่ซ้ำใน Flappy (ผ่านท่อ +1), Pong, Shooter — โครงเดิมทั้งนั้น
ลองตอบสามข้อนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
ate_food จะกลายเป็นเงื่อนไข "ตาย" ได้ไหมeat" ได้ พรุ่งนี้เราจะตรวจ "หัวชนกำแพง → จบเกม + เสียง game_over" ด้วยโครงเดียวกันได้ไหม (ใบ้: แค่เปลี่ยนสิ่งที่เทียบ กับสิ่งที่ทำหลังเทียบ)game.sfx("eat") ตอนกิน กับไฟ error ที่กะพริบในเครื่องใช้ไฟฟ้า เป็นเรื่องเดียวกัน — "เกิดเหตุการณ์ขึ้น ระบบส่งสัญญาณออกมาให้คนรู้"ถ้าตอบสามข้อข้างบนได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด:
ate_foodวันนี้ไม่ใช่แค่การกินอาหาร มันคือ collision detection ที่ทั้งเครื่องเกมยืนอยู่บนมัน
เทคนิคที่เราเขียนวันนี้ ไม่มีอันไหนเป็นของสมมติเลย ทุกอันมีระบบจริงที่ทำงานด้วยหลักเดียวกัน:
:27-32) — เอนจินเกมวางไอเทม/ศัตรูไม่ให้ทับกัน · สร้าง token/รหัสห้องสุ่มแล้ว retry ถ้าชน · จองที่นั่งที่ยังว่าง ล้วนจ่ายต้นทุน E = 1/(1-f) เดียวกันเมื่อของเต็มขึ้นgame.sfx("eat") :54) — notification บนมือถือ · ปุ่มที่มีเสียง/สั่นตอบกลับ · เครื่องมือแพทย์ beep เมื่อค่าหลุดเกณฑ์ ล้วนคือ "ตรวจเจอเหตุการณ์ → ยิงสัญญาณให้รู้ทันที" แบบเดียวกับงูงับอาหารแล้วมีเสียงcol * CELL_PX :31, :64) — tilemap เกม 2D · addressing จอ LED matrix · scale UI ตาม DPI · แผนที่ GIS แบ่งโลกเป็น tile ล้วนเก็บเป็น "ช่องจำนวนเต็ม" แล้วคูณเป็นพิกัดจริงตอนแสดงผลinsert(0) / pop() :48, :60) — ring buffer เก็บค่าเซนเซอร์ N ตัวล่าสุด · undo history · คิวงานใน OS · sliding window บน stream ล้วนคือ "เติมด้านหนึ่ง เอาออกอีกด้าน" เหมือนตัวงูเกม Snake เกมเล็ก ๆ เกมเดียว ซ่อนเทคนิคที่วิศวกรจริงใช้ทำงานกันทุกวันไว้ครบสี่อย่าง เข้าใจที่นี่แล้ว เอาไปใช้ที่อื่นได้ทันที
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหางานจริงได้:
place_food วนไม่จบ — กระดานเกือบเต็มจน rejection sampling พลาดซ้ำ ๆ (E = 1/(1-f) พุ่งสูง) จะทำ "ทางออกฉุกเฉิน" ยังไง ถ้าเก็บ list ของช่องว่าง แล้วสุ่มหยิบตรง ๆ จะดีกว่าไหม แลกกับอะไรnext_head, snake_body, GRID_COLS, GRID_ROWS): จะเขียนเงื่อนไข "หัวออกนอกกระดาน" กับ "หัวชนตัวเอง" ยังไง เขียนคร่าว ๆ ลงใบงาน แล้วคาบหน้ามาเทียบกันเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากโค้ดที่พิมพ์เองวันนี้ — ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นคนออกแบบเกม ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css
CAPTURE: snake_step3_eat.png — รันบนบอร์ด งูยาว 5+ ปล้อง, Score: 3 มุมซ้ายบน, ตอนหัวกำลังจะงับช่องสว่าง
LEFT: walk = insert head + pop tail
existing body (stays)
divider
RIGHT: eat = insert head, no pop
new head fades IN, tail STAYS
API-call boundary
engine