คาบ 13 — Showcase + ส่งมอบ Release

ส่งเกมของกลุ่มให้เพื่อนโหลดไปเล่นเองได้ (คาบสุดท้าย)

ปิดคอร์ส Developer II — คาบ 13/13 (apex ของคอร์ส)

คาถาประจำคาบ: "เกมของเรา เพื่อนต้องโหลดไปรันเองได้ โดยไม่ต้องมีเราอยู่ข้าง ๆ"

กลุ่ม 3 คน / 1 บอร์ด · MVP สุดท้าย (FINAL)

เป้าหมายของคาบ

ตลอด 12 คาบที่ผ่านมา น้อง ๆ ทำ Snake (MVP-1) และ Flappy (MVP-2) จนเล่นจบได้บนบอร์ดจริง แล้วเริ่มต่อยอดเป็นเกมของกลุ่มเองใน mygame.py

วันนี้เราจะเอาเกมของกลุ่มมาทำให้ครบลูป เริ่ม → เล่น → Game Over → กด Start เริ่มใหม่ แล้วออก Release v1.0 ให้กลุ่มข้าง ๆ โหลดไปรันบนบอร์ดของเขาเองได้

สิ่งที่อยากให้น้อง ๆ เห็นกับตา: กลุ่มอื่นโหลดเกมของน้อง ๆ จากหน้า Release → เปิด mygame.py ใน BENTO IDE แล้วกด Program to Device → เล่นได้เลย โดยที่น้อง ๆ ไม่ต้องเข้าไปยุ่งด้วย
นั่นแหละคือความหมายจริง ๆ ของคำว่า "ส่งมอบซอฟต์แวร์"

3 ไอเดียของคาบสุดท้าย

#1 เกมครบรอบ
เกมจริงต้องปิดวงจรชีวิตได้เอง: เริ่ม→เล่น→จบ→กด START เริ่มใหม่ (firmware soft reset จัดการให้)
#2 "ส่งมอบ" ≠ "ทำเสร็จ"
โค้ดเสร็จเมื่อ คนอื่นเอาไปรันเองได้ จากของที่เราปล่อย
#3 Showcase = feedback loop
เล่นเกมเพื่อน → ชม → ติเชิงสร้างสรรค์ → ชุมชน dev โตขึ้นจากตรงนี้

หน้าตาของ "#1 เกมครบรอบ" บนของจริง คือเกมที่รันจบรอบได้บนหน้า Playground/Release ของบอร์ด — เพื่อนเปิดแล้วเล่นได้ทันที เหมือนภาพนี้:


ที่มา: ภาพหน้าจอบอร์ด PSoC Edge รันบน BENTO IDE — สื่อประกอบคอร์ส BDH & TESAIoT

คาบนี้ ไม่มี API ใหม่เลย เราแค่รวมทุกอย่างที่ฝึกมา 12 คาบเข้าด้วยกัน แล้ว "ส่งมอบ" ให้เป็น

โครง 70/30 — ย้ำครั้งสุดท้าย

bentogame.py — 70% core (ให้มา · ห้ามแก้) game.start / Box / Sprite / Text / keys / run / hit / sfx update() — 30% ที่เราเขียนเอง กติกาเกมใน mygame.py (init + update) BACK = ออก START = restart (run() จัดการให้)

เราเขียนแค่ในช่อง 30% · ห้ามแก้ bentogame.py · ทุก verb เป็น game. call ที่เตรียมไว้ให้แล้ว · bentogame ฝังในเฟิร์มแวร์ (frozen) เพื่อน import ได้เลย

จุดเริ่มของกลุ่ม — practise_codes/mygame.py

เปิดมาเกมเปล่า ๆ ที่ "รันได้เลย": ผู้เล่นขยับได้ + มี score บนจอ มี 3 ช่อง TODO ให้เติมเอง

    # practise_codes/mygame.py — โครง update() เปล่าที่รอเราเติม
    def update():
        global score
        k = game.keys()              # อ่านปุ่มจากจอย
        if k.left:  player.move(-PLAYER_SPEED, 0)
        if k.right: player.move(PLAYER_SPEED, 0)
        if k.up:    player.move(0, -PLAYER_SPEED)
        if k.down:  player.move(0, PLAYER_SPEED)
        # BACK = ออกเกม, START = เริ่มใหม่ -> game.run() จัดการให้เอง
        # TODO #2 gameplay: game.hit() + score
        # TODO #3 polish: Box -> Sprite + game.sfx("gameover")
        return True                  # เล่นต่อ (return False เฉพาะตอนชนะ/แพ้จริง)

โครงทั้งไฟล์มีแค่ 3 จังหวะ: init รัน ครั้งเดียว ตอนเปิด → game.run() วน update() ให้เองทุกเฟรม → ตอนชนะ/แพ้เรา return False เพื่อจบรอบ:

init (ครั้งเดียว) title / Box / Text / score=0 game.run() วน update() ทุกเฟรม: keys -> move -> draw return True = เล่นต่อ ~30 ครั้ง/วินาที return False จบรอบ -> GAME OVER เรียก 1 ครั้ง ชนะ/แพ้

