คาบ 7 — Pong #3: AI คู่ต่อสู้ตัวแรก

ไม้ AI ตามลูก + state machine + คะแนน

คาถาประจำคาบ: "AI ไม่ใช่ของลึกลับ มันคือ 'กฎไม่กี่บรรทัด' ที่อ่านสถานะแล้วตัดสินใจว่าจะขยับไปทางไหน"

MVP-1 — Pong เล่นจบเกมได้เต็ม + มีคู่ต่อสู้ AI · 3 คน / 1 บอร์ด

เป้าหมายคาบนี้ — จาก "ตีลูกกลับ" สู่ "เล่นจบเกมได้จริง"

มีแล้วจากคาบก่อน
ลูกบอลวิ่ง + เด้งกำแพง
ไม้เราขยับมีน้ำหนัก
ไม้เรา "ตีลูกกลับ" ได้
เพิ่มในคาบ 7
ไม้ฝั่งขวา = AI วิ่งตามลูก
นับคะแนน + เสิร์ฟใหม่
state machine: start / play / over
ใครถึงเพดานก่อน ชนะ

ปลายทางที่เราจะไปให้ถึง — Pong จริงบนบอร์ด BENTO เล่นกับ AI จนจบเกม ขึ้น "YOU WIN!"

ผลที่อยากเห็น: บอร์ดเล่น Pong กับคอมพิวเตอร์ได้ครบรอบ คะแนนเดินจาก 0:0 ขึ้นไปเรื่อย ๆ แล้วจบด้วยหน้า YOU WIN! / AI WINS นี่คือ MVP-1 ของเรา

ปูพื้น — ของที่เราต่อยอด (ไม่ต้องรื้อใหม่)

ลูกบอลของเราเป็น เวกเตอร์ ที่มีความเร็ว 2 แกน เด้งกำแพงบน-ล่างด้วยการกลับเครื่องหมาย:

ball_x+=ball_vx,ball_y+=ball_vy\text{ball\_x} \mathrel{+}= \text{ball\_vx}, \qquad \text{ball\_y} \mathrel{+}= \text{ball\_vy}

เด้งกำแพงบน-ล่าง:ball_vy=ball_vy\text{เด้งกำแพงบน-ล่าง:}\quad \text{ball\_vy} = -\,\text{ball\_vy}

ของที่ทำในไฟล์ pong_step3.py คือ "ตีลูกกลับ" เมื่อชนไม้เรา (สะท้อนแกน X + เร่ง + มุม + สปิน)
คาบนี้เราจะต่อยอดด้วยแนวคิดใหม่ 3 อย่าง:
1. AI Tracker — กฎไม่กี่บรรทัด อ่าน ball_y แล้วสั่งไม้ขวาขยับ
2. คะแนน + เสิร์ฟใหม่ — ลูกหลุดข้าง = อีกฝั่งได้แต้ม แล้วเริ่มกลางจอ
3. State machine — start / play / over ควบคุมว่าเกมอยู่ช่วงไหน

ทบทวน — pong_step3.py: ไม้เราตีลูกกลับ

ไฟล์ practise_codes/pong_step3.py เป็นจุดเริ่มของคาบ ไม้เรา "ตีโดน" ลูกแล้วลูกเด้งกลับ:

# --- ตีกลับเมื่อชนไม้เรา (ลูกกำลังวิ่งเข้าหาไม้: ball_vx < 0) ---
if ball_vx < 0 and game.hit(ball, player_paddle):
    # hit_offset: โดนบน/กลาง/ล่างของไม้ = -1..1 (ใช้กำหนดมุมเด้ง)
    hit_offset = ((ball_y + BALL_SIZE / 2) - (player_y + PADDLE_H / 2)) / (PADDLE_H / 2)
    ball_vx = min(-ball_vx + SPEEDUP, BALL_CAP)   # กลับทิศ + เร่ง (มีเพดาน)
    ball_vy += hit_offset * 2.0                   # มุมตามจุดสัมผัส
    ball_vy += player_speed * SPIN                # สปินจากไม้ที่กำลังเคลื่อน
    game.sfx("paddle")
  • ball_vx < 0 ให้เช็ก ทิศทางก่อนเสมอ กันลูกที่เพิ่งเด้งออกไปแล้วโดนตีซ้ำ
  • game.hit(...) คือของที่ระบบเตรียมให้ เช็กว่ากล่องสองใบซ้อนทับกันไหม (หลักการ AABB)
  • min(..., BALL_CAP) คือเพดานความเร็ว ถ้าไม่ใส่ ลูกจะเร็วขึ้นเรื่อย ๆ จนตีไม่ทัน

