clamp_ball_speed()ทุกครั้งที่ตีโดน เราบวกเข้า ball_vx (เร่ง + speedup) และ ball_vy (มุม + สปิน) แรลลี่ยาว ๆ ลูกจะเร็วขึ้นเรื่อย ๆ จนตีไม่ทัน เกมเต็มจึงมีเพดาน — แต่เป็นเพดานของ ขนาดความเร็วรวม ไม่ใช่ราย ๆ แกน:
def clamp_ball_speed(): # pong_full.py:46
global ball_vx, ball_vy
speed = (ball_vx * ball_vx + ball_vy * ball_vy) ** 0.5 # :49 |v| = ทฤษฎีพีทาโกรัส
if speed > BALL_MAXV: # :50 เกินเพดาน (14.0) ไหม
k = BALL_MAXV / speed # :51 อัตราส่วนที่ต้องหด
ball_vx *= k # :52 หดทั้งสองแกน...
ball_vy *= k # :53 ...ด้วย k ตัวเดียวกัน
k ตัวเดียวกัน เวกเตอร์จึงหดสั้นลงแต่ ทิศทาง (มุมเด้ง) คงเดิม — ลูกเร็วไม่เกินเพดาน แต่ยังพุ่งไปทางเดิมclamp ความเร็วไม้ทีละแกน/สเกลาร์ ถ้าเอาวิธีนั้นมาครอบ ball_vx, ball_vy แยกกัน มุมจะเพี้ยน ลูกจะเด้งผิดทางpong_full.py:114 ไม้ซ้าย · :121 ไม้ขวา) — คุมความเร็วก่อนเฟรมถัดไปนี่คือเหตุผลที่เก็บความเร็วเป็น เวกเตอร์ 2 แกน ตั้งแต่ Pong #1: การจำกัด "ความยาวเวกเตอร์" ให้อยู่ในวงกลมโดยรักษามุม คือเทคนิคเดียวกับ velocity-clamp ในเอนจินเกมจริงและการ normalize เวกเตอร์ใน DSP/กราฟิก

ที่มา: "Atari Pong arcade game cabinet" — Rob Boudon / Ubcule, CC BY 2.0, Wikimedia Commons
ตู้ Pong ของ Atari (1972) คือเกมแรก ๆ ที่คนเล่น "สู้กับเครื่อง" ได้ ตอนนั้นยังไม่มี CPU ด้วยซ้ำ ไม้ฝั่งคอมพิวเตอร์ขับด้วย วงจรลอจิกไม่กี่ตัว ที่ทำงานง่าย ๆ คือ "ลูกอยู่ตรงไหน ก็ขยับไม้ไปทางนั้น"
นั่นคือไอเดียเดียวกับที่เราจะเขียนวันนี้ ต่างกันแค่เราเขียนเป็น Python ไม่กี่บรรทัดแทนวงจร เห็นไหมว่า "AI คู่ต่อสู้" ตัวแรกของโลกเกม ก็เริ่มจากกฎง่าย ๆ แบบนี้
ครึ่งศตวรรษผ่านไป หลักคิดยังเหมือนเดิม: อ่านสถานะ → ตัดสินใจ → ขยับ สิ่งที่เราทำวันนี้คือรากเดียวกับ AI ในเกมยุคใหม่
Original Atari PONG 1972 arcade machine gameplay video — McFly · ดูไม้ฝั่งคอมพิวเตอร์ "วิ่งตามลูก" ของจริงเมื่อ 50 ปีก่อน นี่คือพฤติกรรมเดียวกับที่เราจะเขียนเป็นโค้ดวันนี้
ไม้ฝั่งขวาเป็นของคอมพิวเตอร์ มันแค่ อ่านตำแหน่งลูก แล้วขยับเข้าหา เท่านั้นเอง:
# ไม้ AI (ขวา) = วิ่งตามลูก
target_y = ball_y - PADDLE_H / 2 # อยากให้กลางไม้ตรงกับลูก
if ai_y < target_y - 5: ai_y += AI_SPEED # ลูกอยู่ล่างกว่า -> เลื่อนลง
elif ai_y > target_y + 5: ai_y -= AI_SPEED # ลูกอยู่บนกว่า -> เลื่อนขึ้น
ai_y = max(0, min(game.HEIGHT - PADDLE_H, ai_y)) # ไม่ให้หลุดจอ
ai_paddle.move_to(ai_paddle.x, ai_y)
pong_full.py:145-153: เทียบ จุดกึ่งกลาง ลูกกับไม้ (ball_c :146 · paddle_c :147), deadzone ± 5 (:148,150) และใช้ ai_speed ต่อโหมด (ไม่ใช่ค่าคงที่ตัวเดียว) — ดูไฟล์เต็มใน full_games/pong_full.py
ทำไมต้องมี - 5 กับ + 5 ในเงื่อนไข? ถ้าไม่มี ตอนลูกอยู่ตรงกลางไม้พอดี ไม้จะ
ขยับขึ้น-ลงสลับกันทุกเฟรม กลายเป็น สั่น (jitter) น่ารำคาญ (ค่าจริงในเกมเต็ม pong_full.py:148,150)