ลองเติมเองให้สุดก่อนนะ — เดี๋ยวเฉลยพร้อมกันในห้องเรียน

ขั้น A — ทำให้เกม "ครบรอบ"

"ครบรอบ" บน Bento ไม่ต้องเขียน loop รอบนอกเอง — เฟิร์มแวร์จัดการให้แล้ว:

    # mygame.py — โครงทั้งไฟล์: init ครั้งเดียว แล้วเรียก game.run() ครั้งเดียว
    game.title("MY GAME")   # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
    score = 0
    player = game.Box(game.WIDTH // 2, game.HEIGHT // 2, 32, 32, game.CYAN)
    label  = game.Text("Score: 0", 12, 10, game.WHITE)

    def update():
        ...
        # ชนะ/แพ้จริง -> return False -> แสดง GAME OVER แล้วหยุดรอ
        return True

    game.run(update, fps=30)            # เรียกครั้งเดียวพอ

ปุ่มระบบ game.run() จัดการให้ทุกเกม: BACK = ออกกลับ Playground, START = เริ่มเกมใหม่ (รัน MicroPython ใหม่ตั้งแต่ต้นไฟล์ → โค้ด init ด้านบนรันใหม่ = เกมสด ๆ รอบใหม่)

START รีเซ็ตทั้งบอร์ด (soft reset) → ตัวแปรใน RAM ทั้งหมดถูกล้าง รวมถึง score

ขั้น A — "ครบรอบ" หน้าตาเป็นยังไงตอนรันจริง

วงจรหนึ่งรอบของเกมเดินแบบนี้ ไล่จากซ้ายไปขวา แล้วพอกด START ก็ย้อนกลับไปเริ่มที่ init ใหม่:

จุดที่อยากให้สังเกต คือ ลูกศรสีแดง ตอนกด START — มันไม่ได้กระโดดกลับไปแค่ตอนเริ่มเล่น แต่ย้อนไปถึง init บนสุดของไฟล์ เหมือนเปิดโปรแกรมใหม่ทั้งตัว เพราะงั้น score กับตัวแปรอื่นใน RAM จึงเริ่มจากศูนย์ทุกครั้ง โดยที่เราไม่ต้องเขียนโค้ดรีเซ็ตเอง

ขั้น A — state machine ของ "ครบรอบ"

init (บนสุดไฟล์) start + score=0 เล่น (update) GAME OVER รอ START กด START → soft reset → รันไฟล์ใหม่ตั้งแต่ init

START = soft reset → กลับไป init บนสุดของไฟล์ เอง (ไม่ต้องเขียน while True ครอบ) · เช็คผ่าน: เล่นแพ้ → เห็น GAME OVER + Score → กด START → เกมเริ่มรอบใหม่สด ๆ

อยากเก็บ best score ข้ามรอบ? RAM ถูกล้างตอน START → ต้องเซฟ ลงไฟล์:
try: best = int(open("best.txt").read()) ... except: best = 0 ตอน init · เขียนกลับ open("best.txt","w").write(str(best)) ตอนจบรอบ · ถ้ายังไม่ถึงขั้นนั้น ตัด best ทิ้งไปก่อนได้ ทำเกมให้ครบรอบสำคัญกว่า

ขั้น B — เติม gameplay (เฉลยช่องที่ 2)

หัวใจของเกมคือ "ชนแล้วเกิดอะไร" — ที่นี่คือชนเป้าแล้ว +1 แต้ม:

    # ช่อง gameplay ที่เราเติมเอง
    if game.hit(player, target):       # ผู้เล่นแตะเป้า?
        score += 1
        label.set("Score: %d" % score) # อัปเดตป้ายคะแนน
        game.sfx("point")              # เสียงได้แต้ม
        _move_target()                 # เด้งเป้าไปจุดใหม่
        if score >= WIN_SCORE:         # ครบเป้า = ชนะ
            game.Text("YOU WIN!", game.WIDTH // 2 - 70,
                      game.HEIGHT // 2, game.GREEN)
            game.sfx("win")
            return False

นี่คือ pattern เดียวกับที่น้อง ๆ ใช้ใน Snake (กินอาหาร) และ Flappy (ลอดท่อ) — แค่เปลี่ยน "เงื่อนไขชน" กับ "ผลที่ตามมา" ให้เป็นกติกาของกลุ่มเอง

เสียงในคาบนี้game.sfx("point") ตอนชนเป้าได้แต้ม (เสียง eat ของ Snake / score ของ Flappy) และ game.sfx("gameover") ตอนแพ้ คือ feedback ที่ทำให้เกมของกลุ่ม "มีชีวิต" (กดเล่นฟังได้จริง):

snake_eat

flappy_score

game_over

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

เปิดกล่องดำ update() — ผังทั้งเกม mygame.py

ผัง lifecycle ก่อนหน้าหยุดที่กล่อง "วน update()" เฉย ๆ ทีนี้เรากางข้างในของ update() ออกมาทั้งหมด — หนึ่งเฟรมทำอะไรบ้าง ตั้งแต่อ่านปุ่มจนถึงจุดที่ return False (ชนะ) แล้ว game.run() วนให้เองทุกเฟรม (mygame.py:64)

init → run:17-23,64 keys():38 player.move:39-46 hit?:51 score+1 · set · sfx · move_target:52-55 score≥WIN?:56 YOU WIN!return False :57-59 ใช่ ใช่ ไม่ ไม่ return True → เฟรมถัดไป (30 Hz) :61

สัญกรณ์เดียวกับทุกผัง: ▭ฟ้า = ประมวลผล · ▭เขียว = อ่าน input · ◇ม่วง = ตัดสินใจ · ▭เทามน = เริ่ม/จบ · เส้นประ = วนกลับ

ทฤษฎี → โค้ด: AABB ชนกล่อง game.hit(a,b):51 (คือ pattern เดียวกับ Snake กินอาหาร / Flappy ลอดท่อ) · FSM ชนะ score ≥ WIN_SCORE:56 · return False = สภาวะจบ (terminal) → :59 · ทั้งหมดนี้อยู่ใน 30% ที่เราเขียน ส่วน game.run() (70%) เป็นตัววนเรียกให้ :64

ขั้น B — ยืม pattern จากเกมอ้างอิงได้

Snake = pattern "กินของแล้วโตขึ้น" (full_games/snake_full.py:50-55) · Flappy = pattern "scroll ของเข้ามา + ชน = จบ" (full_games/flappy_full.py:54-60)
หลักการเดียว: อ่าน state → ตรวจชน → เปลี่ยน state + ให้รางวัล/ลงโทษ

ขั้น C — กับดัก 32 widget (ห้ามข้าม VERIFY)

สร้าง sprite/text ครั้งเดียว นอก update() เท่านั้น:

    # mygame.py — สร้างครั้งเดียวก่อนลูป อย่าสร้างใน update()!
    player = game.Sprite("ship", 380, 350)
    target = game.Sprite("snake_food", 0, 0)
    label  = game.Text("Score: 0", 12, 10, game.WHITE)

ใน frame loop ให้ใช้ .move_to() / .set() กับ widget ตัวเดิม อย่าเรียก Box/Sprite/Text ซ้ำทุกเฟรม

ขีดจำกัด IPC widget (Python ปิดที่ ~26–32 ตัว):

Nwidget สะสม32สร้างทุกเฟรม ×  F เฟรม    ชน cap แล้วค้างN_{\text{widget สะสม}} \le 32 \quad\Longrightarrow\quad \text{สร้างทุกเฟรม } \times \;F \text{ เฟรม} \;\Rightarrow\; \text{ชน cap แล้วค้าง}

VERIFY ที่แท้จริง คือเล่นจริง ≥3 รอบติด แล้วไม่ค้าง

ขั้น C — ทำไมสร้าง widget ในลูปถึงค้าง (อ่านกราฟ)

อสมการเมื่อกี้แปลเป็นภาพได้ตรง ๆ ลองดูว่า "จำนวน widget สะสม" โตยังไงเมื่อเวลาผ่านไปทีละเฟรม:

  • เส้นส้ม = เผลอสร้าง Sprite/Text ใหม่ใน update() แต่ละเฟรมบวกเพิ่มเรื่อย ๆ จำนวนจึงไต่ขึ้นเป็นเส้นตรง พอแตะ เส้นประแดง (cap = 32) ระบบก็เปิด widget ใหม่ไม่ได้อีก เกมค้าง
  • เส้นเขียว = สร้างครบทุกตัวครั้งเดียวตอน init แล้วในลูปแค่ขยับด้วย .move_to() / เปลี่ยนข้อความด้วย .set() จำนวนจึงคงที่ (ในรูปคือ 3 ตัว) อยู่ห่างจาก cap สบาย ๆ เล่นได้ไม่จำกัดเวลา

สังเกตว่าเส้นส้มไม่ได้ค้างทันที มันค่อย ๆ ไต่ — เกมเลยดู "ปกติ" อยู่ราว ๆ ครึ่งนาทีก่อนจะค้าง นี่คือเหตุผลที่ต้องเล่นยาว ๆ ตอน VERIFY ไม่ใช่แค่แตะ ๆ แล้วผ่าน

ขั้น D — ออก Release v1.0

    git add mygame.py                    # ship เฉพาะเกมตัวเอง — bentogame frozen (ห้าม git add .)
    git commit -m "team3: final game ครบรอบ + START restart (v1.0)"
    git push -u origin team-yourgame

GitHub → Releases → Draft → tag v1.0Attach: mygame.py (bentogame frozen ไม่ต้องแนบ) → Publish

Release แนบแค่ mygame.py พอ เพราะ bentogame ฝังอยู่ในเฟิร์มแวร์ (frozen) เพื่อน import ได้เลย ไม่ต้องแนบ
อย่าลืมเขียนวิธีรันไว้ใน note: เปิด mygame.py ใน BENTO IDE แล้วกด Program to Device

ขั้น D — CI เขียวก่อน merge ก่อน Release

branch → commit เฉพาะไฟล์ mygame.py → PR → CI เขียว → review ไขว้ approve → merge → Release v1.0

ทั้งเส้นทางนี้คือสิ่งที่ทีมซอฟต์แวร์จริงทำกันทุกวัน จุดสำคัญอยู่ที่ลูกศร "ส่งมอบ" สีม่วง ก่อนถึงตรงนั้นเป็นงานของทีมเรา (เขียน → ตรวจ → รวม) แต่หลังจากนั้นคนอื่นต้องหยิบไปรันต่อได้เอง โดยที่เราไม่ได้อยู่ด้วย

ขั้น E — ส่งมอบ (หัวใจของวัน)

สลับบอร์ด: กลุ่มข้าง ๆ โหลดเกมของน้อง ๆ → รันเอง ต่อหน้าน้อง ๆ (น้อง ๆ แค่ดู ห้ามช่วยพิมพ์)

    1. โหลด mygame.py จากหน้า Release ของเพื่อน
    2. เปิด mygame.py ใน BENTO IDE   (bentogame frozen แล้ว ไม่ต้องวางเพิ่ม)
    3. กด Program to Device          -> เล่นได้เลย

เราไม่ใช้ exec(open(...)) ในการรัน — ส่งมอบกันด้วย Program to Device ใน BENTO IDE
เพื่อนรันได้เองโดยไม่ต้องถามน้อง ๆ เลย นั่นคือ ส่งมอบสำเร็จ (FINAL ผ่าน)
ถ้ายังติด ให้จดไว้ก่อนว่าเพราะอะไร (ชื่อ sprite ผิด? ลืมบอกวิธีรัน?) แล้วค่อย ๆ แก้ note ให้ครบ

ขั้น E–F — Game Jam รอบห้อง + โหวต

แต่ละกลุ่ม = 1 บูธ (เกมที่ ship เป็น Release แล้ว) → เพื่อนเดินรอบห้อง โหลดไปรันบนบอร์ดตัวเอง เล่น + ให้ feedback → คนละ 1 ข้อติเชิงสร้างสรรค์ → โหวต WINNER

Game Jam ที่เราทำวันนี้ คือของจริงทั่วโลก

สิ่งที่กำลังจะเกิดในห้องเรา ไม่ใช่กิจกรรมจำลอง นักพัฒนาเกมทั่วโลกรวมตัวกันแบบนี้จริง ๆ ในงานอย่าง Global Game Jam จับกลุ่มกันเล็ก ๆ ตั้งโจทย์ แล้วลงมือทำเกมให้เสร็จในเวลาจำกัด เหมือนที่น้อง ๆ ทำกับ mygame.py

ที่มา: "Global Game Jam 2019 à Arles" — Yannickvernet, CC BY-SA 4.0, Wikimedia Commons

ทักษะชุดเดียวกับที่ฝึกมา 13 คาบ — ทำเกมให้ครบรอบ, ส่งมอบให้คนอื่นรันได้, รับฟังคำติเพื่อแก้ — คือทักษะที่ทีมพัฒนาจริงใช้ ไม่ว่าจะทำเกม แอป หรือผลิตภัณฑ์ฝังตัว

ลองดูบรรยากาศจริงของงานระดับโลก — คนนับหมื่นทั่วโลกจับกลุ่มทำเกมพร้อมกันในสุดสัปดาห์เดียว เหมือนที่เรากำลังจะทำในห้อง:

What is Global Game Jam? — GlobalGameJam (ช่องทางการของงาน)

กับดักที่เจอบ่อย (อ่านก่อน debug)

อาการ สาเหตุ วิธีแก้
กด START แล้วไม่เริ่มใหม่ ดักปุ่ม START เองใน update() ลบ if k.start: return Falsegame.run() จัดการ restart ให้
best หาย/เป็น 0 ทุกครั้ง เก็บ best ในตัวแปร RAM ที่ START ล้างทิ้ง เซฟ best ลงไฟล์ (best.txt) อ่านตอน init
เล่น ~30 วิ แล้วค้าง สร้าง widget ในลูป → ชน 32 cap สร้างครั้งเดียวตอน init + .move_to/.set
เพื่อนรันไม่ได้ ลืมแนบ mygame.py หรือไม่บอกวิธีรัน แนบ mygame.py + เขียน "Program to Device"
sprite เป็นกล่องเขียว desktop sim / fw เก่า pixel-art เห็นบน บอร์ดจริง เท่านั้น

หน้าตาของอาการแถวสุดท้าย — ซ้ายคือ "ผิด" (sprite กลายเป็นกล่องเขียวทึบ) ขวาคือ "ถูก" (pixel-art ขึ้นจริงบนบอร์ด):


ที่มา: ภาพประกอบอาการ fallback (ซ้าย) + ภาพหน้าจอบอร์ดจริง (ขวา) — สื่อประกอบคอร์ส BDH & TESAIoT

sprite = pixel-art เห็นได้บน บอร์ดจริง เท่านั้น (desktop/fw เก่า fallback เป็นกล่องเขียว)

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

คาบนี้ไม่มี API ใหม่ แต่สิ่งที่ฝึก คือกระบวนการที่ทีมวิศวกรรมจริงใช้กับทุกผลิตภัณฑ์ ไม่ใช่แค่เกม:

ฝั่ง Software Engineering

  • branch → PR → CI → merge → tag → Release คือ release lifecycle มาตรฐานของวงการ
  • "เสร็จเมื่อคนอื่นรันเองได้" คือนิยามของ reproducible build / artifact ที่แยกจากเครื่องเรา
  • review ไขว้ + รับคำติ = code review กับ feedback loop ที่ทำให้ของดีขึ้นทุกรอบ

ฝั่ง Embedded / Python

  • กด START = soft reset กลับสู่ known start state หลักเดียวกับที่ MCU รีบูตเข้าสู่สถานะตั้งต้นที่คาดเดาได้
  • score หายเพราะอยู่ใน RAM ส่วน best score รอดเพราะเขียนลง ไฟล์ (non-volatile) — เส้นแบ่งความจำที่งานฝังตัวเจอทุกวัน
  • สร้าง widget ครั้งเดียวแล้ว reuse คือการ จัดการทรัพยากรจำกัด (มี cap แค่ 32) แบบเดียวกับ buffer/handle บนชิป

"release" ในงานซอฟต์แวร์คืออะไร ทำไมถึงต่างจาก "เขียนโค้ดเสร็จ" — ตรงกับสิ่งที่เราทำวันนี้พอดี:

What is RELEASE in Software Development? Easy Explanation for Beginners — TS4U

Acceptance (FINAL) + ปิดคอร์ส

  • [ ] เล่น ครบรอบบนบอร์ดจริง (เล่น → จบ → กด START เริ่มใหม่ ≥3 รอบติดไม่ค้าง)
  • [ ] ใช้ 70% core เท่านั้น — ไม่แก้ bentogame.py
  • [ ] PR → CI เขียว → review ไขว้ → merge
  • [ ] Release v1.0 แนบ mygame.py + วิธีรัน (bentogame frozen ไม่ต้องแนบ)
  • [ ] กลุ่มอื่นโหลดไปรันได้สำเร็จต่อหน้า (นิยามของ "ส่งมอบ")
  • [ ] โชว์ + demo ที่ Game Jam + ตอบปากเปล่าได้

ทำเอง 30% — เติมเกมของกลุ่มใน practise_codes/mygame.py (ลองเองให้สุดก่อน เดี๋ยวเฉลยในห้อง) ให้ครบ 3 ช่อง — restart มาจาก START ของ game.run() แล้ว

ผ่านครบทุกข้อ คือจบคอร์ส Developer II แล้ว น้อง ๆ สร้างเกม Embedded ด้วย MicroPython ของตัวเองได้จริง ครบ 70/30 ภูมิใจในตัวน้อง ๆ นะ
หมายเหตุ: DevOps เต็มรูป + AI agent ยกไปต่อใน Developer III

เฉลย mygame.py — artifact ที่เราจะส่งมอบจริง (อ่านเข้าใจ แล้วพิมพ์เอง)

ไฟล์ solution_codes/mygame.py คือเกมเต็มที่เติม 3 ช่องครบแล้ว มีไว้ให้น้อง ๆ เทียบ หลังลงมือเติม practise_codes/mygame.py เอง ไม่ใช่ให้เปิดลอกวางส่ง คะแนนของคาบนี้อยู่ที่ "เพื่อนโหลดไปรันได้จริง" กับใบงาน ไม่ใช่โค้ดตรงเฉลย ลองเติมเองให้สุดสัก 15 นาที ติดตรงไหนค่อยเปิดอ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง

ต่างจากคาบก่อน ๆ ตรงที่คาบนี้ ทั้งไฟล์คือ "ของที่จะ Release" เราจึงจะอ่านเฉลยในมุมใหม่: อะไรบ้างที่ทำให้ไฟล์ 65 บรรทัดนี้ "ส่งมอบให้คนอื่นรันได้"

import bentogame as game

PLAYER_SPEED = 8          # ขยับทีละกี่พิกเซลต่อเฟรม
FPS = 30                  # ความถี่ของลูป
WIN_SCORE = 5             # ชนเป้าครบเท่านี้ = ชนะ
  • import bentogame as game (:11) — ทั้งไฟล์พึ่ง bentogame ตัวเดียว ไม่มี import อื่น เพราะ core ฝังอยู่ในเฟิร์มแวร์ (frozen) ตามสไลด์ โครง 70/30 นี่คือเหตุผลตรง ๆ ที่ Release แนบแค่ mygame.py ก็พอ เพื่อน import ได้เลย
  • PLAYER_SPEED / FPS / WIN_SCORE (:13-15) ยกค่าปรับได้ขึ้นบนสุดเป็นชุดเดียว — เพื่อนที่โหลด artifact ไปอยากให้เกมเร็วขึ้นหรือชนะยากขึ้น แก้เลขบรรทัดเดียว ไม่ต้องแกะ update()
  • ตั้งชื่อ UPPER_CASE บอกคนอ่านว่า "นี่คือค่าคงที่ ปรับได้ปลอดภัย" ต่างจาก score ที่เปลี่ยนไปเรื่อยระหว่างเล่น — นิสัยแยก "ค่า" ออกจาก "โครง" ที่ใช้มาตั้งแต่ MVP-1 Snake
  • ถ้า hard-code เลข 8/30/5 กระจายในโค้ด คนที่รับ artifact ไปแก้ต้องไล่หาทุกจุด กลายเป็นของที่ส่งมอบต่อยาก

artifact ที่ดีคือไฟล์ที่คนรับเปิดมาแล้วเห็น "ปุ่มปรับ" อยู่ตรงหน้า ไม่ต้องเดาว่าตัวเลขไหนแก้ได้

เฉลย · STATE — สร้างครั้งเดียวก่อนลูป (known start state)

game.title("MY GAME")                          # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)

# 1) STATE — เกมต้องจำอะไรบ้าง (สร้างครั้งเดียว ก่อนลูป)
score = 0
player = game.Box(game.WIDTH // 2, game.HEIGHT // 2, 32, 32, game.CYAN)
target = game.Box(120, 120, 28, 28, game.YELLOW)
label = game.Text("Score: 0", 12, 10, game.WHITE)
  • game.title("MY GAME") (:17) ทำหน้า Start ให้ในตัว (Start=เล่น Back=ออก) เราไม่ต้องเขียนเอง มันเป็นส่วนของ 70% core ที่ย้ำไว้ในสไลด์ 70/30
  • player / target / label (:21-23) ประกาศ นอก update() ครั้งเดียว — ตรงกับสไลด์ ขั้น C กับดัก 32 widget เป๊ะ: widget สร้างครั้งเดียว ในลูปแค่ .move_to() / .set() ถ้าเผลอย้ายสามบรรทัดนี้เข้าไปใน update() เกมจะไต่ชน cap 32 แล้วค้างราวครึ่งนาที
  • game.WIDTH // 2, game.HEIGHT // 2 (:21) วางผู้เล่นกลางจอด้วยขนาดจอจริง ไม่ใช่เลขพิกัดตายตัว — artifact จึงรันได้บนบอร์ดทุกขนาดจอ ไม่ผูกกับความละเอียดของเครื่องเรา (นี่คือหน้าตาของคำว่า reproducible)
  • score = 0 (:20) คือ known start state เดียวกับที่สไลด์ ขั้น A พูด: กด START = soft reset → รันไฟล์ใหม่ตั้งแต่บรรทัดนี้ → score กลับเป็น 0 เอง เราไม่ต้องเขียนโค้ดรีเซ็ตสักบรรทัด

โค้ด init ที่ดีคือ "รูปถ่ายสถานะเริ่มเกม" กด START ทีไร บอร์ดกลับมายืนที่รูปนี้เป๊ะทุกครั้ง

เฉลย · เงื่อนไขจบ + game.run() — จุดที่ทำให้ "ครบรอบ"

    # 3) WIN / LOSE — ชนเป้า = +1 แต้ม, ครบ WIN_SCORE = ชนะ
    if game.hit(player, target):
        score += 1
        label.set("Score: %d" % score)
        game.sfx("point")
        _move_target()
        if score >= WIN_SCORE:
            game.Text("YOU WIN!", game.WIDTH // 2 - 70, game.HEIGHT // 2, game.GREEN)
            game.sfx("win")
            return False

    return True  # เล่นต่อ


game.run(update, fps=FPS)
  • game.hit(player, target) (:51) คือ AABB ชนกล่อง — pattern เดียวกับสไลด์ ขั้น B (Snake กินอาหาร / Flappy ลอดท่อ) เปลี่ยนแค่ "ชนแล้วเกิดอะไร" ให้เป็นกติกาของกลุ่มเอง
  • return False เมื่อ score >= WIN_SCORE (:56-59) คือ terminal state ที่ผัง update() ก่อนหน้านี้เรียกว่า "สภาวะจบ" — game.run() เห็น False แล้วขึ้น GAME OVER รอ START
  • return True (:61) = เล่นต่อเฟรมถัดไป ทุกเฟรมต้องตอบ run() ว่าจบยัง นี่คือ "สัญญา" ระหว่าง 30% ที่เราเขียน กับ 70% ที่วนให้
  • game.run(update, fps=FPS) (:64) เรียก ครั้งเดียว ปิดท้ายไฟล์ เราไม่เขียน while True เอง run() เป็นตัววน + จัดการ Start/Back ให้ — เกมครบรอบโดยเราเขียนแค่กติกา นี่คือทั้งไฟล์ที่จะ ship

artifact ที่ส่งมอบไม่มี loop รอบนอกเลย เราส่ง "กติกา" ให้เฟิร์มแวร์ที่เพื่อนมีอยู่แล้วเป็นคนวน นั่นคือเหตุผลที่ไฟล์เล็กจนแนบไฟล์เดียวก็พอ

เฉลย · 3 ช่องประกอบกันเป็น "เกมที่ส่งมอบได้" ยังไง

ช่อง บรรทัด หน้าที่ในเกม บทบาทตอน Release / ส่งมอบ
ค่าปรับ (const) :13-15 PLAYER_SPEED / FPS / WIN_SCORE ปุ่มปรับที่คนรับแก้ได้โดยไม่แตะ logic
STATE (init) :19-23 สร้าง widget + score=0 ครั้งเดียว known start state ที่ START รีเซ็ตกลับมา · ไม่ชน cap 32
TICK (update) :38-46 อ่าน keys()player.move 30% ที่เราเขียน · run() วนให้ทุกเฟรม
WIN / LOSE :51-61 hit → +แต้ม → return False / True terminal state ที่ทำให้เกม "ครบรอบ"
hook :64 game.run(update, fps=FPS) เรียกครั้งเดียว = ไม่ต้องแนบ loop · แนบแค่ไฟล์นี้
  • ไล่จากบนลงล่าง แต่ละช่องเพิ่ม "ความรับผิดชอบ" ทีละชั้นบนโครงเดิม: ค่า → หน่วยความจำเริ่มต้น → กติกาต่อเฟรม → กติกาจบ → มอบให้ 70% เป็นคนวน
  • ถอดช่องไหนออก artifact พังคนละแบบ: ไม่มี WIN → เกมไม่จบรอบ (เล่นไปเรื่อย ไม่มี GAME OVER) · STATE ย้ายเข้าลูป → ค้างที่ cap 32 · ลืม returnrun() ไม่รู้ว่าจบเมื่อไหร่
  • ทั้งหมดนี้อยู่ใน 30% ที่เราเขียน ส่วน Box / Text / keys / hit / sfx / run (70%) bentogame frozen ให้มาแล้ว — นี่คือเหตุผลจริงที่ Release แนบ mygame.py ไฟล์เดียวจบ

เกมเต็มไม่ใช่โค้ดยาว ๆ มันคือ 3 ช่องเล็ก ๆ ที่ต่อกันถูกที่ แล้วปล่อยให้ core ที่ทุกคนมีเหมือนกันเป็นคนรัน

เชื่อมจุด — "ส่งมอบ" มาจากไหน แล้วจะพาเราไปถึงไหน

ที่มา — "ส่งมอบ" ยืนอยู่บนอะไร ย้อนไปสไลด์ 3 ไอเดียของคาบสุดท้าย ข้อ #2 บอกไว้ว่า "ส่งมอบ ≠ ทำเสร็จ" โค้ดเสร็จเมื่อคนอื่นเอาไปรันเองได้ ไอเดียนี้ไม่ได้เพิ่งเกิดวันนี้ มันต่อยอดจากสไลด์ โครง 70/30 โดยตรง: ที่เราแยก 70% core (frozen ในเฟิร์มแวร์) ออกจาก 30% ที่เราเขียน ก็เพื่อให้วันนี้แนบแค่ mygame.py ไฟล์เดียว เพื่อนที่มี core เหมือนกันก็รันต่อได้ ปัญหาดั้งเดิมที่ "ส่งมอบ" มาแก้คือ "โค้ดที่รันได้แค่บนบอร์ดของคนเขียน" ซึ่งไร้ค่ากับทุกคนที่เหลือ

ที่ไป — ทักษะนี้จะโตเป็นอะไร เส้นทาง branch → PR → CI เขียว → merge → tag → Release ในสไลด์ ขั้น D ไม่ใช่พิธีกรรมเฉพาะคาบนี้ มันคือ release lifecycle ที่ทีมซอฟต์แวร์จริงใช้กับทุก build ไม่ว่าจะเป็นแอปมือถือหรือเฟิร์มแวร์ในรถ วันนี้ artifact คือ mygame.py 65 บรรทัด พรุ่งนี้ artifact เดียวกันคือเฟิร์มแวร์ทั้งตัว หรือ container ของบริการที่คนล้านคนใช้ — กระบวนการเท่าเดิม

  • จำสไลด์ 70/30 ที่บอกว่า bentogame ฝังในเฟิร์มแวร์ (frozen) ได้ไหม ถ้า core ไม่ frozen เราจะต้องแนบอะไรเพิ่มใน Release บ้าง แล้วเพื่อนจะรันยากขึ้นแค่ไหน?
  • ถ้าวันนี้เราทำให้ "เพื่อนอีกกลุ่มโหลด mygame.py ไปรันได้เอง" ได้ พรุ่งนี้เราจะทำให้ "คนที่เราไม่เคยเจอ ดาวน์โหลดจากอินเทอร์เน็ตไปรัน" ได้ไหม ต่างกันแค่ไหน? (ใบ้: หน้า Release บน GitHub เปิดให้ทุกคนโหลดอยู่แล้ว)
  • สังเกตไหมว่า "กด START แล้ว score กลับเป็น 0" (known start state จากขั้น A) กับ "เพื่อนโหลดไปรันได้ผลเหมือนเรา" (reproducible) จริง ๆ คือเรื่องเดียวกัน — เริ่มจากสถานะที่คาดเดาได้เสมอ?
ที่มา 70/30 + core frozen (แยกของที่ต้องแนบ) วันนี้ ship mygame.py ไฟล์เดียว (เพื่อนโหลดไปรันได้) ที่ไป release lifecycle ทุก build (แอป/เฟิร์มแวร์/บริการ)

การหยั่งรู้ที่อยากให้เกิดคือ "ส่งมอบ" ไม่ใช่ขั้นตอนสุดท้ายที่ต่อท้ายงาน แต่คือเหตุผลที่เราออกแบบ 70/30 มาตั้งแต่ต้นคอร์ส

ใช้จริงที่ไหน — Release · artifact · frozen core

สี่เทคนิควันนี้ไม่ได้อยู่แค่ในคาบเรียน เฟิร์มแวร์กับซอฟต์แวร์จริงใช้ทั้งสี่อย่างนี้ทุกวัน:

App store / OTA — ส่ง artifact ที่ build แล้ว Release .apk / .bin เครื่อง ผู้ใช้ ผู้ใช้ดึงไปรันเอง ไม่ต้องมี source runtime ฝังในเครื่อง — ส่งแค่โค้ด mygame.py เครื่องของเพื่อน bentogame (frozen) engine ฝังมาแล้ว — แนบแค่ไฟล์เกม semantic versioning — tag แต่ละ Release v1.0 v1.1 v1.2 รู้ว่ารันเวอร์ชันไหน · rollback ได้ CI gate — เขียวก่อนถึง merge PR CI เขียว เทสต์ผ่าน merge CI ไม่ผ่าน = กันไว้ ไม่ให้โค้ดพังหลุดถึงผู้ใช้
  • App store / OTA update — แอปมือถือกับเฟิร์มแวร์ IoT ส่ง "artifact ที่ build แล้ว" (.apk / .bin) ให้เครื่องผู้ใช้ดึงไปรันเอง เหมือนเพื่อนโหลด mygame.py จากหน้า Release (ขั้น D–E) — คนใช้ไม่ต้องมี source ก็รันได้
  • runtime / SDK ฝังในเครื่อง — Python มี stdlib ติดมากับ interpreter, เกม console มี engine ฝังในตัว เราส่งแค่โค้ดเกม — หลักเดียวกับ bentogame frozen ที่ทำให้ Release แนบไฟล์เดียวพอ (สไลด์ 70/30)
  • semantic versioning + tag — ติด tag v1.0, v1.1 บนทุก Release เพื่อรู้ว่ารันเวอร์ชันไหนและ rollback ได้ (tag v1.0 ใน ขั้น D) เลขเวอร์ชันคือสัญญาว่า "artifact นี้คือชุดนี้ ไม่ใช่ชุดอื่น"
  • CI gate ก่อน merge — GitHub Actions รันเทสต์อัตโนมัติ ต้องเขียวก่อนถึง merge ได้ (สไลด์ CI เขียวก่อน merge) — ด่านที่กันโค้ดพังไม่ให้หลุดถึงมือผู้ใช้

เทคนิควันนี้ไม่ใช่กิจกรรมในห้องเรียน มันคือขั้นตอนที่ทีมซอฟต์แวร์ทั่วโลกเดินซ้ำทุกครั้งที่ปล่อยของออกจากมือ

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

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

ก่อน — "ทำเสร็จ" (รันได้แค่บนเครื่องเรา) โค้ดบนบอร์ดคนเขียน ผูกกับเครื่องเรา เพื่อนรันเองไม่ได้ ย้ายเครื่องเมื่อไร = รันไม่ได้ ไร้ค่ากับคนอื่น เปลี่ยนวิธีคิด หลัง — "ส่งมอบ" (artifact ที่ใครก็รันได้) Release: mygame.py ใครก็โหลดไปได้ กด Program to Device ไม่ต้องมีเราอยู่ด้วย — เพื่อนรันเองได้เลย
  • ถ้า Release ต้องมี "วิธีเล่น" ให้เพื่อนอ่าน จะออกแบบ README ของเกมยังไง ให้คนที่ไม่เคยเจอเราโหลดไปแล้วรันถูกตั้งแต่ครั้งแรก ต้องบอกอะไรบ้าง (ปุ่มไหนทำอะไร / เงื่อนไขชนะ / วิธี Program to Device)?
  • ถ้าอยากให้เกมเก็บ best score ข้ามรอบ ที่ START ล้าง RAM ทุกครั้ง จะออกแบบให้เซฟลงไฟล์ (best.txt) ยังไง ให้ artifact ที่ส่งไปแล้วเพื่อนเล่นแล้วสถิติไม่หาย — ต้องแนบ best.txt ไปด้วยไหม หรือให้โค้ดสร้างเองตอนรันครั้งแรก?
  • ถ้าสองกลุ่มอยากรวมเกมเป็นชุดเดียว (เมนูเลือกเกม) จะออกแบบให้แต่ละ mygame.py อยู่ร่วมกันได้ยังไง โดยไม่ชน widget cap 32 และไม่แก้ bentogame — ใครโหลดของใครมาต่อ แล้วตั้งชื่อไฟล์กันชนยังไง?
  • ถ้าให้ CI ตรวจเกมอัตโนมัติก่อน merge จะออกแบบเทสต์อะไรที่ "รันเกมโดยไม่มีคนกดจอย" เพื่อพิสูจน์ว่า mygame.py ไม่ค้าง / ครบรอบจริง — คำถามนี้พาเราเข้าสู่ DevOps เต็มรูป + AI agent ที่รออยู่ในคอร์ส Developer III

เลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากเกมที่น้อง ๆ เพิ่ง ship เอง — ตรงนั้นแหละคือจุดที่คนเขียนเกมกลายเป็นคนส่งมอบซอฟต์แวร์

fit-css

← Roadmap (TOC)