เสียงในคาบนี้ — Pong คาบนี้เล่นเสียง paddle ตอนตีลูกกลับ, score ตอนได้แต้ม, และ win ตอนขึ้น YOU WIN! (กดเล่นฟังได้จริง):

pong_paddle

pong_score

pong_win

ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer

contact angle — โดนคนละจุด เด้งคนละมุม

เราวัดว่าลูกโดนไม้ตรงไหน บน/กลาง/ล่าง ออกมาเป็นค่า 1+1-1 \dots +1:

hit_offset=(ball_center_y)(paddle_center_y)PADDLE_H/2\text{hit\_offset} = \frac{(\text{ball\_center\_y}) - (\text{paddle\_center\_y})}{\text{PADDLE\_H} / 2}

โดนบน → -1 (เด้งขึ้นชัน) โดนกลาง → 0 (เด้งตรง) โดนล่าง → +1 (เด้งลงชัน)

นำ hit_offset ไปบวกเข้ากับ ball_vy โดนกลางไม้ลูกจะเด้งตรง โดนปลายไม้ลูกจะเด้งชัน เหมือนฟิสิกส์ปิงปองจริง:

ball_vy+=hit_offset×2.0\text{ball\_vy} \mathrel{+}= \text{hit\_offset} \times 2.0

โดนบน/กลาง/ล่างของไม้ → เด้งออกคนละมุม ค่า hit_offset เป็นตัวกำหนด

เพดานความเร็ว "แบบเวกเตอร์" — clamp_ball_speed()

ทุกครั้งที่ตีโดน เราบวกเข้า ball_vx (เร่ง + speedup) และ ball_vy (มุม + สปิน) แรลลี่ยาว ๆ ลูกจะเร็วขึ้นเรื่อย ๆ จนตีไม่ทัน เกมเต็มจึงมีเพดาน — แต่เป็นเพดานของ ขนาดความเร็วรวม ไม่ใช่ราย ๆ แกน:

v=vx2+vy2ถ้า v>BALL_MAXVvx,vy×=BALL_MAXVv|v| = \sqrt{v_x^{\,2} + v_y^{\,2}} \qquad \text{ถ้า } |v| > \text{BALL\_MAXV} \Rightarrow v_x,\,v_y \mathrel{\times}= \frac{\text{BALL\_MAXV}}{|v|}

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 ตัวเดียวกัน
วงกลม |v| = BALL_MAXV |v|=16 (เกิน) ×k → |v|=14 มุมเท่าเดิม
  • คูณ ทั้งสองแกนด้วย k ตัวเดียวกัน เวกเตอร์จึงหดสั้นลงแต่ ทิศทาง (มุมเด้ง) คงเดิม — ลูกเร็วไม่เกินเพดาน แต่ยังพุ่งไปทางเดิม
  • ต่างจากคาบ 6 ที่ clamp ความเร็วไม้ทีละแกน/สเกลาร์ ถ้าเอาวิธีนั้นมาครอบ ball_vx, ball_vy แยกกัน มุมจะเพี้ยน ลูกจะเด้งผิดทาง
  • เรียกทันทีหลังคำนวณการชน (pong_full.py:114 ไม้ซ้าย · :121 ไม้ขวา) — คุมความเร็วก่อนเฟรมถัดไป

นี่คือเหตุผลที่เก็บความเร็วเป็น เวกเตอร์ 2 แกน ตั้งแต่ Pong #1: การจำกัด "ความยาวเวกเตอร์" ให้อยู่ในวงกลมโดยรักษามุม คือเทคนิคเดียวกับ velocity-clamp ในเอนจินเกมจริงและการ normalize เวกเตอร์ใน DSP/กราฟิก

ที่มาของ "คู่ต่อสู้คอมพิวเตอร์" — Pong ปี 1972