ซ้าย: ไม่มี deadzone → ไม้เด้งขึ้น-ลงทุกเฟรม (jitter) · ขวา: มี ±5 → ลูกอยู่ในแถบ ไม้นิ่งสนิท
เลข
±5คือ "เขตยอมรับ (deadzone)" ลูกอยู่ในช่วงนี้ ถือว่าตรงกันแล้ว ไม้อยู่นิ่ง ไม่ต้องไล่ตาม
ถ้าเอาเงื่อนไข if/elif สามบรรทัดมาวาดเป็นกราฟ จะเห็นรูปร่างชัดขึ้น แกนนอนคือ ระยะคลาด (ลูกห่างจากกลางไม้เท่าไร) แกนตั้งคือ คำสั่งขยับ ที่ไม้ได้รับ:

ช่วงราบกลางกราฟ (สีเขียว) คือ deadzone ระยะคลาดเล็ก ๆ ถูก "กลืน" เป็นศูนย์ ไม้เลยไม่กระตุกไป-มา นี่คือเทคนิคเดียวกับที่ใช้กรองสัญญาณก้านจอย (analog stick) ไม่ให้สั่นตอนปล่อยมือ

ลูกวิ่งเหมือนกันทั้ง 3 จอ — 2.0 ตามไม่ทัน (ไม้ห่างลูกมาก) · 4.2 ตามติดสูสี · 8.0 แทบเกาะลูกตลอด
อยากให้น้อง ๆ ลองเล่นจริง: เปลี่ยนค่า
AI_SPEEDแล้วจะรู้สึกถึงความต่างได้ทันที ตรงนี้แหละคือหัวใจของคาบนี้
เกมเต็มในfull_games/pong_full.pyไม่ได้ใช้เลขตัวเดียว แต่ตั้งไว้ 3 โหมด ที่MODES(:33): EASY = 2.2 · NORMAL = 3.2 · HARD = 4.6 (เปิดมาที่ NORMAL ก่อน) — ค่าai_speedถูกดึงตามโหมดที่เลือกที่:167
สิ่งที่เราทำวันนี้คือบันได AI ขั้นที่ 1 (Tracker) ขั้นถัดไป (เลือกโหมด EASY/HARD, ทำให้พลาดบ้าง) ก็เสียบเข้าที่จุดเดียวกันนี้
ดูการทำงานจริงของกฎสามบรรทัด ลูกวิ่งไปไหน ไม้ AI (ขวา) ก็ไล่ตาม ball_y เส้นประคือระยะที่มันพยายามไล่ให้ตรง:

AI_SPEED พิกเซลต่อเฟรม นี่แหละที่ทำให้เรายังพอตีชนะมันได้เกมไม่ได้มีแค่ "เล่น" มันมีหลายช่วง เราใช้แนวคิด state machine: เกมอยู่ได้ครั้งละ 1 สถานะ และมี "ทางออก" ไปสถานะถัดไป
ในเกมเล็กของเรา PLAY คือ
on_each_frame()ที่วนทุกเฟรม ส่วน OVER เกิดเมื่อเราreturn False(เกมหยุด) และกด START เพื่อออกก็คือทางออกของ PLAY เช่นกัน
elif ball_vx > 0 and game.hit(ball, ai_paddle): # ลูกไปขวา + ชนไม้ AI
ball_vx = -min(ball_vx + SPEEDUP, BALL_CAP) # กลับทิศไปซ้าย (ติดลบ)
# --- นับคะแนน + เสิร์ฟใหม่ ---
if ball_x < 0: # หลุดซ้าย -> AI ได้แต้ม
ai_score += 1
score_text.set("%d : %d" % (player_score, ai_score)); serve(1)
elif ball_x > game.WIDTH: # หลุดขวา -> เราได้แต้ม
player_score += 1
score_text.set("%d : %d" % (player_score, ai_score)); serve(-1)
# --- จบเกม (เข้าสถานะ OVER) ---
if player_score >= WIN_SCORE or ai_score >= WIN_SCORE:
player_won = player_score > ai_score
game.Text("YOU WIN!" if player_won else "AI WINS", 320, 180,
game.GREEN if player_won else game.RED)
return False
pong_full.py:128-143: เกมเต็มปล่อยให้ลูก "เลยขอบไป 25px" ก่อนค่อยนับ (ball_x < -25 / > WIDTH+25 :128,136), ใช้ชื่อ left_score/right_score, เพดานชนะ win_score ต่อโหมด (EASY 7 · NORMAL/HARD 10, :33) และเช็กชนะทันทีในบล็อกเดียวกัน (:131,139 → return False)
serve(direction)(pong_full.py:36-43) ย้ายลูกกลับ กลางจอ (:39-40) แล้วตั้งความเร็วใหม่ทันที จุดสำคัญคือ:43—ball_vy = random.randint(-22, 22) / 10.0— สุ่มมุมเสิร์ฟ เล็กน้อย (±2.2 px/เฟรม) เลียนแบบlv_randของเกม C ทำให้ทุกครั้งที่เสิร์ฟลูกออกมุมไม่ซ้ำเดิม เกมจึงไม่กลายเป็นแพตเทิร์นเดียวซ้ำ ๆ

เราเพิ่มแต้มตามฝั่งที่ลูกหลุดออกไป แล้วจบเกมเมื่อมีฝ่ายถึงเพดานคะแนน:
MODES :33): EASY = 7, NORMAL/HARD = 10 — unpack ที่ :167core 70% เขียนลูปไว้ให้แล้ว เราเติมแค่บล็อก 30% เข้าไปใน
on_each_frame()
update() ของ pong_full.py หนึ่งเฟรมรวมทุกอย่างของคาบเป็น ผังควบคุมทั้งโปรแกรม แบ่งอ่านเป็นสองท่อน (บน = ฟิสิกส์+การชน · ล่าง = คะแนน+AI+วนแมตช์) สัญกรณ์เดียวกับคาบ 6: ◇ ม่วง = จุดตัดสินใจ, เขียว = I/O, ฟ้า/ส้ม = ประมวลผล, เทา = เริ่ม/จบ, เส้นประ = วนกลับ
อ่านผังนี้แล้วจะเห็นว่า "เกมจบ" ไม่ใช่จุดพิเศษ มันคือแค่ ◇
ถึงเพดาน?ที่ตอบ Yes แล้วreturn Falseออกจากลูป — ส่วน ◇ลูกหลุดขอบ?ที่ตอบ No คือ 99% ของเฟรมที่เกมยังวิ่งปกติ
ก่อนเปิด pong_full.py มาดูภาพรวมกันก่อนว่าเกมเต็มไม่ได้โผล่มาทีเดียว มันไต่ขึ้นมาทีละขั้น แต่ละขั้นเพิ่ม ไอเดียใหม่แค่อย่างเดียว ต่อยอดจากขั้นก่อน:
| ขั้น | ไอเดียใหม่หนึ่งอย่าง | ไฟล์ · บรรทัด |
|---|---|---|
| step3 | ตีลูกกลับเมื่อชนไม้ (collision + reflect) | pong_step3.py (คาบ 6) |
| step4 | คู่ต่อสู้ AI วิ่งตามลูก + นับคะแนน + first-to-N | pong_step4.py:67-73 (AI ตามลูก, AI_SPEED=4.2 :30) · :97-106 (คะแนน + serve() :47-50) · :108-115 (จบเกม, WIN_SCORE=7 :31) |
| step5 | เมนูโหมด EASY/NORMAL/HARD + AI tracker ปรับตามโหมด | pong_step5.py:36-39 (MODES) · :94-104 (เมนูเลือกโหมด) · :121-127 (AI ใช้ ai_speed ต่อโหมด) |
| เกมเต็ม | รวมทั้งหมด + สุ่มมุมเสิร์ฟ + speed-clamp เวกเตอร์ | pong_full.py:33 (MODES) · :43 (สุ่มเสิร์ฟ) · :46-53 (clamp_ball_speed) |
pong_step4.py:67-73), นับคะแนน (:97-106) และเพดานชนะ (:108-115) เกมก็ "จบได้" มีแพ้-มีชนะจริง ๆ ไม้ขวาเป็น AI ไม่ใช่ผู้เล่นคนที่สอง เพราะ C track รวมปุ่ม A/B/X/RB เป็น .action เดียว เล่น 2 คน mirror ข้าม track ไม่ได้ (เหตุผลเต็มในหัวไฟล์ :15-18) — ขั้นนี้คือหมุดที่พิสูจน์ว่าเกม Python ของเรา parity กับ C step4 บนจอMODES :36-39) แล้วให้ AI ดึงความเร็ว ai_speed ตามโหมด (:121-127) ตาราง MODES ชุดนี้ เหมือน pong_full.py:33 เป๊ะ — step5 จึงเป็นบันไดขั้นก่อน pong_full.py โดยตรง เหลือแค่การขัดเงา (สุ่มมุมเสิร์ฟ :43, speed-clamp เวกเตอร์ :46-53, chrome ของจอ) ที่เกมเต็มเติมเข้าไป
เปิดอ่านเฉลยสองขั้นนี้เอง:
solution_codes/pong_step4.pyและsolution_codes/pong_step5.pyคือ answer key ของขั้น step4/step5 อ่านทีละบรรทัดเทียบกับตารางด้านบน แล้วจะเห็นว่าpong_full.pyในสไลด์ถัดไปคือ step5 + การขัดเงา ไม่ใช่ของใหม่ทั้งก้อน
full_games/pong_full.py ใน BENTO IDE
ตีโต้ครบรอบ → ได้แต้มชี้ขาด → ขึ้นหน้า YOU WIN! (ตรงกับขั้นตอน 4–5)