ที่มา: "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 Tracker: "กฎไม่กี่บรรทัด" (หัวใจของคาบนี้)

ไม้ฝั่งขวาเป็นของคอมพิวเตอร์ มันแค่ อ่านตำแหน่งลูก แล้วขยับเข้าหา เท่านั้นเอง:

# ไม้ 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
อ่าน state: ball_y ตัดสินใจ: บน/ล่าง? ขยับ: ±AI_SPEED

deadzone ±5 — กันไม้ "สั่น" ตอนลูกอยู่กลางพอดี

ทำไมต้องมี - 5 กับ + 5 ในเงื่อนไข? ถ้าไม่มี ตอนลูกอยู่ตรงกลางไม้พอดี ไม้จะ
ขยับขึ้น-ลงสลับกันทุกเฟรม กลายเป็น สั่น (jitter) น่ารำคาญ (ค่าจริงในเกมเต็ม pong_full.py:148,150)

target_y deadzone ±5 = อยู่นิ่ง ลูกบนกว่า → เลื่อนขึ้น ลูกล่างกว่า → เลื่อนลง

ซ้าย: ไม่มี deadzone → ไม้เด้งขึ้น-ลงทุกเฟรม (jitter) · ขวา: มี ±5 → ลูกอยู่ในแถบ ไม้นิ่งสนิท

เลข ±5 คือ "เขตยอมรับ (deadzone)" ลูกอยู่ในช่วงนี้ ถือว่าตรงกันแล้ว ไม้อยู่นิ่ง ไม่ต้องไล่ตาม

deadzone เป็น "กราฟขั้นบันได" ของคำสั่งขยับ

ถ้าเอาเงื่อนไข if/elif สามบรรทัดมาวาดเป็นกราฟ จะเห็นรูปร่างชัดขึ้น แกนนอนคือ ระยะคลาด (ลูกห่างจากกลางไม้เท่าไร) แกนตั้งคือ คำสั่งขยับ ที่ไม้ได้รับ:

คำสั่งขยับ={+AI_SPEEDe>+5(ลูกอยู่ล่างกว่า)05e+5(deadzone)AI_SPEEDe<5(ลูกอยู่บนกว่า)e=target_yai_y\text{คำสั่งขยับ} = \begin{cases} +\text{AI\_SPEED} & e > +5 \quad(\text{ลูกอยู่ล่างกว่า}) \\ 0 & -5 \le e \le +5 \quad(\text{deadzone}) \\ -\text{AI\_SPEED} & e < -5 \quad(\text{ลูกอยู่บนกว่า}) \end{cases} \qquad e = \text{target\_y} - \text{ai\_y}

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

AI_SPEED — เลขตัวเดียวที่เปลี่ยน "ความฉลาด"

2.0
AI ช้าลง
เราตีชนะง่าย
3.0–4.5
กำลังพอดี
สนุก สูสี
8.0
AI เก่งมาก
รับเกือบทุกลูก

ลูกวิ่งเหมือนกันทั้ง 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 ไล่ตามลูกจริง ๆ

ดูการทำงานจริงของกฎสามบรรทัด ลูกวิ่งไปไหน ไม้ AI (ขวา) ก็ไล่ตาม ball_y เส้นประคือระยะที่มันพยายามไล่ให้ตรง:

  • ไม้กลายเป็น สีแดง ตอนกำลังขยับ (อยู่นอก deadzone) → ยังไล่ไม่ทัน
  • ไม้กลายเป็น สีเขียว ตอนลูกเข้ามาในแถบ deadzone → ถือว่าตรงแล้ว ไม้หยุดนิ่ง ไม่สั่น
  • สังเกตว่าไม้ "ตามหลัง" ลูกนิดหน่อยเสมอ เพราะขยับได้ทีละ AI_SPEED พิกเซลต่อเฟรม นี่แหละที่ทำให้เรายังพอตีชนะมันได้

state machine — เกมอยู่ "ช่วงไหน" ตอนนี้

เกมไม่ได้มีแค่ "เล่น" มันมีหลายช่วง เราใช้แนวคิด state machine: เกมอยู่ได้ครั้งละ 1 สถานะ และมี "ทางออก" ไปสถานะถัดไป

START รอกด START PLAY ลูกวิ่ง + นับคะแนน OVER WIN / LOSE เริ่ม ถึงเพดาน

ในเกมเล็กของเรา PLAY คือ on_each_frame() ที่วนทุกเฟรม ส่วน OVER เกิดเมื่อเรา return False (เกมหยุด) และกด START เพื่อออกก็คือทางออกของ PLAY เช่นกัน

ตีไม้ขวา (mirror) + นับคะแนน + จบเกม

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,139return False)

serve(direction) (pong_full.py:36-43) ย้ายลูกกลับ กลางจอ (:39-40) แล้วตั้งความเร็วใหม่ทันที จุดสำคัญคือ :43ball_vy = random.randint(-22, 22) / 10.0สุ่มมุมเสิร์ฟ เล็กน้อย (±2.2 px/เฟรม) เลียนแบบ lv_rand ของเกม C ทำให้ทุกครั้งที่เสิร์ฟลูกออกมุมไม่ซ้ำเดิม เกมจึงไม่กลายเป็นแพตเทิร์นเดียวซ้ำ ๆ

คะแนน + จบเกม (สูตรย่อ)

เราเพิ่มแต้มตามฝั่งที่ลูกหลุดออกไป แล้วจบเกมเมื่อมีฝ่ายถึงเพดานคะแนน:

ball_x<0ai_score+=1;ball_x>WIDTHplayer_score+=1\text{ball\_x} < 0 \Rightarrow \text{ai\_score}\mathrel{+}=1; \qquad \text{ball\_x} > \text{WIDTH} \Rightarrow \text{player\_score}\mathrel{+}=1

จบเกม    max(player_score, ai_score)WIN_SCORE\text{จบเกม} \iff \max(\text{player\_score},\ \text{ai\_score}) \ge \text{WIN\_SCORE}

ในไฟล์ full_games/pong_full.py คะแนนชนะเป็น per-mode (จาก MODES :33): EASY = 7, NORMAL/HARD = 10 — unpack ที่ :167
น้อง ๆ ปรับเลขในตาราง MODES เพื่อให้จบไวขึ้นตอนเดโมก็ได้ ลองดูว่าเลขไหนสนุกสุด

ส่วน 30% ของเราเสียบตรงไหนในลูป

game.start() on_each_frame() — วนทุกเฟรม (fps) 70% core: game.hit / move 30% เรา: AI tracker + คะแนน + จบเกม YOU WIN / LOSE return False = จบ

core 70% เขียนลูปไว้ให้แล้ว เราเติมแค่บล็อก 30% เข้าไปใน on_each_frame()

ผังทั้งโปรแกรม — update() ของ pong_full.py หนึ่งเฟรม

รวมทุกอย่างของคาบเป็น ผังควบคุมทั้งโปรแกรม แบ่งอ่านเป็นสองท่อน (บน = ฟิสิกส์+การชน · ล่าง = คะแนน+AI+วนแมตช์) สัญกรณ์เดียวกับคาบ 6: ◇ ม่วง = จุดตัดสินใจ, เขียว = I/O, ฟ้า/ส้ม = ประมวลผล, เทา = เริ่ม/จบ, เส้นประ = วนกลับ

อ่านจอยkeys() :76 ไม้ผู้เล่นaccel/friction :82-93 ลูกวิ่งx+=vx,y+=vy :96 ชนขอบบน/ล่าง? ชนไม้?:108/116 วาดลูก+ไม้เราmove_to :93,125 vy = -vy:100/104 สะท้อน+มุม+สปิน → clamp_ball_speed():109-122 No No Yes Yes ท่อนบน: ฟิสิกส์ + การชน (ต่อท่อนล่าง)
ต่อจากท่อนบนทุกเฟรม ลูกหลุดขอบ? :128/136 ไม้ AI: ±5 → ai_speed:145-153 วาดไม้ AImove_to :153 แต้ม += 1:129/137 ถึงเพดาน?:131/139 game_over()return False :203 serve():135/143 No (ยังไม่มีใครหลุด) Yes Yes No หลังเสิร์ฟ → เฟรมถัดไป game.run(update, fps=60) :199 · วน while แมตช์ใหม่ :161-204