หน้าจบเกมจริงบนบอร์ด BENTO
ถ้าทำถึงตรงนี้ได้ ก็ผ่าน MVP-1 แล้ว Pong ของเราเล่นจบเกมได้ และมีคู่ต่อสู้ AI
ไม้ AI ที่ดูเหมือนเล่น ๆ จริง ๆ แล้วคือ วงจรป้อนกลับ (feedback loop) หัวใจของงานควบคุมฝังตัวทั้งหมด:

ฝั่ง Embedded / ควบคุม
ฝั่ง Python / Algorithm
if/elif/else คือ decision rule พื้นฐานของ AI ทุกตัว เริ่มจากตรงนี้แล้วค่อยซับซ้อนขึ้นai_y ตาม ball_y คือคณิตพิกัดล้วน ๆHow to make a simple Game AI for Pong — Dan Zaidan · แนวคิด "AI = กฎไม่กี่ข้อที่ไล่ตามลูก" ไม่ผูกกับภาษาใด ตรงกับที่เราทำวันนี้
| อาการ | สาเหตุ | แก้ |
|---|---|---|
| ลูกติดไม้ สั่น / ตีซ้ำ | ลืมเช็กทิศ ball_vx<0/>0 ก่อน hit |
เช็กทิศ ก่อน game.hit เสมอ |
| ลูกเร็วจนตีไม่ทัน | ลืม BALL_CAP |
ครอบด้วย min(..., BALL_CAP) |
| AI สั่นกระตุกกลางจอ | ไม่มี deadzone | ใส่ - 5 / + 5 ในเงื่อนไข |
| AI เก่งจนชนะไม่ได้ | AI_SPEED สูงไป |
ลดเหลือ 3.0–4.5 |
| คะแนนขึ้นทีละหลายแต้ม | serve() ไม่ย้ายลูกกลับกลางทันที |
เรียก serve() ทันทีหลังนับแต้ม |
| จอยกดไม่ติดหลังโหลดใหม่ | บอร์ดรีเซ็ต | ถอด-เสียบสาย USB จอยใหม่ |

แถวแรกของตาราง — ซ้าย: ลืมเช็กทิศ ลูกติดไม้ "สั่น" ตีซ้ำทุกเฟรม · ขวา: เช็ก ball_vx<0 ก่อน hit แล้วลูกเด้งออกสะอาด
วันนี้เราได้:
ball_y แล้วขยับ ai_y ตาม (กฎไม่กี่บรรทัด)AI_SPEED ให้เกมสูสี
practise_codes/pong_step3.py↗ เติมส่วนตีลูกกลับให้ครบ
(ถ้าติด ยกมือถามได้เลย เดี๋ยวเฉลยในห้อง) แล้วลองต่อยอด:AI_SPEED 3 ค่า แล้วบอกว่าค่าไหนสนุกสุด เพราะอะไรfull_games/pong_full.py
ส่งงาน: อัปโหลดโค้ด + แนบรูปจอตอนคะแนนใกล้จบ (เช่น
6:5) พร้อมเขียนอธิบายสั้น ๆ 1 ประโยคว่า "AI ของเราตัดสินใจอย่างไร"
pong_step4.py — อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เองเฉลยสองไฟล์ในคาบนี้ (solution_codes/pong_step4.py และ pong_step5.py) มีไว้ เทียบ ไม่ใช่ลอก คะแนนอยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์กลับด้วยมือตัวเอง ตอนพิมพ์เองนั่นแหละที่สมองจำ pattern ได้ ต่อไปเราจะแกะทีละส่วน เพราะทุกบรรทัดมีเหตุผล เริ่มจาก ค่าคงที่กับตัวช่วยเสิร์ฟ ก่อน:
ACCEL, MAX_SPEED, FRICTION = 1.5, 12.0, 0.78
SPEEDUP, BALL_CAP, SPIN = 0.35, 14.0, 0.28
SERVE_SPEED = 6.2
AI_SPEED = 4.2 # ความเร็วที่ไม้ AI วิ่งตามลูก
WIN_SCORE = 7 # first-to-N
def serve(direction):
global ball_x, ball_y, ball_vx, ball_vy
ball_x, ball_y = game.WIDTH / 2, game.HEIGHT / 2
ball_vx, ball_vy = SERVE_SPEED * direction, 3.4
AI_SPEED กับ WIN_SCORE ถูกยกขึ้นเป็นค่าคงที่ตั้งชื่อบนสุด (:30-31) ไม่ฝังเลข 4.2/7 ลงกลางลูป นี่คือบทเรียนจากสไลด์ "AI_SPEED — เลขตัวเดียวที่เปลี่ยนความฉลาด": อยากปรับความยากก็แก้จุดเดียว ไม่ต้องไล่หาทั้งไฟล์serve(direction) รับ ทิศ เป็นพารามิเตอร์ +1/-1 (:47-50) จุดเดียวคุมทั้งเสิร์ฟไปซ้ายและไปขวา ตรงกับสไลด์ "นับคะแนน + เสิร์ฟใหม่" ที่บอกว่าลูกหลุดข้างไหนก็เสิร์ฟกลับด้านนั้น เขียนฟังก์ชันเดียวใช้ซ้ำ ดีกว่าก๊อปโค้ดเสิร์ฟสองชุดglobal ในบรรทัด :48 จำเป็นเพราะ serve ต้อง เขียนทับ ตัวแปรระดับโมดูล ถ้าลืมบรรทัดนี้ Python จะสร้างตัวแปรใหม่ในฟังก์ชันแล้วลูกไม่ขยับจริง เป็นกับดักที่เด็กเจอบ่อยตอนแยกโค้ดออกเป็นฟังก์ชันครั้งแรกก่อนดูโค้ด AI สังเกตนิสัยนี้ไว้ก่อน: เลขที่เราอยากปรับบ่อยให้ตั้งชื่อไว้บนสุด งานที่ทำซ้ำให้ห่อเป็นฟังก์ชัน สองอย่างนี้จะกลับมาช่วยเราทุกคาบที่เหลือ
บล็อกที่เป็นงาน 30% ของน้องจริง ๆ คือตรงนี้ ไม้ฝั่งขวาเป็นของคอมพิวเตอร์ มันแค่ อ่าน ball_y แล้วก้าวเข้าหา เท่านั้น ไม่มีอะไรลึกลับกว่านี้:
# ----- เติมส่วนนี้เอง: ไม้ AI (ขวา) = วิ่งตามลูก -----
# อยากให้กลางไม้ตรงกับลูก -> ลูกอยู่ล่างก็ลง, อยู่บนก็ขึ้น
target_y = ball_y - PADDLE_H / 2
if ai_y < target_y - 6: ai_y += AI_SPEED
elif ai_y > target_y + 6: ai_y -= AI_SPEED
ai_y = max(0, min(game.HEIGHT - PADDLE_H, ai_y))
ai_paddle.move_to(ai_paddle.x, ai_y)
target_y = ball_y - PADDLE_H / 2 (:69) คือจุดที่ทำให้ กลางไม้ ตรงกับลูก ไม่ใช่ขอบบนไม้ ตรงกับสไลด์ "AI Tracker: กฎไม่กี่บรรทัด" — เราเล็งที่ศูนย์กลาง ไม้เลยรับลูกได้ทั้งใบไม่ใช่แค่ครึ่งบนif/elif สองบรรทัด (:70-71) คือ decision rule ล้วน ๆ: ลูกอยู่ล่างก็ += AI_SPEED อยู่บนก็ -= AI_SPEED นี่คือรูป sense → decide → act ที่สไลด์ "เชื่อมโยงรากฐาน" พูดถึง เขียนเป็น Python สามบรรทัดแทนวงจรลอจิกของตู้ Pong ปี 1972- 6 / + 6 คือ deadzone จากสไลด์ "deadzone ±5 — กันไม้สั่น" สังเกตว่าเฉลยจริงใช้ ±6 (สไลด์สอนใช้ ±5 เพื่อให้เลขกลม) ค่านี้ ปรับได้ ไม่ใช่ตรรกะ กว้างขึ้น = ไม้นิ่งขึ้นแต่เฉื่อย แคบลง = ไวขึ้นแต่เสี่ยงสั่นmax(0, min(...)) (:72) หนีบไม้ไม่ให้หลุดจอ เป็น clamp แบบสเกลาร์ทีละแกน ต่างจาก clamp_ball_speed() ของลูกบอลที่ต้องหนีบ "ความยาวเวกเตอร์" — ไม้ขยับแกนเดียวจึงหนีบตรง ๆ ได้อยากให้จำภาพนี้ไว้: "AI" ตัวแรกของน้องคือ
ifสามบรรทัดที่อ่านสถานะแล้วเลือกทิศ ทุกอย่างที่ซับซ้อนกว่านี้ ก็ต่อยอดจากโครงเดียวกันนี้ทั้งนั้น
พอมีคู่ต่อสู้ตีโต้ได้แล้ว เกมถึงจะ "จบได้" เราต้องรู้ว่าลูกหลุดข้างไหน ใครได้แต้ม แล้วเมื่อไรควรหยุด บล็อกนี้คือส่วนที่ทำให้ Pong กลายเป็น เกมจริง:
if ball_x < 0: # หลุดซ้าย -> ฝั่งขวา (AI) ได้แต้ม
ai_score += 1
score_text.set("%d : %d" % (player_score, ai_score))
serve(1)
elif ball_x > game.WIDTH: # หลุดขวา -> ผู้เล่น (ซ้าย) ได้แต้ม
player_score += 1
score_text.set("%d : %d" % (player_score, ai_score))
serve(-1)
if player_score >= WIN_SCORE or ai_score >= WIN_SCORE:
player_won = player_score > ai_score
game.sfx("win" if player_won else "lose")
game.Text("YOU WIN!" if player_won else "AI WINS",
game.WIDTH // 2 - 70, game.HEIGHT // 2,
game.GREEN if player_won else game.RED)
game_over = True
return False
serve(1) หลุดขวาเรียก serve(-1) (:102,106) — ทิศเสิร์ฟตรงข้ามกับข้างที่หลุด ลูกจึงพุ่งเข้าหาคนที่เพิ่งเสียแต้ม เป็นกติกาปิงปองจริง นี่คือเหตุผลที่เมื่อกี้เราออกแบบ serve(direction) ให้รับทิศscore_text.set(...) อัปเดตป้ายคะแนนทันทีทุกครั้งที่แต้มเปลี่ยน (:101,105) ป้ายบนจอคือช่องบอกผลให้คนเล่นรู้ว่าเกมเดินถึงไหน ถ้าลืมเรียก คะแนนในหัวเปลี่ยนแต่จอค้าง คนเล่นงงgame_over = True; return False (:114-115) คือ ทางออกของสถานะ PLAY ไปสู่ OVER ตรงกับสไลด์ "state machine — เกมอยู่ช่วงไหน": return False บอก game.run ให้หยุดวนลูป เกมจึงค้างที่หน้า YOU WIN! ไม่วิ่งต่อgame.sfx("win" if player_won else "lose") (:110) เลือกเสียงตามผล เสียงคืออีกช่องบอกผลนอกจากตัวหนังสือ — น้องจะได้ยินก่อนอ่านทันด้วยซ้ำ (เสียงชุดนี้เราจะลงลึกในคาบหน้า)เกม "จบ" ไม่ใช่จุดวิเศษ มันคือแค่เงื่อนไข
>= WIN_SCOREที่เป็นจริง แล้วเราreturn Falseออกจากลูป จำโครงนี้ไว้ เกมอื่นทุกเกมก็ปิดด้วยวิธีเดียวกัน
step4 ให้ AI เก่งระดับเดียว (AI_SPEED ตัวเดียว) step5 ต่อยอด จุดเดียว: เปลี่ยนเลขคงที่เป็นตารางโหมด แล้วให้กฎ AI เดิมดึงค่าตามโหมดที่เลือก โครงโค้ดไม่เปลี่ยนเลย เปลี่ยนแค่ "ค่ามาจากไหน":
MODES = [
("EASY", 2.2, 0.85, 7),
("NORMAL", 3.2, 1.00, 10),
("HARD", 4.6, 1.20, 10),
]
target_y = ball_y - PADDLE_H / 2 # อยากให้กลางไม้ตรงกับลูก
if ai_y < target_y - 6: ai_y += ai_speed # ลูกอยู่ล่างกว่า -> ขยับลง
elif ai_y > target_y + 6: ai_y -= ai_speed # ลูกอยู่บนกว่า -> ขยับขึ้น
| ชิ้นส่วน | step4 (pong_step4.py) |
step5 (pong_step5.py) |
ต่อยอดเป็น |
|---|---|---|---|
| ความเก่ง AI | AI_SPEED = 4.2 คงที่ (:30) |
ai_speed ต่อโหมด 2.2 / 3.2 / 4.6 (:37-39) |
เลือกความยากได้ก่อนเริ่ม |
| เพดานชนะ | WIN_SCORE = 7 คงที่ (:31) |
win_score ต่อโหมด 7 / 10 / 10 (:37-39) |
สั้น-ยาวตามโหมด |
| ความเร็วลูก | SERVE_SPEED เดียว (:29) |
คูณ ball_mul 0.85 / 1.00 / 1.20 (:37-39) |
โหมดยากลูกไวขึ้น |
| กฎ tracker | if/elif ... AI_SPEED (:70-71) |
if/elif ... ai_speed (:124-125) |
โครงเดิม ค่ามาจากตาราง |
MODES[selected_mode] การแยก "ค่า" ออกจาก "โครง" คือบันไดขั้นเดียวที่ทำให้เกมปรับความยากได้ โดยไม่ต้องเขียน AI ใหม่ถ้าเห็นว่า step5 คือ step4 ที่ "ย้ายเลขไปไว้ในตาราง" น้องก็จับหลักการออกแบบที่ใช้ได้ทั้งชีวิตแล้ว: ทำให้ของทำงานก่อน แล้วค่อยดึงเลขออกมาเป็นพารามิเตอร์ทีหลัง
หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องโค้ด แต่เรื่องมองให้ทะลุว่า if สามบรรทัดที่เราเพิ่งเขียน จริง ๆ แล้วมันคือชิ้นส่วนของอะไร
ที่มา — กฎนี้ยืนอยู่บนอะไร ย้อนไปสไลด์ "ที่มาของคู่ต่อสู้คอมพิวเตอร์ — Pong ปี 1972" ตู้ Atari ขับไม้ฝั่งคอมด้วยวงจรลอจิกไม่กี่ตัว ตรรกะคือ "ลูกอยู่ตรงไหน ขยับไม้ไปทางนั้น" ทีนี้เปิดกฎ AI ที่เราเขียน (pong_step4.py:70-71) ดูอีกที มันคือประโยคเดียวกันเป๊ะ ต่างแค่สมัยก่อนต่อสาย วันนี้เราพิมพ์ Python และสไลด์ "AI Tracker: กฎไม่กี่บรรทัด" ก็บอกไว้แล้วว่า AI ไม่ใช่ของลึกลับ มันคือ sense → decide → act ที่วนทุกเฟรม เหมือน feedback loop ในสไลด์ "เชื่อมโยงรากฐาน"
ที่ไป — กฎนี้จะโตเป็นอะไร วันนี้ไม้ AI อ่าน ball_y แล้วตัดสินใจ ในคาบ 9 "เซนเซอร์: เอียงบอร์ดเล่นเกม" เราจะเปลี่ยน "ตัวที่อ่าน" จาก ball_y เป็น ค่าจากเซนเซอร์วัดการเอียง แต่โครง sense → decide → act ยังเหมือนเดิมทุกบรรทัด ไม้ที่ไล่ลูกวันนี้ คือศัตรูที่ไล่ผู้เล่นในเกมถัดไป และคือระบบควบคุมที่อ่านค่าจริงจากโลกในโปรเจกต์จบ
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
deadzone ±6 ที่กันไม้ไม่ให้สั่นได้ไหม มันคือเรื่องเดียวกับการกรอง noise ของก้านจอย (analog stick) ตอนปล่อยมือหรือเปล่า? (ใบ้: ค่าคลาดเล็ก ๆ ถูกกลืนเป็นศูนย์เหมือนกัน)ball_y เป็น player_y?target_y = ball_y - ... แล้วขยับ ai_y เข้าหา คือคณิต พิกัด ล้วน ๆ เรื่องเดียวกับที่เราจะใช้ให้เซนเซอร์เอียงคุมตำแหน่งของบนจอในคาบ 9ถ้าตอบได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด: ไม้ AI ที่ดูเล่น ๆ วันนี้ คือเมล็ดของทุกระบบที่ "อ่านโลกแล้วตัดสินใจ" ที่เราจะปลูกต่อทั้งคอร์ส
กฎ AI สามบรรทัดกับของที่ห้อมล้อมมันในคาบนี้ ไม่ใช่ของสมมติ มันคือหลักที่ระบบจริงใช้กันทุกวัน:
±6 ของเรา ถ้าไม่มี คอมเพรสเซอร์เปิด-ปิดถี่ ๆ (สั่น) เหมือนไม้ไม่มี deadzonetarget_y = ... แล้วขยับเข้าหาทีละก้าว ต่างจากไม้เราแค่หมุนเป็นองศาแทนพิกเซลreturn Falseสี่อย่างที่เราแตะวันนี้ ไม่มีอันไหนเป็นของเล่น เทอร์โมสตัทในบ้าน กล้องในมือถือ ศัตรูในเกมโปรด ทำงานด้วยหลักเดียวกับไม้ AI ที่น้องเพิ่งเขียน
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาของจริงได้:
ball_y ปัจจุบัน จึงตามหลังลูกเสมอ ถ้าให้มัน เดา ว่าลูกจะไปถึงขอบขวาที่ตำแหน่งไหนแล้วรอไว้ก่อน จะคำนวณยังไง? (นี่คือคำถามเดียวกับระบบนำวิถีและรถอัตโนมัติที่ต้องเล็งจุดในอนาคต)player_score กับ ai_score แล้วค่อย ๆ เร่ง/ลด ai_speed ให้สูสีตลอด (rubber-band) จะออกแบบเงื่อนไขยังไงให้ไม่โกงจนคนเล่นจับได้?game.sfx("win") / game.sfx("lose") ตอนจบเกม ถ้าอยากให้ เสียง สื่ออารมณ์มากขึ้น เช่น แรลลี่ยิ่งยาวเสียงตีลูกยิ่งสูงขึ้น จะออกแบบยังไง? คาบหน้า "เสียงเกม: sfx + tone" จะลงลึกเรื่องนี้เลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากกฎ AI ที่พิมพ์เองวันนี้ ตรงนั้นแหละคือจุดที่น้องเริ่มออกแบบ AI เป็น ไม่ใช่แค่พิมพ์ตามเฉลย
fit-css
Marp deck สำหรับคาบ 7 — Fundamental of Embedded Systems Developer I (MicroPython-only, 14 คาบ). Render: npx @marp-team/marp-cli session-07.md -o session-07.html ต่อจากคาบก่อนหน้า (Pong: เด้งกำแพง + ไม้ขยับ + ตีลูกกลับ). คาบนี้: AI tracker ตัวแรก + state machine + คะแนน -> MVP-1 Pong เล่นจบเกมได้. โค้ดที่อ้าง: practise_codes/pong_step3.py (เด็กเติม), full_games/pong_full.py (เกมเต็ม MVP-1).
ภาพปลายทาง: Pong จริงบนบอร์ด BENTO เล่นกับ AI ได้