อ่านผังนี้แล้วจะเห็นว่า "เกมจบ" ไม่ใช่จุดพิเศษ มันคือแค่ ◇ ถึงเพดาน? ที่ตอบ Yes แล้ว return False ออกจากลูป — ส่วน ◇ ลูกหลุดขอบ? ที่ตอบ No คือ 99% ของเฟรมที่เกมยังวิ่งปกติ

ขั้นบันได Pong — step3 → step4 → step5 → เกมเต็ม

ก่อนเปิด pong_full.py มาดูภาพรวมกันก่อนว่าเกมเต็มไม่ได้โผล่มาทีเดียว มันไต่ขึ้นมาทีละขั้น แต่ละขั้นเพิ่ม ไอเดียใหม่แค่อย่างเดียว ต่อยอดจากขั้นก่อน:

step3 ตีลูกกลับ (collision) pong_step3.py · คาบ 6 step4 AI ตามลูก + คะแนน + first-to-N pong_step4.py :67-73 · :108-115 step5 เมนูโหมด + AI ต่อโหมด pong_step5.py :36-39 · :121-127 เกมเต็ม + สุ่มมุมเสิร์ฟ + speed-clamp pong_full.py :33 · :43 · :46-53
ขั้น ไอเดียใหม่หนึ่งอย่าง ไฟล์ · บรรทัด
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)
step4 — เกมจบได้จริงครั้งแรก. พอมี AI ฝั่งขวา (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 บนจอ
step5 — AI เกมตัวแรกที่ปรับความยากได้. เพิ่มเมนู EASY/NORMAL/HARD (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 + การขัดเงา ไม่ใช่ของใหม่ทั้งก้อน

MVP-1 — Pong เล่นจบเกมได้จริง

  1. เปิดไฟล์เกมเต็ม full_games/pong_full.py ใน BENTO IDE
  2. กดปุ่ม Program to Device เพื่อโหลดลงบอร์ด
  3. กด UP/DOWN ขยับไม้เรา ไม้ขวาเป็น AI ตีโต้กับเราเอง
  4. ปล่อยลูกหลุด → คะแนนขึ้นถูกฝั่ง → เสิร์ฟใหม่กลางจอ
  5. เล่นจนมีฝ่ายถึงเพดาน → ขึ้น YOU WIN! / AI WINS เกมหยุด

ตีโต้ครบรอบ → ได้แต้มชี้ขาด → ขึ้นหน้า YOU WIN! (ตรงกับขั้นตอน 4–5)

หน้าจบเกมจริงบนบอร์ด BENTO

ถ้าทำถึงตรงนี้ได้ ก็ผ่าน MVP-1 แล้ว Pong ของเราเล่นจบเกมได้ และมีคู่ต่อสู้ AI

เชื่อมโยงรากฐาน · ไม้ AI ตัวนี้สอนวิศวกรรมอะไรเราบ้าง

ไม้ AI ที่ดูเหมือนเล่น ๆ จริง ๆ แล้วคือ วงจรป้อนกลับ (feedback loop) หัวใจของงานควบคุมฝังตัวทั้งหมด:

ฝั่ง Embedded / ควบคุม

  • รูปแบบ sense → decide → act ที่วนทุกเฟรม คือหัวใจของ control loop ในงานจริง (thermostat, มอเตอร์, cruise control)
  • deadzone คือเทคนิคเดียวกับการกรอง noise ของ ADC/joystick ไม่ให้ค่าเล็ก ๆ ทำให้ระบบกระตุก

ฝั่ง Python / Algorithm

  • กฎ if/elif/else คือ decision rule พื้นฐานของ AI ทุกตัว เริ่มจากตรงนี้แล้วค่อยซับซ้อนขึ้น
  • state machine (start/play/over) คือโครงที่ระบบฝังตัวใช้คุมโหมดการทำงานเสมอ
  • ตำแหน่งบนจอเป็น coordinate system การไล่ 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_vx ก่อน hit ครอบด้วย BALL_CAP deadzone ±5 กันไม้สั่น

สรุปคาบ 7 + ทำเอง 30%

วันนี้เราได้:

  • [ ] AI Tracker: ไม้ขวาอ่าน ball_y แล้วขยับ ai_y ตาม (กฎไม่กี่บรรทัด)
  • [ ] deadzone ±5 กันไม้สั่น + ปรับ AI_SPEED ให้เกมสูสี
  • [ ] นับคะแนนถูกฝั่ง + เสิร์ฟใหม่กลางจอ
  • [ ] state machine start / play / over → จบเกมเมื่อถึงเพดานคะแนน
  • [ ] อธิบายได้ว่า "AI ของเรา = กฎที่อ่านสถานะแล้วเลือกการเคลื่อนที่"

การบ้าน 30% (ทำเอง): เริ่มจาก practise_codes/pong_step3.py เติมส่วนตีลูกกลับให้ครบ (ถ้าติด ยกมือถามได้เลย เดี๋ยวเฉลยในห้อง) แล้วลองต่อยอด:
1. ปรับ AI_SPEED 3 ค่า แล้วบอกว่าค่าไหนสนุกสุด เพราะอะไร
2. ทำให้ AI "พลาดบ้าง" สัก 1 ใน 10 เฟรม (ไม่ขยับ) แล้วสังเกตว่าเล่นง่ายขึ้นไหม
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 สังเกตนิสัยนี้ไว้ก่อน: เลขที่เราอยากปรับบ่อยให้ตั้งชื่อไว้บนสุด งานที่ทำซ้ำให้ห่อเป็นฟังก์ชัน สองอย่างนี้จะกลับมาช่วยเราทุกคาบที่เหลือ

เฉลย · หัวใจของคาบ — กฎ 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 สู่ step5 — เลขคงที่กลายเป็นตาราง

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) โครงเดิม ค่ามาจากตาราง
  • แถวล่างสุดคือหัวใจ: กฎ AI สามบรรทัดของ step4 กับ step5 หน้าตาเหมือนกันเป๊ะ ต่างแค่ step4 ใช้ค่าคงที่ step5 ดึงจาก MODES[selected_mode] การแยก "ค่า" ออกจาก "โครง" คือบันไดขั้นเดียวที่ทำให้เกมปรับความยากได้ โดยไม่ต้องเขียน AI ใหม่
  • นี่คือ pattern เดียวกับสไลด์ "ขั้นบันได Pong": แต่ละขั้นเพิ่มไอเดียใหม่ แค่อย่างเดียว ต่อยอดของเดิม ไม่รื้อทำใหม่ทั้งก้อน

ถ้าเห็นว่า step5 คือ step4 ที่ "ย้ายเลขไปไว้ในตาราง" น้องก็จับหลักการออกแบบที่ใช้ได้ทั้งชีวิตแล้ว: ทำให้ของทำงานก่อน แล้วค่อยดึงเลขออกมาเป็นพารามิเตอร์ทีหลัง

เชื่อมจุดให้เห็นภาพ — ไม้ AI ตัวนี้มาจากไหน จะพาเราไปถึงไหน

หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องโค้ด แต่เรื่องมองให้ทะลุว่า 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 ยังเหมือนเดิมทุกบรรทัด ไม้ที่ไล่ลูกวันนี้ คือศัตรูที่ไล่ผู้เล่นในเกมถัดไป และคือระบบควบคุมที่อ่านค่าจริงจากโลกในโปรเจกต์จบ

ที่มา วงจรลอจิกไล่ลูก (Pong 1972) วันนี้ if 3 บรรทัด อ่าน ball_y (AI tracker) ที่ไป sense→decide→act อ่านเซนเซอร์ (คาบ 9 · เกมถัดไป)

ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:

  • จำ deadzone ±6 ที่กันไม้ไม่ให้สั่นได้ไหม มันคือเรื่องเดียวกับการกรอง noise ของก้านจอย (analog stick) ตอนปล่อยมือหรือเปล่า? (ใบ้: ค่าคลาดเล็ก ๆ ถูกกลืนเป็นศูนย์เหมือนกัน)
  • ถ้าวันนี้เราเขียนกฎให้ไม้ ไล่ตามลูก ได้ พรุ่งนี้เราจะเขียนกฎให้ศัตรู ไล่ตามผู้เล่น ได้ไหม? โค้ดจะเปลี่ยนไปมากไหม ถ้าแค่สลับ ball_y เป็น player_y?
  • สังเกตไหมว่า target_y = ball_y - ... แล้วขยับ ai_y เข้าหา คือคณิต พิกัด ล้วน ๆ เรื่องเดียวกับที่เราจะใช้ให้เซนเซอร์เอียงคุมตำแหน่งของบนจอในคาบ 9

ถ้าตอบได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด: ไม้ AI ที่ดูเล่น ๆ วันนี้ คือเมล็ดของทุกระบบที่ "อ่านโลกแล้วตัดสินใจ" ที่เราจะปลูกต่อทั้งคอร์ส

ใช้จริงที่ไหน — tracker rule · deadzone · state machine

กฎ AI สามบรรทัดกับของที่ห้อมล้อมมันในคาบนี้ ไม่ใช่ของสมมติ มันคือหลักที่ระบบจริงใช้กันทุกวัน:

เทอร์โมสตัท / แอร์ · hysteresis เป้า ± deadzone เกินแถบ→เปิด · ต่ำกว่า→ปิด · ไม่มีแถบ=สั่น กล้องตามหน้า / กิมบอลกันสั่น target = กลางเฟรม มอเตอร์ขยับเข้าหาทีละก้าว sense → decide → act (องศาแทนพิกเซล) ศัตรู / NPC ไล่ตามผู้เล่น target = player_y — tracker rule เดิม + อ้อมสิ่งกีดขวาง ผู้เล่น ศัตรู state machine ในเครื่องจริง รอ ทำงาน เตือน ไฟจราจร / ตู้กดน้ำ — ครั้งละ 1 สถานะ เหมือน start / play / over
  • เทอร์โมสตัท / แอร์ — อ่านอุณหภูมิ เทียบเป้า สั่งเปิด/ปิดคอมเพรสเซอร์ คือ sense → decide → act ตัวเดียวกับไม้ AI และมี deadzone (hysteresis) เป๊ะแบบ ±6 ของเรา ถ้าไม่มี คอมเพรสเซอร์เปิด-ปิดถี่ ๆ (สั่น) เหมือนไม้ไม่มี deadzone
  • กล้องตามหน้า / กิมบอลกันสั่น — มอเตอร์หมุนกล้องให้ "กลางเฟรมตรงกับใบหน้า" คือ target_y = ... แล้วขยับเข้าหาทีละก้าว ต่างจากไม้เราแค่หมุนเป็นองศาแทนพิกเซล
  • NPC / ศัตรูในเกมจริง — มอนสเตอร์ที่วิ่งเข้าหาผู้เล่น พื้นฐานคือ tracker rule เดียวกับที่เราเขียน แค่เพิ่มการหาทางอ้อมสิ่งกีดขวาง
  • state machine ในเครื่องจริง — ไฟจราจร, ตู้กดน้ำ, เมนูเครื่องใช้ไฟฟ้า คุมด้วยสถานะครั้งละหนึ่ง มีทางออกไปสถานะถัดไป เหมือน start / play / over ที่เราใช้ปิดเกมด้วย return False

สี่อย่างที่เราแตะวันนี้ ไม่มีอันไหนเป็นของเล่น เทอร์โมสตัทในบ้าน กล้องในมือถือ ศัตรูในเกมโปรด ทำงานด้วยหลักเดียวกับไม้ AI ที่น้องเพิ่งเขียน

ต่อยอด — คิดต่อเอง

ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาของจริงได้:

แบบตอบสนอง — ไล่ ball_y ปัจจุบัน ไม้เล็ง y ปัจจุบัน → ตามหลัง พลาดลูกที่พุ่งขึ้น เปลี่ยนวิธีคิด แบบทำนาย — เล็งจุดที่ลูกจะไปถึง เล็งจุดที่ลูกจะไปถึง → รอรับพอดี ทันเสมอ = ระบบนำวิถี / รถอัตโนมัติ ที่เล็งจุดในอนาคต ไม่ใช่ตำแหน่งปัจจุบัน
  • AI ที่พลาดเป็น — ถ้าอยากให้ AI "เผลอ" ไม่ขยับสัก 1 ใน 10 เฟรม จะเสียบตรงไหนของกฎสามบรรทัด และทำให้ EASY พลาดบ่อยกว่า HARD ได้ยังไงโดยไม่แตะโครงเดิม?
  • 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 ได้

← Roadmap (TOC)