คาบ 13 — คาบพิเศษ

กล่องเครื่องมือนักพัฒนาเกม: จากเกมสู่หน้าจอควบคุมในงานจริง

12 คาบที่ผ่านมา น้อง ๆ เก็บเทคนิคไว้ครบแล้ว ตั้งแต่ไฟ LED ปุ่ม จอ จอย ฟิสิกส์ เสียง จนถึงชุดหมุนเวียนและการชน

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

วันนี้ดูอะไรบ้าง

ส่วน เรื่อง มาจากคาบ
1 หัวใจดวงเดียว: วงวนของเกม = วงวนของเครื่องควบคุม 3–4
2 การเคลื่อนที่และฟิสิกส์: การ์ดสมการ 6 ใบ 3 · 5 · 6 · 7 · 10 · 11
3 ของหลายชิ้น: ชุดหมุนเวียน + ชนแบบกล่อง + งบของบนจอ 4 · 11 · 12
4 สถานะ: ไฟจราจร → เมนูเกม → เครื่องจักร 2 · 7 · 12
5 HUD ในเกม และหน้าจอควบคุมในโรงงาน 4 · 10 · 12
6 ภาพ เสียง และความสนุกจากคณิตศาสตร์ 8 · 11 · 12 · เกมตัวเต็ม
7 จากเกมสู่งานจริง และสิ่งที่จะเกิดสัปดาห์หน้า —

ทุกการ์ดมีจังหวะเดียวกัน: ดูสมการ → ดูโค้ดจริง → ลองเล่นโค้ดชุด (คัดลอกไปวางใน BENTO Emulator) → ลองแก้ตัวเลข → จำกฎหนึ่งบรรทัด

วิธีเล่นโค้ดชุดของคาบนี้ใน BENTO Emulator

ทุกหน้าที่มีหัวข้อ ลองเล่น มีโค้ดชุดพร้อมเล่นหนึ่งไฟล์ ตามด้วยหน้า โค้ดเต็ม พร้อมปุ่มคัดลอก · ชุดหยิบใช้จากเกมตัวเต็มมีปุ่มคัดลอก ทีละชิ้น ด้วย

  1. เปิด BENTO IDE ide.tesaiot.dev ด้วย Chrome หรือ Edge
  2. ในหน้า โค้ดเต็ม กดปุ่ม คัดลอกโค้ด
  3. วางในช่องเขียนโค้ดของ BENTO IDE แล้วกด Run your code in the BENTO Emulator
  4. หน้าแรกขึ้น "Start = Play" — กด Enter แล้วเล่นได้เลย · อยากลองบนบอร์ดจริง ใช้ไฟล์เดียวกันกด Program to Device
ปุ่มบนจอย คีย์บอร์ดใน Emulator
Start · Back Enter · Backspace
ลูกศร (D-pad) ปุ่มลูกศร
A · B · X · Y Z หรือ Space · X · C · V

ในคาบเล่นไฟล์ ★ 6 ไฟล์ ตามใบงาน: s13_02 · s13_06 · s13_09 · s13_tank_hmi · s13_13 · s13_14 — ไฟล์ที่เหลือ 15 ไฟล์ลองแก้ที่บ้าน (ภารกิจลองแก้ครบทุกตัวอย่าง ส่ง 13 ต.ค.) · ทุกไฟล์คัดลอกไฟล์เดียวก็เล่นได้

โค้ดชุดทั้งหมดของคาบนี้ (1/2)

ไฟล์ (กดแล้วเปิดแท็บใหม่) เรื่อง
s13_01_one_loop.py↗ ส่วนที่ 1: หัวใจดวงเดียว
s13_02_integrate.py↗ การ์ด 1: สะสมค่าทีละเฟรม ลูกบอลกับถังน้ำใช้สมการเดียวกัน
s13_03_bounce_angle.py↗ การ์ด 2: สะท้อน และมุมกระทบ
s13_04_accel_friction.py↗ การ์ด 3: เร่ง หน่วง และเพดาน (ลื่นหรือหนืด ลองรู้สึกเอง)
s13_05_clamp_ramp.py↗ การ์ด 4: จำกัดขอบ และเร่งแบบมีเพดาน
s13_06_deadzone.py↗ การ์ด 5: ตัวควบคุมสามระดับ + ช่องว่าง (เห็นไม้สั่นกับตา)
s13_07_frame_timer.py↗ การ์ด 6: เวลาคือการนับเฟรม (ตัวจับเวลาสามตัวบนจอเดียว)
s13_08_pool.py↗ ส่วนที่ 3: ชุดหมุนเวียน (ยืม-คืน ไม่สร้างใหม่ระหว่างเล่น)
s13_09_aabb.py↗ ชนแบบกล่อง เห็นสี่เงื่อนไขเป็น True/False สด ๆ บนจอ
s13_10_fsm_machine.py↗ ส่วนที่ 4: เครื่องสถานะของเครื่องซักผ้า (อยู่ในสถานะเดียวเสมอ)
s13_11_game_hud.py↗ HUD ในเกม

โค้ดชุดทั้งหมดของคาบนี้ (2/2)

ไฟล์ (กดแล้วเปิดแท็บใหม่) เรื่อง
s13_12_sprite_gallery.py↗ ห้องแสดงภาพจากเกมตัวเต็มครบ 20 แบบ
s13_13_reskin_farm.py↗ เอาภาพเดิมไปเล่าเรื่องใหม่
s13_14_sound_board.py↗ แผงเสียง 21 เสียง · ในเกมคือรางวัล ในโรงงานคือคำเตือน
s13_15_game_juice.py↗ ความสนุกที่มาจากคณิตศาสตร์ที่เรียนแล้ว (game juice)
s13_16_kit_characters.py↗ ชุดตัวละครสำเร็จรูป ยกมาจากเกมเต็มของจริง หยิบไปใส่เกมของน้องได้ทันที
s13_17_kit_scenes.py↗ ชุดฉากสำเร็จรูป ยกมาจากเกมเต็มของจริง พร้อมราคาวิดเจ็ตของแต่ละฉาก
s13_18_kit_props_hud.py↗ ชุดของประกอบ + ป้าย HUD สำเร็จรูป ยกมาจากเกมเต็มของจริง
s13_19_kit_sounds.py↗ สูตรเสียงประกอบจากเกมเต็มของจริง เหตุการณ์ไหนใช้เสียงไหน + ทำนองแบบไม่ใช้ sleep
s13_conveyor_sort.py↗ · ฝึกเติมเอง practise↗ สายพานคัดแยกพัสดุ
s13_tank_hmi.py↗ · ฝึกเติมเอง practise↗ หน้าจอควบคุมถังน้ำโรงเรือน

ส่วนที่ 1 — หัวใจดวงเดียว

เกม (ทุก 1/30 วินาที) หน้าจอควบคุม (ทุกรอบ)
เข้า รับปุ่ม k = game.keys() วัด อ่านปุ่ม เซนเซอร์ ตัวนับ
คิด อัปเดต: ขยับ ชน นับคะแนน ตัดสิน เกินเกณฑ์ไหม ต้องเปลี่ยนสถานะไหม
ออก วาด: move_to() set() เสียง ทำ + โชว์ สั่งเครื่อง อัปเดตจอ เสียงเตือน
def update():
    k = game.keys()            # เข้า: รับปุ่ม (วัด)
    # ... ขยับ ชน นับคะแนน     # คิด: อัปเดต (ตัดสิน)
    box.move_to(x, y)          # ออก: วาด (ทำ + โชว์)
    return True                # คืน False เมื่อต้องการจบ

game.run(update, fps=30)       # เรียก update() 30 ครั้งต่อวินาที

กฎ: ทุกระบบเรียลไทม์คือวงวนเดียวกัน ต่างกันแค่ว่าอะไรเข้า อะไรออก

ลองเล่น · ส่วนที่ 1: หัวใจดวงเดียว

คัดลอกไฟล์ s13_01_one_loop.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอย 4 ทิศ = ขับยาน · A B X Y = ปุ่มโน้ต (ทุกปุ่มรวมกัน = โด ถึง โด สูง)
  • ดูแถบบน 3 ช่อง: เข้า = ปุ่มที่กดอยู่ · คิด = ตำแหน่งที่คำนวณได้ · ออก = นับเฟรม
  • กดค้างไว้ เสียงดังแค่ครั้งแรก ไฟ "เสียง" ติดแวบเดียว — นี่คือการจับจังหวะกด (edge)
  • ดันยานชนขอบ แล้วดูช่อง "คิด" ขึ้นคำว่า ชนขอบ

ลองแก้ แล้วรันใหม่

  1. FPS = 30 -> 10 : ยานช้าลง 3 เท่า ทั้งที่ STEP เท่าเดิม — ความเร็วจริง = STEP x FPS พิกเซลต่อวินาที
  2. เปลี่ยน game.pressed_once(name, k) เป็น getattr(k, name) : กดค้างแล้วโน้ตดังรัวทุกเฟรม ไฟเสียงติดค้าง นี่คือเหตุผลที่ปุ่มยิงกับปุ่มรับทราบสัญญาณเตือนต้องจับจังหวะกด
  3. ในฟังก์ชัน show() เปลี่ยน if text != shown[key]: เป็น if True: : จอยังถูก แต่ส่งข้อความข้ามแกนเพิ่มเฟรมละ 2 ครั้ง บนบอร์ดจริงจะเริ่มหน่วงเมื่อของบนจอเยอะ — วาดเฉพาะที่เปลี่ยนคือกฎของหน้าจอควบคุมด้วย

โค้ดเต็ม · s13_01_one_loop.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_01_one_loop.py — คาบ 13 (คาบพิเศษ) ส่วนที่ 1: หัวใจดวงเดียว = เข้า -> คิด -> ออก ทุกเฟรม
#
# หลักการ   : ทุกระบบเรียลไทม์คือวงวนเดียว game.run() เรียก on_frame() 30 ครั้งต่อวินาที (คาบ 3-4)
#             เข้า (วัด)          อ่านปุ่ม k = game.keys() และดูว่าปุ่มไหน "เพิ่งกด"    (คาบ 2)
#             คิด (ตัดสิน)        ตำแหน่งใหม่ = เดิม + ก้าว แล้วจำกัดขอบ              (คาบ 3)
#             ออก (ทำ + โชว์)     ship.move_to() · hud.set() · เสียงสั้นตอนเพิ่งกด    (คาบ 8)
# ลองเล่น   : กด Start · จอย 4 ทิศ = ขับยาน · A B X Y = ปุ่มโน้ต (ทุกปุ่มรวมกัน = โด ถึง โด สูง)
#             ดูแถบบน 3 ช่อง: เข้า = ปุ่มที่กดอยู่ · คิด = ตำแหน่งที่คำนวณได้ · ออก = นับเฟรม
#             กดค้างไว้ เสียงดังแค่ครั้งแรก ไฟ "เสียง" ติดแวบเดียว — นี่คือการจับจังหวะกด (edge)
#             ดันยานชนขอบ แล้วดูช่อง "คิด" ขึ้นคำว่า ชนขอบ
# ของบนบอร์ด: จอ · จอย · ปุ่ม · ลำโพง
# ในเกม     : ทุกเกมของคอร์ส (s04_catch · pong · shooter) คือฟังก์ชัน on_frame() หน้าตาแบบนี้
# ในงานจริง : เครื่องควบคุมในโรงงาน (PLC) ก็วนแบบนี้ทุกรอบ: อ่านเซนเซอร์ -> ตัดสิน -> สั่งเครื่อง + อัปเดตจอ
#             ตัวเลขในไฟล์นี้เป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s03_box_joystick.py (ขยับตามจอย + ขอบ) · s02_button_fsm.py (จับจังหวะกด)
#             s08_sound.py (แต่งโน้ตเอง)
# งบวิดเจ็ต : 14 ชิ้น (กรอบหัว 3 + หัวข้อ 3 + ค่า 3 + สนาม 1 + ยาน 1 + ไฟเสียง 1 + ป้ายเสียง 1 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ (ไฟล์นี้ข้าม)
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30            # เฟรมต่อวินาที (game.run เรียก on_frame กี่ครั้งต่อวินาที)
STEP = 6            # ยานขยับกี่พิกเซลต่อเฟรมเมื่อกดจอย
LAMP_FRAMES = 8     # ไฟเสียงติดค้างกี่เฟรมหลังโน้ตดัง (นับเฟรม ไม่ใช้ sleep)
NOTE = {"left": 60, "down": 62, "up": 64, "right": 65,     # โด เร มี ฟา
        "a": 67, "b": 69, "x": 71, "y": 72}                # ซอล ลา ที โด(สูง)
ORDER = ("left", "right", "up", "down", "a", "b", "x", "y")
AREA_X, AREA_Y, AREA_W, AREA_H = 8, 74, 776, 290           # สนามที่ยานวิ่งได้
SHIP_W, SHIP_H = 62, 28                                    # ขนาดภาพ ship
X_MIN, X_MAX = AREA_X + 4, AREA_X + AREA_W - SHIP_W - 4
Y_MIN, Y_MAX = AREA_Y + 4, AREA_Y + AREA_H - SHIP_H - 4
PANEL, GRAY = 0x16222E, 0x556070


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ ----
def clamp(value, low, high):
    return max(low, min(high, value))


def held_names(k):
    # เข้า: แปลงปุ่มที่กดอยู่ให้เป็นข้อความ เช่น "LEFT UP A" (ไม่กดเลย = "-")
    names = [name.upper() for name in ORDER if getattr(k, name)]
    return " ".join(names) if names else "-"


def next_pos(x, y, k):
    # คิด: ก้าวตามจอย แล้วจำกัดขอบ คืน (x ใหม่, y ใหม่, ชนขอบไหม)
    want_x = x + (STEP if k.right else 0) - (STEP if k.left else 0)
    want_y = y + (STEP if k.down else 0) - (STEP if k.up else 0)
    new_x, new_y = clamp(want_x, X_MIN, X_MAX), clamp(want_y, Y_MIN, Y_MAX)
    return new_x, new_y, (new_x != want_x or new_y != want_y)


# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("ONE LOOP")
for col, head, color in ((8, "เข้า = วัด", game.CYAN), (272, "คิด = ตัดสิน", game.YELLOW),
                         (536, "ออก = ทำ + โชว์", game.GREEN)):
    game.Box(col, 2, 248, 64, PANEL, border=color, border_w=2, radius=6)
    game.Text(head, col + 12, 8, color)
in_text = game.Text("-", 20, 36, game.WHITE)
think_text = game.Text("", 284, 36, game.WHITE)
out_text = game.Text("", 548, 36, game.WHITE)
lamp = game.Box(712, 12, 16, 16, GRAY, radius=8)
game.Text("เสียง", 734, 8, game.WHITE)
game.Box(AREA_X, AREA_Y, AREA_W, AREA_H, 0x0E1620, border=0x334455, border_w=1)
x, y = (X_MIN + X_MAX) // 2, (Y_MIN + Y_MAX) // 2
ship = game.Sprite("ship", x, y)

frame, lamp_left = 0, 0
shown = {"in": "-", "think": ""}


def show(widget, key, text):
    # อัปเดตป้ายเฉพาะตอนข้อความเปลี่ยน (ทุก set() = หนึ่งข้อความข้ามแกนไปวาดจอ)
    if text != shown[key]:
        widget.set(text)
        shown[key] = text


# ---- 5) วงวนหลัก ----
def on_frame():
    global x, y, frame, lamp_left
    # ===== เข้า (วัด): อ่านปุ่มเฟรมนี้ + หาปุ่มที่ "เพิ่งกด" =====
    k = game.keys()
    note = None
    for name in ORDER:                              # ต้องเรียกทุกปุ่มทุกเฟรม ให้มันจำเฟรมก่อนได้
        if game.pressed_once(name, k):
            note = NOTE[name]                       # กดค้าง = True แค่เฟรมแรก

    # ===== คิด (ตัดสิน): คำนวณสถานะใหม่ ยังไม่แตะจอเลย =====
    frame += 1
    new_x, new_y, at_edge = next_pos(x, y, k)
    moved = new_x != x or new_y != y
    x, y = new_x, new_y

    # ===== ออก (ทำ + โชว์): วาด เสียง และป้าย เฉพาะที่เปลี่ยน =====
    if moved:
        ship.move_to(x, y)
    show(in_text, "in", held_names(k))
    show(think_text, "think", "x %d  y %d%s" % (x, y, "  ชนขอบ" if at_edge else ""))
    out_text.set("เฟรม %d  (%d วิ)" % (frame, frame // FPS))     # เปลี่ยนทุกเฟรม = 1 ข้อความ
    if note is not None:
        game.tone(note, ms=120)                     # เสียงสั้น ไม่ต่อคิว ไม่ต้อง sleep
        if lamp_left == 0:
            lamp.set_color(game.ORANGE)
        lamp_left = LAMP_FRAMES
    elif lamp_left > 0:                             # นับถอยหลังแล้วดับไฟ
        lamp_left -= 1
        if lamp_left == 0:
            lamp.set_color(GRAY)
    return True                                     # คืน False เมื่ออยากจบเกม


game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) FPS = 30 -> 10 : ยานช้าลง 3 เท่า ทั้งที่ STEP เท่าเดิม — ความเร็วจริง = STEP x FPS พิกเซลต่อวินาที
# 2) เปลี่ยน game.pressed_once(name, k) เป็น getattr(k, name) : กดค้างแล้วโน้ตดังรัวทุกเฟรม ไฟเสียงติดค้าง
#    นี่คือเหตุผลที่ปุ่มยิงกับปุ่มรับทราบสัญญาณเตือนต้องจับจังหวะกด
# 3) ในฟังก์ชัน show() เปลี่ยน if text != shown[key]: เป็น if True: : จอยังถูก แต่ส่งข้อความข้ามแกนเพิ่มเฟรมละ 2 ครั้ง
#    บนบอร์ดจริงจะเริ่มหน่วงเมื่อของบนจอเยอะ — วาดเฉพาะที่เปลี่ยนคือกฎของหน้าจอควบคุมด้วย

12 คาบในหน้าเดียว

คาบ เทคนิคที่ได้ ไฟล์
1 สั่ง LED · จังหวะเปิด-ปิด (duty cycle) s01_led_blink↗
2 จับจังหวะกดปุ่ม (edge) · เครื่องสถานะไฟจราจร s02_button_fsm↗ s02_traffic_v2↗
3 กล่องตามจอย · จำกัดขอบ · เร่งแบบมีเพดาน s03_box_joystick↗
4 วงวนเกม · ชนแบบกล่อง · คะแนน · ตัวจับเวลา s04_catch↗
5 สะสมค่าทีละเฟรม · สะท้อน pong_step1↗
6 เร่ง · แรงเสียดทาน · ไม้มีน้ำหนัก pong_step2↗ pong_step3↗
7 มุมกระทบ · AI สามระดับ + ช่องว่าง · สถานะแมตช์ pong_step4↗ pong_step5↗
8 เสียงสำเร็จรูป · แต่งโน้ตเอง s08_sound↗
9 เอียงบอร์ด → ความเร็ว s09_tilt↗
10 ยานมีน้ำหนัก · HUD คะแนน/ชีวิต shooter_step1↗ shooter_step2↗
11 ชุดหมุนเวียน · คูลดาวน์ shooter_step3↗
12 ชนหลายคู่ · ชีวิต · GAME OVER · ตัวจับเวลาปล่อยศัตรู shooter_step4↗ shooter_step5↗

ทุกไฟล์อยู่ใน solution_codes/↗ เปิดรันได้ทันที

การ์ด 1 · สะสมค่าทีละเฟรม (คาบ 5)

xใหม่=x+vxyใหม่=y+vyx_{\text{ใหม่}} = x + v_x \qquad y_{\text{ใหม่}} = y + v_y

ball_x += ball_vx       # ขยับลูกตามความเร็ว      (pong_step1.py)
ball_y += ball_vy
ในเกม ในงานจริง
ลูกบอลวิ่ง · ยานลอย · ศัตรูตก ระดับน้ำในถัง = เดิม + น้ำเข้า − น้ำออก · ระยะทางที่รถ AGV วิ่งได้ · ยอดชิ้นงานสะสม

กฎ: ตำแหน่งไม่ได้ถูกกำหนดทีเดียว แต่ถูก สะสม ทีละเฟรม

ลองเล่น · การ์ด 1: สะสมค่าทีละเฟรม ลูกบอลกับถังน้ำใช้สมการเดียวกัน

คัดลอกไฟล์ s13_02_integrate.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยซ้าย/ขวา = ลด/เพิ่ม v ทีละ 1 · จอยขึ้น/ลง = เพิ่ม/ลดน้ำเข้าทีละ 0.10
  • A = เริ่มใหม่ · ดูบรรทัดสีขาวสองบรรทัด: สมการเดียวกันกำลังคิดเลขจริงทุกเฟรม
  • ขีดเหลืองใต้ลูก = ที่อยู่ของลูกทุก 6 เฟรม ยิ่ง v มากขีดยิ่งห่าง (ห่าง = 6 x v)
  • ตั้งน้ำเข้า 0.30 ให้เท่าน้ำออก แล้วดูระดับน้ำนิ่ง — เหมือนลูกตอน v = 0

ลองแก้ แล้วรันใหม่

  1. TRAIL_EVERY = 6 -> 1 : ขีดเหลืองห่างกันเท่า v พอดี — นี่คือ "ก้าว" หนึ่งเฟรมของลูก
  2. OUT = 0.30 -> 0.0 แล้วกดน้ำเข้าลงจนเป็น 0.00 : ระดับน้ำนิ่ง เหมือนลูกตอน v = 0 ทุกอย่าง
  3. ลบบรรทัด level = max(0.0, min(100.0, level)) : สมการบอกน้ำ 130% หรือ -20% ซึ่งไม่มีจริง การ์ด 4 (จำกัดขอบ) จึงต้องมาคู่กับการ์ด 1 เสมอ

โค้ดเต็ม · s13_02_integrate.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_02_integrate.py — คาบ 13 (คาบพิเศษ) การ์ด 1: สะสมค่าทีละเฟรม ลูกบอลกับถังน้ำใช้สมการเดียวกัน
#
# หลักการ   : การ์ด 1 สะสมค่าทีละเฟรม   ค่าใหม่ = ค่าเดิม + ที่เปลี่ยนต่อเฟรม               (คาบ 5)
#             ลูกบอล   x = x + v                        แบบเดียวกับ ball_x += ball_vx ใน pong_step1.py
#             ถังน้ำ    ระดับ = ระดับ + น้ำเข้า - น้ำออก   แบบเดียวกับ s13_tank_hmi.py
#             ถังมีขอบ 0 ถึง 100 (การ์ด 4 คาบ 3) · ลูกวิ่งทะลุขวาแล้วโผล่ซ้าย (วนรอบ)
# ลองเล่น   : กด Start · จอยซ้าย/ขวา = ลด/เพิ่ม v ทีละ 1 · จอยขึ้น/ลง = เพิ่ม/ลดน้ำเข้าทีละ 0.10
#             A = เริ่มใหม่ · ดูบรรทัดสีขาวสองบรรทัด: สมการเดียวกันกำลังคิดเลขจริงทุกเฟรม
#             ขีดเหลืองใต้ลูก = ที่อยู่ของลูกทุก 6 เฟรม ยิ่ง v มากขีดยิ่งห่าง (ห่าง = 6 x v)
#             ตั้งน้ำเข้า 0.30 ให้เท่าน้ำออก แล้วดูระดับน้ำนิ่ง — เหมือนลูกตอน v = 0
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : ลูกบอลวิ่ง · ยานลอย · ศัตรูตก (pong_step1.py · shooter_step2.py)
# ในงานจริง : ระดับน้ำในถังโรงเรือน · ระยะทางที่รถ AGV วิ่งได้ · ยอดชิ้นงานสะสมบนสายพาน
#             ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : pong_step1.py (สะสมค่า) · s13_tank_hmi.py (ถังน้ำ 10 ก้อน)
# งบวิดเจ็ต : 25 ชิ้น (หัวเรื่อง 1 + สมการ 4 + ราง 1 + ลูก 1 + ขีด 4 + ป้ายขีด 1 + ถัง 1 + แถบน้ำ 10
#             + ป้ายปุ่ม 1 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ (ไฟล์นี้ข้าม)
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
V_START, V_MAX = 4, 12          # ความเร็วลูกตอนเริ่ม และสูงสุด (พิกเซลต่อเฟรม)
IN_START, IN_MAX = 0.40, 1.00   # น้ำเข้าตอนเริ่ม และสูงสุด (% ต่อเฟรม)
OUT = 0.30                      # น้ำออกคงที่ (% ต่อเฟรม)
LEVEL_START = 30.0              # ระดับน้ำตอนเริ่ม (%)
TRAIL_EVERY = 6                 # วางขีดเหลืองทุกกี่เฟรม
LANE_X, LANE_Y, LANE_W, LANE_H = 16, 100, 370, 100
BALL = 14                       # ภาพ ball กว้าง/สูง 14 px
X_LO, X_HI = LANE_X + 4, LANE_X + LANE_W - BALL - 4
TANK_X, TANK_Y, TANK_W, TANK_H = 470, 100, 120, 262
SEG_H = 24                      # แถบน้ำหนึ่งก้อน (10 ก้อน = 240 px = 100%)


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def integrate(value, change):
    # การ์ด 1: หัวใจของไฟล์นี้ — ลูกบอลและถังน้ำเรียกฟังก์ชันนี้ตัวเดียวกัน
    return value + change


def wrap(x):
    # ลูกทะลุขวา -> โผล่ซ้าย และกลับกัน (ไม่ใช่สะท้อน สะท้อนอยู่การ์ด 2)
    span = X_HI - X_LO
    return x - span if x > X_HI else (x + span if x < X_LO else x)


def signed(value, fmt):
    # (4, "%d") -> "+ 4" · (-0.3, "%.2f") -> "- 0.30" ให้สมการบนจอเหมือนที่เขียนบนกระดาน
    return ("- " if value < 0 else "+ ") + (fmt % abs(value))


# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("INTEGRATE")
game.Text("สมการเดียว: ใหม่ = เดิม + ที่เปลี่ยน", 16, 4, game.YELLOW)
game.Text("ลูกบอล    x = x + v", 16, 36, game.CYAN)
ball_eq = game.Text("", 16, 64, game.WHITE)
game.Text("ถังน้ำ    น้ำ = น้ำ + เข้า - ออก", 420, 36, game.CYAN)
tank_eq = game.Text("", 420, 64, game.WHITE)
game.Box(LANE_X, LANE_Y, LANE_W, LANE_H, 0x16222E, border=0x334455, border_w=1)
ticks = [game.Box(-20, LANE_Y + 50, 3, 26, game.YELLOW) for _ in range(4)]
ball = game.Sprite("ball", X_LO, LANE_Y + 24)
game.Text("ขีดเหลือง = ลูกอยู่ตรงนี้ทุก %d เฟรม" % TRAIL_EVERY, 16, 210, game.YELLOW)
game.Box(TANK_X, TANK_Y, TANK_W, TANK_H, 0x1E2A36, border=0x8899AA, border_w=2)
segments = []
for i in range(10):                                   # ก้อนที่ 0 อยู่ก้นถัง
    seg = game.Box(TANK_X + 10, TANK_Y + TANK_H - 10 - (i + 1) * SEG_H + 2, TANK_W - 20, SEG_H - 4, game.BLUE)
    seg.hide()
    segments.append(seg)
game.Text("ซ้ายขวา = v  ขึ้นลง = น้ำเข้า  A = ใหม่", 16, 300, game.WHITE)

x, v, level, inflow = X_LO, V_START, LEVEL_START, IN_START
frame, next_tick = 0, 0
shown = {"bars": 0, "ball": "", "tank": "", "limit": False}


def show(widget, key, text):
    if text != shown[key]:                            # ป้ายเปลี่ยนเมื่อข้อความเปลี่ยนเท่านั้น
        widget.set(text)
        shown[key] = text


# ---- 5) วงวนหลัก ----
def on_frame():
    global x, v, level, inflow, frame, next_tick
    keys = game.keys()

    # เข้า: ปุ่มเปลี่ยน "ที่เปลี่ยนต่อเฟรม" ไม่ได้ย้ายลูกหรือเติมน้ำตรง ๆ
    if game.pressed_once("right", keys): v = min(v + 1, V_MAX)
    if game.pressed_once("left", keys):  v = max(v - 1, -V_MAX)
    if game.pressed_once("up", keys):    inflow = round(min(inflow + 0.10, IN_MAX), 2)
    if game.pressed_once("down", keys):  inflow = round(max(inflow - 0.10, 0.0), 2)
    if game.pressed_once("a", keys):
        x, v, level, inflow = X_LO, V_START, LEVEL_START, IN_START
        game.sfx("select")

    # คิด: สมการเดียวกันสองที่
    old_x, old_level = x, level
    x = wrap(integrate(x, v))                              # ลูกบอล
    level = integrate(level, inflow - OUT)                 # ถังน้ำ
    level = max(0.0, min(100.0, level))                    # การ์ด 4: ถังมีขอบเสมอ
    at_limit = level >= 100.0 or level <= 0.0
    frame += 1

    # ออก: วาด + ป้าย เฉพาะที่เปลี่ยน
    ball.move_to(x, ball.y)
    if frame % TRAIL_EVERY == 0:                           # ยืมขีดเก่าสุดมาวางใหม่ (วนรอบ 4 ขีด)
        ticks[next_tick].move_to(int(x) + 5, ticks[next_tick].y)
        next_tick = (next_tick + 1) % len(ticks)
    show(ball_eq, "ball", "%d %s = %d" % (old_x, signed(v, "%d"), old_x + v))
    note = "  เต็ม" if level >= 100.0 else ("  แห้ง" if level <= 0.0 else "")
    show(tank_eq, "tank", "%.1f %s %s = %.1f%s" % (old_level, signed(inflow, "%.2f"),
                                                    signed(-OUT, "%.2f"), level, note))
    bars = max(0, min(10, int(level) // 10))               # ถังมี 10 ก้อน · แตะเฉพาะก้อนที่เปลี่ยน
    while shown["bars"] < bars:
        segments[shown["bars"]].show()
        shown["bars"] += 1
    while shown["bars"] > bars:
        shown["bars"] -= 1
        segments[shown["bars"]].hide()
    if at_limit and not shown["limit"]:
        game.sfx("deny")                                   # ถังเต็มหรือแห้ง เตือนครั้งเดียว
    shown["limit"] = at_limit
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) TRAIL_EVERY = 6 -> 1 : ขีดเหลืองห่างกันเท่า v พอดี — นี่คือ "ก้าว" หนึ่งเฟรมของลูก
# 2) OUT = 0.30 -> 0.0 แล้วกดน้ำเข้าลงจนเป็น 0.00 : ระดับน้ำนิ่ง เหมือนลูกตอน v = 0 ทุกอย่าง
# 3) ลบบรรทัด level = max(0.0, min(100.0, level)) : สมการบอกน้ำ 130% หรือ -20% ซึ่งไม่มีจริง
#    การ์ด 4 (จำกัดขอบ) จึงต้องมาคู่กับการ์ด 1 เสมอ

การ์ด 2 · สะท้อน และมุมกระทบ (คาบ 5, 7)

vy←−vy(ชนขอบ)vy+=2⋅offset,    offset∈[−1,1](ชนไม้)v_y \leftarrow -v_y \quad \text{(ชนขอบ)} \qquad\qquad v_y \mathrel{+}= 2 \cdot \text{offset}, \;\; \text{offset} \in [-1, 1] \quad \text{(ชนไม้)}

if ball_y <= 0:          # ชนขอบบน -> กลับทิศแนวตั้ง     (pong_step1.py)
    ball_y = 0
    ball_vy = -ball_vy
    game.sfx("wall")
ในเกม ในงานจริง
Pong · ทุบอิฐ · ลูกเด้งผนัง ชิ้นงานกระทบแผ่นเบี่ยงทางบนสายพานแล้วเปลี่ยนทิศ · หุ่นยนต์ดูดฝุ่นกลับทิศเมื่อชนผนัง

กฎ: สะท้อน = กลับเครื่องหมายความเร็ว และ ดึงตำแหน่งกลับเข้าขอบ

ลองเล่น · การ์ด 2: สะท้อน และมุมกระทบ

คัดลอกไฟล์ s13_03_bounce_angle.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยขึ้น/ลง = ขยับไม้ (ซ้ายมือ) · ปล่อยให้ลูกเด้งผนังบน ล่าง ขวา
  • ลองรับลูกด้วย "ปลายบน" "กลาง" "ปลายล่าง" ของไม้ แล้วดูบรรทัด offset กับ vy
  • จุดสีบนขอบไม้ = ตำแหน่งที่ลูกเพิ่งโดน (เขียว = กลาง · เหลือง · ส้ม = ปลายไม้)
  • ผนังกะพริบขาวตอนลูกชน · รับพลาด = ลูกเสิร์ฟใหม่จากกลางจอทันที

ลองแก้ แล้วรันใหม่

  1. ลบบรรทัด vy = -vy ใต้ if by <= TOP : ลูกไถลติดผนังบน ผนังค้างสีขาวพร้อมเสียงรัว สะท้อนต้องมีครบสองอย่าง: ดึงกลับเข้าขอบ และกลับเครื่องหมาย
  2. เปลี่ยน 2 * offset เป็น 6 * offset ในฟังก์ชัน paddle_vy : ตีปลายไม้นิดเดียว ลูกชันขึ้นทันที
  3. VY_MAX = 9.0 -> 30.0 : ตีปลายไม้ซ้ำหลายครั้ง ลูกชันจนเกือบเด้งขึ้นลงอยู่กับที่ — เพดานจึงสำคัญ

โค้ดเต็ม · s13_03_bounce_angle.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_03_bounce_angle.py — คาบ 13 (คาบพิเศษ) การ์ด 2: สะท้อน และมุมกระทบ
#
# หลักการ   : การ์ด 2 สะท้อน    ชนผนัง: vy = -vy แล้วดึงลูกกลับเข้าขอบ (ต้องครบสองบรรทัด)   (คาบ 5)
#             มุมกระทบ          offset = (กลางลูก - กลางไม้) / ครึ่งไม้  อยู่ใน -1 ถึง 1
#                               vy += 2 * offset  แบบเดียวกับ pong_step3.py                  (คาบ 7)
#             vy มีเพดาน VY_MAX (การ์ด 4 คาบ 3) ลูกจะได้ไม่ชันจนเด้งขึ้นลงอยู่กับที่
# ลองเล่น   : กด Start · จอยขึ้น/ลง = ขยับไม้ (ซ้ายมือ) · ปล่อยให้ลูกเด้งผนังบน ล่าง ขวา
#             ลองรับลูกด้วย "ปลายบน" "กลาง" "ปลายล่าง" ของไม้ แล้วดูบรรทัด offset กับ vy
#             จุดสีบนขอบไม้ = ตำแหน่งที่ลูกเพิ่งโดน (เขียว = กลาง · เหลือง · ส้ม = ปลายไม้)
#             ผนังกะพริบขาวตอนลูกชน · รับพลาด = ลูกเสิร์ฟใหม่จากกลางจอทันที
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : Pong · ทุบอิฐ · ลูกเด้งผนัง (pong_step1.py ถึง pong_step3.py)
# ในงานจริง : ชิ้นงานกระทบแผ่นเบี่ยงทางบนสายพานแล้วเปลี่ยนทิศ · หุ่นยนต์ดูดฝุ่นกลับทิศเมื่อชนผนัง
#             ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : pong_step1.py (สะท้อนผนัง) · pong_step3.py (มุมกระทบ hit_offset)
# งบวิดเจ็ต : 10 ชิ้น (ผนัง 3 + ไม้ 1 + จุดโดน 1 + ลูก 1 + ข้อความ 3 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ (ไฟล์นี้ข้าม)
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
BALL_VX = 8.0                   # ความเร็วแนวนอนของลูก (คงที่ ไฟล์นี้ดูเฉพาะ vy)
SERVE_VY, VY_MAX = 2.0, 9.0     # vy ตอนเสิร์ฟ และเพดานความเร็วแนวตั้ง
PADDLE_SPEED = 9                # ไม้ขยับกี่พิกเซลต่อเฟรม
FLASH = 4                       # ผนังกะพริบกี่เฟรม (นับเฟรม ไม่ใช้ sleep)
TOP, BOTTOM, RIGHT = 70, 356, 776                     # ขอบสนามด้านใน (ผนังอยู่นอกเส้นเหล่านี้)
BALL, PADDLE_X, PADDLE_W, PADDLE_H = 14, 20, 14, 90   # ขนาดภาพ ball / paddle
WALL = 0x8899AA


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def clamp(value, low, high):
    return max(low, min(high, value))


def hit_offset(ball_y, paddle_y):
    # โดนกลางไม้ = 0 · ปลายบน = -1 · ปลายล่าง = +1 (สูตรเดียวกับ pong_step3.py แล้วจำกัดขอบ)
    return clamp(((ball_y + BALL / 2) - (paddle_y + PADDLE_H / 2)) / (PADDLE_H / 2), -1.0, 1.0)


def paddle_vy(vy, offset):
    # การ์ด 2: มุมกระทบ แล้วจำกัดเพดาน (การ์ด 4)
    return clamp(vy + 2 * offset, -VY_MAX, VY_MAX)


# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("BOUNCE")
game.Text("ชนผนัง:  vy = -vy        ชนไม้:  vy += 2 * offset", 16, 4, game.YELLOW)
hud_hit = game.Text("รับลูกด้วยไม้ แล้วดูตัวเลขตรงนี้", 16, 34, game.WHITE)
hud_score = game.Text("รับได้ 0   พลาด 0", 600, 34, game.WHITE)
walls = {"top": game.Box(0, TOP - 6, 792, 6, WALL), "bottom": game.Box(0, BOTTOM, 792, 6, WALL),
         "right": game.Box(RIGHT, TOP - 6, 16, BOTTOM - TOP + 12, WALL)}
paddle = game.Sprite("paddle", PADDLE_X, (TOP + BOTTOM - PADDLE_H) // 2)
paddle.w, paddle.h = PADDLE_W, PADDLE_H              # ภาพไม่รู้ขนาดตัวเอง ตั้งเองก่อนใช้ game.hit
mark = game.Box(-20, 0, 8, 14, game.GREEN)           # จุดที่ลูกเพิ่งโดนไม้ (จอดนอกจอไว้ก่อน)
ball = game.Sprite("ball", 396, 200)
ball.w, ball.h = BALL, BALL

bx, by, vx, vy = 396.0, 200.0, -BALL_VX, SERVE_VY
paddle_y, hits, misses, flash_wall, flash_left = paddle.y, 0, 0, None, 0


def flash(name):
    # ผนังที่ลูกชนกะพริบขาว FLASH เฟรม พร้อมเสียง wall
    global flash_wall, flash_left
    if flash_wall is not None:
        walls[flash_wall].set_color(WALL)
    flash_wall, flash_left = name, FLASH
    walls[name].set_color(game.WHITE)
    game.sfx("wall")


# ---- 5) วงวนหลัก ----
def on_frame():
    global bx, by, vx, vy, paddle_y, hits, misses, flash_wall, flash_left
    keys = game.keys()

    # เข้า: ขยับไม้แล้วจำกัดขอบ (จุดโดนเลื่อนตามไม้)
    step = (PADDLE_SPEED if keys.down else 0) - (PADDLE_SPEED if keys.up else 0)
    new_y = clamp(paddle_y + step, TOP, BOTTOM - PADDLE_H)
    if new_y != paddle_y:
        if hits:
            mark.move_to(mark.x, mark.y + new_y - paddle_y)
        paddle_y = new_y
        paddle.move_to(PADDLE_X, paddle_y)
    if flash_left > 0:                               # นับถอยหลังแล้วคืนสีผนัง
        flash_left -= 1
        if flash_left == 0:
            walls[flash_wall].set_color(WALL)
            flash_wall = None

    # คิด: สะสมค่า แล้วสะท้อน = ดึงกลับเข้าขอบ + กลับเครื่องหมาย (ต้องครบสองบรรทัด)
    bx += vx
    by += vy
    if by <= TOP:                                    # ชนผนังบน
        by = TOP
        vy = -vy
        flash("top")
    elif by >= BOTTOM - BALL:                        # ชนผนังล่าง
        by = BOTTOM - BALL
        vy = -vy
        flash("bottom")
    if bx >= RIGHT - BALL:                           # ชนผนังขวา (กลับทิศแนวนอนแทน)
        bx = RIGHT - BALL
        vx = -vx
        flash("right")
    ball.move_to(bx, by)

    # ออก: ชนไม้ = มุมกระทบ · หลุดซ้าย = พลาด เสิร์ฟใหม่
    if vx < 0 and game.hit(ball, paddle):
        offset = hit_offset(by, paddle_y)
        bx, vx = PADDLE_X + PADDLE_W, abs(vx)
        vy = paddle_vy(vy, offset)
        hits += 1
        far = abs(offset)                            # เขียว = กลางไม้ · เหลือง · ส้ม = ปลายไม้
        mark.set_color(game.GREEN if far < 0.34 else (game.YELLOW if far < 0.67 else game.ORANGE))
        mark.move_to(PADDLE_X + PADDLE_W, int(paddle_y + PADDLE_H / 2 + offset * PADDLE_H / 2) - 7)
        hud_hit.set("offset %+.2f  ->  vy += %+.2f  ->  vy %+.1f" % (offset, 2 * offset, vy))
        game.sfx("paddle")
    elif bx < -BALL:
        misses += 1
        bx, by, vx = 396.0, 200.0, -BALL_VX
        vy = SERVE_VY if misses % 2 else -SERVE_VY
        game.sfx("lose")
    else:
        return True
    hud_score.set("รับได้ %d   พลาด %d" % (hits, misses))
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) ลบบรรทัด vy = -vy ใต้ if by <= TOP : ลูกไถลติดผนังบน ผนังค้างสีขาวพร้อมเสียงรัว
#    สะท้อนต้องมีครบสองอย่าง: ดึงกลับเข้าขอบ และกลับเครื่องหมาย
# 2) เปลี่ยน 2 * offset เป็น 6 * offset ในฟังก์ชัน paddle_vy : ตีปลายไม้นิดเดียว ลูกชันขึ้นทันที
# 3) VY_MAX = 9.0 -> 30.0 : ตีปลายไม้ซ้ำหลายครั้ง ลูกชันจนเกือบเด้งขึ้นลงอยู่กับที่ — เพดานจึงสำคัญ

การ์ด 3 · เร่ง หน่วง และเพดาน (คาบ 6, 10)

v+=a  (กดอยู่)v×=f  (ปล่อย)v=clamp(v,−vmax⁡,vmax⁡)vn=v0f nv \mathrel{+}= a \;\text{(กดอยู่)} \qquad v \mathrel{\times}= f \;\text{(ปล่อย)} \qquad v = \text{clamp}(v, -v_{\max}, v_{\max}) \qquad v_n = v_0 f^{\,n}

if keys.up and not keys.down:   player_speed -= ACCEL      # (pong_step2.py)
elif keys.down and not keys.up: player_speed += ACCEL
else:                           player_speed *= FRICTION   # ปล่อยปุ่ม -> ค่อย ๆ หยุด
player_speed = max(-MAX_SPEED, min(MAX_SPEED, player_speed))
ในเกม ในงานจริง
ไม้ตีมีน้ำหนัก · ยานลื่นมีโมเมนตัม สายพานหรือลิฟต์ เร่งจนถึงเพดาน แล้วค่อย ๆ หยุด ไม่กระชาก ของบนสายพานไม่ล้ม

กฎ: ปุ่มเปลี่ยน ความเร็ว ไม่ได้เปลี่ยนตำแหน่ง

ลองเล่น · การ์ด 3: เร่ง หน่วง และเพดาน (ลื่นหรือหนืด ลองรู้สึกเอง)

คัดลอกไฟล์ s13_04_accel_friction.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยซ้าย/ขวา ค้าง = เร่ง · ปล่อย = ไถลแล้วหยุดเอง
  • A = เปลี่ยน f: 0.95 ลื่นเหมือนน้ำแข็ง -> 0.78 พอดีแบบ Pong -> 0.30 หนืดเหมือนโคลน
  • ดูแถบความเร็ว (ก้อนแดงปลายแถบ = ชนเพดาน) และบรรทัด "ไถล" ที่วัดให้ทุกครั้งที่ปล่อย
  • ธงเหลืองปักตรงจุดที่ปล่อยจอย ระยะจากธงถึงยาน = ระยะไถลจริงบนจอ
  • ลองเร่งเต็มที่แล้วปล่อยในแต่ละ f — ยานเดิม ปุ่มเดิม แต่ความรู้สึกต่างกันมาก

ลองแก้ แล้วรันใหม่

  1. FRICTIONS = (0.95, 0.78, 0.30) -> (0.99, 0.78, 0.30) : ทายก่อนว่าไถลกี่ px (สูตร v0 * f / (1 - f)) แล้วเร่งเต็มที่ ปล่อย และอ่านบรรทัด "ไถล" — ไถลไกลจนชนขอบไหม
  2. VMAX = 13.0 -> 6.0 : แถบชนก้อนแดงเร็วขึ้น ยานไม่มีทางเร็วกว่าเพดาน แม้กดค้างนานแค่ไหน
  3. ACCEL = 1.4 -> 0.3 : ยานหนักเหมือนรถบรรทุก ต้องกดค้างนานกว่าจะถึงเพดาน

โค้ดเต็ม · s13_04_accel_friction.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_04_accel_friction.py — คาบ 13 (คาบพิเศษ) การ์ด 3: เร่ง หน่วง และเพดาน (ลื่นหรือหนืด ลองรู้สึกเอง)
#
# หลักการ   : การ์ด 3 เร่ง หน่วง เพดาน                                              (คาบ 6, 10)
#             กดค้าง: v += a   ·   ปล่อย: v *= f   ·   v อยู่ใน -VMAX ถึง +VMAX
#             ปล่อยแล้ว v ลดแบบ v0, v0*f, v0*f*f, ... ระยะไถลรวม ~ v0 * f / (1 - f)
#             f = 0.95 -> ไถล ~247 px · f = 0.78 -> ~46 px · f = 0.30 -> ~6 px (เมื่อ v0 = 13)
# ลองเล่น   : กด Start · จอยซ้าย/ขวา ค้าง = เร่ง · ปล่อย = ไถลแล้วหยุดเอง
#             A = เปลี่ยน f: 0.95 ลื่นเหมือนน้ำแข็ง -> 0.78 พอดีแบบ Pong -> 0.30 หนืดเหมือนโคลน
#             ดูแถบความเร็ว (ก้อนแดงปลายแถบ = ชนเพดาน) และบรรทัด "ไถล" ที่วัดให้ทุกครั้งที่ปล่อย
#             ธงเหลืองปักตรงจุดที่ปล่อยจอย ระยะจากธงถึงยาน = ระยะไถลจริงบนจอ
#             ลองเร่งเต็มที่แล้วปล่อยในแต่ละ f — ยานเดิม ปุ่มเดิม แต่ความรู้สึกต่างกันมาก
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : ไม้ตีมีน้ำหนัก (pong_step2.py) · ยานลื่นมีโมเมนตัม (shooter_step2.py)
# ในงานจริง : สายพานหรือลิฟต์ขนของ เร่งจนถึงเพดาน แล้วค่อย ๆ หยุด ไม่กระชาก ของบนสายพานไม่ล้ม
#             ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : pong_step2.py (ACCEL / FRICTION / MAX_SPEED) · shooter_step2.py (ยานซ้าย-ขวา)
# งบวิดเจ็ต : 25 ชิ้น (ข้อความ 3 + แถบ 14 + ขีดศูนย์ 1 + ป้ายแถบ 3 + พื้น 1 + ธง 1 + ยาน 1 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ (ไฟล์นี้ข้าม)
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
ACCEL, VMAX = 1.4, 13.0                         # ค่าเดียวกับ shooter_step2.py
FRICTIONS = (0.95, 0.78, 0.30)                  # A วนเปลี่ยนสามค่านี้
FEELS = ("ลื่นเหมือนน้ำแข็ง", "พอดีแบบ Pong", "หนืดเหมือนโคลน")
FLOORS = (0xA8E6FF, 0x8899AA, 0x8B5A2B)         # สีพื้น: น้ำแข็ง · ปกติ · โคลน
STOP = 0.2                                      # ช้ากว่านี้ถือว่าหยุดแล้ว (v = 0)
SEGS = 7                                        # ก้อนแถบความเร็วต่อข้าง
SHIP_W, SHIP_Y, FLOOR_Y, BAR_Y = 62, 150, 180, 250
CENTER = game.WIDTH // 2


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ ----
def next_speed(v, push, friction):
    # การ์ด 3 ทั้งใบ: push = -1 ซ้าย · +1 ขวา · 0 ปล่อย
    if push != 0:
        v += push * ACCEL                       # กดค้าง -> เร่ง
    else:
        v *= friction                           # ปล่อย -> หน่วง
    return max(-VMAX, min(VMAX, v))             # เพดาน


def bar_level(v):
    # v -> จำนวนก้อนที่ติด มีเครื่องหมาย (-7 ถึง +7)
    n = int(abs(v) / VMAX * SEGS + 0.5)
    return -n if v < 0 else n


def box_on(side, i, level):
    # ก้อนที่ i (นับจากกลาง) ของข้าง side (-1 ซ้าย / +1 ขวา) ควรติดไหม
    return level * side > 0 and i < abs(level)


# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("SLIDE")
game.Text("กดค้าง: v += a     ปล่อย: v *= f     เพดาน: -%d ถึง +%d" % (int(VMAX), int(VMAX)), 16, 4, game.YELLOW)
feel_text = game.Text("", 16, 34, game.CYAN)
slide_text = game.Text("เร่งให้เต็มแถบ แล้วปล่อยจอย", 16, 62, game.WHITE)
floor = game.Box(0, FLOOR_Y, game.WIDTH, 10, FLOORS[0])
flag = game.Box(-20, FLOOR_Y - 40, 4, 40, game.YELLOW)     # ธงเหลือง = จุดที่เพิ่งปล่อยจอย
ship = game.Sprite("ship", CENTER - SHIP_W // 2, SHIP_Y)
bar = {-1: [], 1: []}
for side in (-1, 1):
    for i in range(SEGS):                       # ก้อนนอกสุดสีแดง = เพดาน
        left = CENTER + 6 + i * 46 if side > 0 else CENTER - 46 - i * 46
        seg = game.Box(left, BAR_Y, 40, 24, game.RED if i == SEGS - 1 else game.GREEN)
        seg.hide()
        bar[side].append(seg)
game.Box(CENTER - 2, BAR_Y - 6, 4, 36, game.WHITE)
game.Text("-%d" % int(VMAX), CENTER - 46 * SEGS, BAR_Y + 34, game.WHITE)
game.Text("+%d" % int(VMAX), CENTER + 46 * SEGS - 30, BAR_Y + 34, game.WHITE)
v_text = game.Text("", CENTER - 30, BAR_Y + 34, game.WHITE)

x, v, mode, prev_push = float(CENTER - SHIP_W // 2), 0.0, 0, 0
slide = None                                    # [เฟรม, พิกเซล, f] ระหว่างวัดระยะไถล
shown = {"level": 0, "v": ""}


def set_mode(m):
    feel_text.set("f = %.2f   %s   (A = เปลี่ยน)" % (FRICTIONS[m], FEELS[m]))
    floor.set_color(FLOORS[m])


set_mode(0)


# ---- 5) วงวนหลัก ----
def on_frame():
    global x, v, mode, prev_push, slide
    keys = game.keys()
    if game.pressed_once("a", keys):            # เข้า: เปลี่ยน f
        mode = (mode + 1) % len(FRICTIONS)
        set_mode(mode)
        game.sfx("select")
    push = (1 if keys.right else 0) - (1 if keys.left else 0)

    # คิด: ปุ่มเปลี่ยน "ความเร็ว" แล้วความเร็วค่อยเปลี่ยนตำแหน่ง
    v = next_speed(v, push, FRICTIONS[mode])
    if push == 0 and abs(v) < STOP:
        v = 0.0
    if push == 0 and prev_push != 0 and abs(v) > 1:
        slide = [0, 0.0, FRICTIONS[mode]]       # เพิ่งปล่อย -> เริ่มวัดระยะไถล ปักธงตรงนี้
        flag.move_to(int(x) + SHIP_W // 2, flag.y)
    elif push != 0:
        slide = None
    prev_push = push
    want = x + v
    x = max(0.0, min(game.WIDTH - SHIP_W, want))
    hit_wall = x != want                        # ชนขอบจอ = หยุด (เร็วพอถึงจะมีเสียง)
    if hit_wall and abs(v) > 2: game.sfx("wall")
    if hit_wall:                v = 0.0

    # ออก: ยาน + แถบ + ป้าย เฉพาะที่เปลี่ยน
    ship.move_to(x, SHIP_Y)
    if slide is not None:
        slide[0] += 1
        slide[1] += abs(v)
        if v == 0.0:
            slide_text.set("f %.2f  ปล่อยแล้วไถล %d เฟรม  %d px%s"
                           % (slide[2], slide[0], int(slide[1]), "  (ชนขอบ)" if hit_wall else ""))
            slide = None
    level = bar_level(v)
    if level != shown["level"]:
        for side in (-1, 1):
            for i in range(SEGS):
                now, before = box_on(side, i, level), box_on(side, i, shown["level"])
                if now and not before: bar[side][i].show()
                if before and not now: bar[side][i].hide()
        shown["level"] = level
    text = "v %+.1f" % v
    if text != shown["v"]:
        v_text.set(text)
        shown["v"] = text
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) FRICTIONS = (0.95, 0.78, 0.30) -> (0.99, 0.78, 0.30) : ทายก่อนว่าไถลกี่ px (สูตร v0 * f / (1 - f))
#    แล้วเร่งเต็มที่ ปล่อย และอ่านบรรทัด "ไถล" — ไถลไกลจนชนขอบไหม
# 2) VMAX = 13.0 -> 6.0 : แถบชนก้อนแดงเร็วขึ้น ยานไม่มีทางเร็วกว่าเพดาน แม้กดค้างนานแค่ไหน
# 3) ACCEL = 1.4 -> 0.3 : ยานหนักเหมือนรถบรรทุก ต้องกดค้างนานกว่าจะถึงเพดาน

การ์ด 4 · จำกัดขอบ และเร่งแบบมีเพดาน (คาบ 3)

x=max⁡(0,min⁡(W−w,x))speed=min⁡(BASE+hold,MAX)x = \max(0, \min(W - w, x)) \qquad\qquad \text{speed} = \min(\text{BASE} + \text{hold}, \text{MAX})

speed = min(BASE_SPEED + hold_frames, MAX_SPEED)     # (s03_box_joystick.py)
ในเกม ในงานจริง
ตัวละครไม่หลุดจอ · กดค้างแล้ววิ่งเร็วขึ้น ค่าตั้งอุณหภูมิไม่เกินช่วงปลอดภัย · มอเตอร์มีความเร็วสูงสุด · เคอร์เซอร์เร่งตามเวลาที่ดันก้าน

กฎ: ทุกค่าที่เปลี่ยนได้ ต้องมี ขอบ เสมอ

ลองเล่น · การ์ด 4: จำกัดขอบ และเร่งแบบมีเพดาน

คัดลอกไฟล์ s13_05_clamp_ramp.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยค้าง = วิ่ง (ยิ่งค้างนานยิ่งเร็ว) · ปล่อย = hold กลับเป็น 0
  • A = สลับเพดาน MAX ระหว่าง 30 กับ 10 แล้วดูขีดแดงบนแถบความเร็วเลื่อน
  • วิ่งชนขอบ: ขอบด้านนั้นกับตัวกล่องเปลี่ยนเป็นสีส้มพร้อมเสียง แต่กล่องไม่มีวันหลุดกรอบ
  • ดูบรรทัดสีขาว: เลขในสูตร min(...) เปลี่ยนตามเฟรมที่กดค้างจริง

ลองแก้ แล้วรันใหม่

  1. MAX_CHOICES = (30, 10) -> (30, 20) แล้วกด A : ทายก่อนว่ากดค้างกี่เฟรมความเร็วถึงหยุดเพิ่ม (20 - 6 = 14 เฟรม)
  2. ลบ clamp(...) ออกจากบรรทัด x = ... (เหลือ hero.x + ...) : วิ่งชนขวาแล้วกล่องหายออกนอกจอ ไม่กลับมา
  3. BASE_SPEED = 6 -> 0 : แตะจอยสั้น ๆ แทบไม่ขยับ ต้องค้างไว้ก่อนถึงจะเริ่มวิ่ง — เหมาะกับเคอร์เซอร์ที่ต้องเล็งละเอียด

โค้ดเต็ม · s13_05_clamp_ramp.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_05_clamp_ramp.py — คาบ 13 (คาบพิเศษ) การ์ด 4: จำกัดขอบ และเร่งแบบมีเพดาน
#
# หลักการ   : การ์ด 4 จำกัดขอบ        x = max(ซ้ายสุด, min(ขวาสุด, x))  ทำทุกเฟรม ทุกแกน    (คาบ 3)
#             เร่งแบบมีเพดาน          speed = min(BASE + hold, MAX)  กดค้างยิ่งนานยิ่งเร็ว
#             สูตรเดียวกับ s03_box_joystick.py ที่ game.Box.move() จำกัดขอบจอให้ ไฟล์นี้เขียนเองให้เห็น
# ลองเล่น   : กด Start · จอยค้าง = วิ่ง (ยิ่งค้างนานยิ่งเร็ว) · ปล่อย = hold กลับเป็น 0
#             A = สลับเพดาน MAX ระหว่าง 30 กับ 10 แล้วดูขีดแดงบนแถบความเร็วเลื่อน
#             วิ่งชนขอบ: ขอบด้านนั้นกับตัวกล่องเปลี่ยนเป็นสีส้มพร้อมเสียง แต่กล่องไม่มีวันหลุดกรอบ
#             ดูบรรทัดสีขาว: เลขในสูตร min(...) เปลี่ยนตามเฟรมที่กดค้างจริง
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : ตัวละครไม่หลุดจอ · กดค้างแล้ววิ่งเร็วขึ้น · ศัตรูเร็วขึ้นตามคะแนนแต่มีเพดาน
# ในงานจริง : ค่าตั้งอุณหภูมิไม่เกินช่วงปลอดภัย · มอเตอร์มีความเร็วสูงสุด
#             เคอร์เซอร์บนหน้าจอควบคุมเร่งตามเวลาที่ดันก้าน · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s03_box_joystick.py (BASE_SPEED · MAX_SPEED · hold_frames)
# งบวิดเจ็ต : 22 ชิ้น (ข้อความ 4 + รางแถบ 1 + แถบ 10 + ขีดเพดาน 1 + ขอบ 4 + กล่อง 1 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
BASE_SPEED = 6                  # ความเร็วฐาน (ค่าเดียวกับ s03_box_joystick.py) เฟรมแรกที่แตะจอยได้ 6 + 1 = 7
MAX_CHOICES = (30, 10)          # A สลับเพดานความเร็วระหว่างสองค่านี้
PER_BOX = 3                     # แถบความเร็ว 1 ก้อน = 3 พิกเซลต่อเฟรม (10 ก้อน = 30)
BAR_STEP = 3                    # แถบเปลี่ยนได้เฟรมละไม่เกิน 3 ก้อน (ไม่ส่งข้อความรวดเดียว)
HERO = 40                       # กล่องกว้าง/สูง 40 px แบบ s03
AX, AY, AW, AH = 8, 94, 776, 272                        # กรอบสนาม (ขอบหนา 6 px)
X_MIN, X_MAX = AX + 6, AX + AW - 6 - HERO
Y_MIN, Y_MAX = AY + 6, AY + AH - 6 - HERO
DIM = 0x334455


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def clamp(value, low, high):
    # การ์ด 4 ครึ่งแรก: ค่าที่เปลี่ยนได้ต้องมีขอบเสมอ
    return max(low, min(high, value))


def ramp_speed(hold, max_speed):
    # การ์ด 4 ครึ่งหลัง: กดค้างยิ่งนานยิ่งเร็ว แต่ไม่เกินเพดาน
    return min(BASE_SPEED + hold, max_speed)


def touching(x, y):
    # แตะขอบไหนอยู่บ้าง: (ซ้าย, ขวา, บน, ล่าง)
    return (x == X_MIN, x == X_MAX, y == Y_MIN, y == Y_MAX)


# ---- 3) ตรวจสมองก่อนเปิดจอ ----
def self_test():
    ok = (ramp_speed(0, 30) == 6 and ramp_speed(14, 30) == 20 and ramp_speed(100, 10) == 10
          and clamp(-5, 0, 10) == 0 and clamp(15, 0, 10) == 10)
    print("ผ่าน! ramp_speed กับ clamp ถูกทุกกรณี" if ok else "ยังไม่ผ่าน: ตรวจ ramp_speed กับ clamp")
    return ok


if not self_test():
    raise SystemExit

# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("CLAMP")
game.Text("speed = min(BASE + hold, MAX)       x = max(ซ้าย, min(ขวา, x))", 16, 4, game.YELLOW)
live = game.Text("กดจอยค้าง ดูความเร็วไต่ขึ้น", 16, 32, game.WHITE)
game.Box(12, 60, 10 * 34 + 4, 26, 0x16222E, border=DIM)
bar = [game.Box(16 + i * 34, 64, 30, 18, game.GREEN) for i in range(10)]   # แถบความเร็ว 10 ก้อน
for seg in bar:
    seg.hide()
ceiling = game.Box(0, 56, 4, 34, game.RED)                  # ขีดแดง = เพดาน MAX
max_text = game.Text("", 372, 62, game.CYAN)
pos_text = game.Text("", 590, 62, game.WHITE)
edges = [game.Box(AX, AY, 6, AH, DIM), game.Box(AX + AW - 6, AY, 6, AH, DIM),    # ซ้าย ขวา
         game.Box(AX, AY, AW, 6, DIM), game.Box(AX, AY + AH - 6, AW, 6, DIM)]    # บน ล่าง
hero = game.Box((X_MIN + X_MAX) // 2, (Y_MIN + Y_MAX) // 2, HERO, HERO, game.CYAN, radius=8)

hold, max_i, bar_on = 0, 0, 0
shown = {"live": "", "pos": "", "touch": (False, False, False, False)}


def set_max():
    top = MAX_CHOICES[max_i]
    max_text.set("MAX %d  (A = สลับ)" % top)
    ceiling.move_to(16 + top * 34 // PER_BOX - 4, ceiling.y)


def show(widget, key, text):
    if text != shown[key]:                                  # ป้ายเปลี่ยนเมื่อข้อความเปลี่ยนเท่านั้น
        widget.set(text)
        shown[key] = text


set_max()


# ---- 5) วงวนหลัก ----
def on_frame():
    global hold, max_i, bar_on
    k = game.keys()
    if game.pressed_once("a", k):                           # เข้า: สลับเพดาน
        max_i = 1 - max_i
        set_max()
        game.sfx("select")

    # คิด: แบบเดียวกับ s03 แล้วจำกัดขอบเองทั้งสองแกน
    moving = k.left or k.right or k.up or k.down
    hold = hold + 1 if moving else 0                        # กดค้าง -> สะสม · ปล่อย -> 0
    top = MAX_CHOICES[max_i]
    speed = ramp_speed(hold, top) if moving else 0
    x = clamp(hero.x + (speed if k.right else 0) - (speed if k.left else 0), X_MIN, X_MAX)
    y = clamp(hero.y + (speed if k.down else 0) - (speed if k.up else 0), Y_MIN, Y_MAX)
    touch = touching(x, y)

    # ออก: กล่อง ขอบ เสียง ป้าย และแถบ เฉพาะที่เปลี่ยน
    if x != hero.x or y != hero.y:
        hero.move_to(x, y)
    if touch != shown["touch"]:
        for i in range(4):
            if touch[i] != shown["touch"][i]:
                edges[i].set_color(game.ORANGE if touch[i] else DIM)
                if touch[i]:
                    game.sfx("wall")                        # เสียงเฉพาะตอนเพิ่งแตะ ไม่ดังรัว
        if any(touch) != any(shown["touch"]):
            hero.set_color(game.ORANGE if any(touch) else game.CYAN)
        shown["touch"] = touch
    if moving:
        show(live, "live", "hold %d  ->  speed = min(%d + %d, %d) = %d" % (hold, BASE_SPEED, hold, top, speed))
    else:
        show(live, "live", "ปล่อยจอย: hold = 0  speed = 0")
    show(pos_text, "pos", "x %d  y %d" % (x, y))
    want = min(speed // PER_BOX, len(bar))
    for _ in range(BAR_STEP):                               # ไล่แถบเข้าหาค่าจริงทีละก้อน
        if bar_on < want:
            bar[bar_on].show()
            bar_on += 1
        elif bar_on > want:
            bar_on -= 1
            bar[bar_on].hide()
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) MAX_CHOICES = (30, 10) -> (30, 20) แล้วกด A : ทายก่อนว่ากดค้างกี่เฟรมความเร็วถึงหยุดเพิ่ม (20 - 6 = 14 เฟรม)
# 2) ลบ clamp(...) ออกจากบรรทัด x = ... (เหลือ hero.x + ...) : วิ่งชนขวาแล้วกล่องหายออกนอกจอ ไม่กลับมา
# 3) BASE_SPEED = 6 -> 0 : แตะจอยสั้น ๆ แทบไม่ขยับ ต้องค้างไว้ก่อนถึงจะเริ่มวิ่ง — เหมาะกับเคอร์เซอร์ที่ต้องเล็งละเอียด

การ์ด 5 · ตัวควบคุมสามระดับ และช่องว่าง (คาบ 7)

target_y = ball_y - PADDLE_H / 2                       # (pong_step5.py)
if ai_y < target_y - 6:   ai_y += ai_speed   # ลูกอยู่ล่างกว่า -> ขยับลง
elif ai_y > target_y + 6: ai_y -= ai_speed   # ลูกอยู่บนกว่า -> ขยับขึ้น
                                             # อยู่ในช่องว่าง ±6 -> อยู่นิ่ง

ในเกม ในงานจริง
ไม้ AI ขยับขึ้น หยุด หรือลง วาล์วมอเตอร์ เปิดเพิ่ม · หยุด · ปิดลง ตามระดับน้ำ ช่องว่างกันวาล์วสั่นไปมา

กฎ: ตัวควบคุมที่ดีต้องรู้ว่า "ใกล้พอแล้ว" — ช่องว่างกันการสั่น

ลองเล่น · การ์ด 5: ตัวควบคุมสามระดับ + ช่องว่าง (เห็นไม้สั่นกับตา)

คัดลอกไฟล์ s13_06_deadzone.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยขึ้น/ลง = ย้ายลูกบอล (เป้า) · ไม้ AI ไล่ตามเอง
  • A = สลับ BAND ระหว่าง 6 กับ 0 · B = ล้างตัวนับ FLIPS
  • BAND 6: ไม้หยุดนิ่งในแถบเขียว ไฟ HOLD ติดค้าง ตัวนับ FLIPS นิ่ง
  • BAND 0: ไม้สั่นกึก ๆ ไฟสลับเขียว-ส้มแทบทุกเฟรม FLIPS พุ่ง และได้ยินเสียงติ๊กรัว

ลองแก้ แล้วรันใหม่

  1. กด A ให้ BAND = 0 แล้วปล่อยจอย : ไม้สั่นไม่หยุด FLIPS ขึ้นหลายสิบครั้งในไม่กี่วินาที วาล์วจริงที่ถูกสั่งกลับทิศถี่ขนาดนี้ สึกพังในไม่กี่วัน
  2. SPEED = 5 -> 2 : ไม้ไล่ช้าลง แต่ตอน BAND 0 สั่นเบาลง (ก้าวสั้น) — เร็วกับนิ่งต้องแลกกัน
  3. NOISE = 1 -> 8 : ค่าวัดสั่นแรงกว่าช่องว่าง FLIPS ขึ้นเองทั้งที่ BAND 6 — เพิ่ม BAND_ON จนนิ่ง

โค้ดเต็ม · s13_06_deadzone.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_06_deadzone.py — คาบ 13 การ์ด 5: ตัวควบคุมสามระดับ + ช่องว่าง (เห็นไม้สั่นกับตา)
#
# หลักการ   : การ์ด 5 ตัวควบคุมสามระดับ   ค่าคลาด e = เป้า - ตำแหน่งไม้                (คาบ 7)
#             e > BAND -> +1 (ขยับลง) · e < -BAND -> -1 (ขยับขึ้น) · นอกนั้น 0 (อยู่นิ่ง)
#             กฎเดียวกับไม้ AI ใน pong_step5.py ที่ใช้ช่องว่าง ±6 px
# ลองเล่น   : กด Start · จอยขึ้น/ลง = ย้ายลูกบอล (เป้า) · ไม้ AI ไล่ตามเอง
#             A = สลับ BAND ระหว่าง 6 กับ 0 · B = ล้างตัวนับ FLIPS
#             BAND 6: ไม้หยุดนิ่งในแถบเขียว ไฟ HOLD ติดค้าง ตัวนับ FLIPS นิ่ง
#             BAND 0: ไม้สั่นกึก ๆ ไฟสลับเขียว-ส้มแทบทุกเฟรม FLIPS พุ่ง และได้ยินเสียงติ๊กรัว
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : ไม้ AI ของ Pong ขยับขึ้น หยุด หรือลง ตามลูก
# ในงานจริง : วาล์วมอเตอร์ เปิดเพิ่ม / หยุด / ปิดลง ตามค่าคลาดของระดับน้ำ · ติ๊กหนึ่งครั้ง = กลับทิศหนึ่งครั้ง
#             ยิ่งถี่ยิ่งสึก ช่องว่างจึงสำคัญ · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : pong_step5.py (ไม้ AI + ช่องว่าง) · s13_tank_hmi.py (วาล์วถังน้ำ)
# งบวิดเจ็ต : 21 ชิ้น (สนาม 1 + เส้นเป้า 1 + แถบช่องว่าง 1 + ลูก 1 + ไม้ 1 + ขีดกลางไม้ 1
#             + ไฟ 3 + ข้อความ 11 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   4) หน้าจอ: สร้างครั้งเดียว   5) วงวนหลัก
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
BAND_ON = 6         # ช่องว่าง ±6 px แบบ pong_step5.py (กด A สลับกับ 0)
SPEED = 5           # ไม้ AI ขยับเฟรมละ 5 px คงที่ — มีแค่ ขึ้น / หยุด / ลง ไม่มีครึ่ง ๆ กลาง ๆ
BALL_STEP = 4       # กดจอยค้าง ลูก (เป้า) ขยับเฟรมละ 4 px
NOISE = 1           # ค่าที่วัดได้สั่น ±1 px เหมือนเซนเซอร์จริง (0 = วัดเป๊ะ)
TOP, BOTTOM = 44, 356           # ขอบบน/ล่างของสนาม
BALL_X, AI_X = 90, 330          # คอลัมน์ของลูก และของไม้ AI
BALL, PADDLE_H = 14, 90         # ขนาดสไปรต์ ball 14x14 · paddle 14x90
HUD_X = 460
LAMP_OFF = 0x3A4450
LAMP_ON = {1: game.GREEN, 0: game.CYAN, -1: game.ORANGE}
MEANING = {1: "OPEN", 0: "HOLD", -1: "CLOSE"}

# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ ----
def controller(error, band):
    # การ์ด 5: ตัวควบคุมสามระดับ คืน +1 = ลง/เปิดเพิ่ม · 0 = นิ่ง/หยุด · -1 = ขึ้น/ปิดลง
    if error > band:
        return 1
    if error < -band:
        return -1
    return 0                           # ใกล้พอแล้ว -> อยู่นิ่ง

def signed(value):
    # ใส่เครื่องหมาย + ให้เลขบวก จะได้เห็นทิศของค่าคลาด
    return ("+%d" % value) if value > 0 else ("%d" % value)

# ---- 4) หน้าจอ: สร้างครั้งเดียว (ของที่สร้างก่อนอยู่ล่าง) ----
game.title("DEADZONE")
game.Text("TARGET", BALL_X - 26, 14, game.YELLOW)
game.Text("AI", AI_X - 4, 14, game.WHITE)
game.Box(30, TOP, 400, BOTTOM - TOP, 0x18202A, border=0x3A4A5A, border_w=1)
line = game.Box(BALL_X, 0, AI_X + 40 - BALL_X, 2, game.YELLOW)       # เส้นเป้าที่กลางลูก
band_box = game.Box(AI_X - 14, 0, 42, 2 * BAND_ON, 0x2E8B57)          # แถบช่องว่าง ±BAND
ball = game.Sprite("ball", BALL_X, 0)
paddle = game.Sprite("paddle", AI_X, 0)
mark = game.Box(AI_X - 8, 0, 30, 3, game.WHITE)                       # ขีดกลางไม้

hud_band = game.Text("", HUD_X, 24, game.WHITE)
hud_error = game.Text("", HUD_X, 60, game.WHITE)
hud_out = game.Text("", HUD_X, 96, game.WHITE)
lamps = {}
for i, out in enumerate((1, 0, -1)):
    lamps[out] = game.Box(HUD_X + 10 + i * 105, 138, 30, 30, LAMP_OFF, radius=15)
    game.Text("%s %s" % (signed(out), MEANING[out]), HUD_X + i * 105, 176, game.WHITE)
game.Text("วาล์ว: เปิดเพิ่ม / หยุด / ปิดลง", HUD_X, 214, 0x9AA8B8)
hud_flips = game.Text("", HUD_X, 258, game.WHITE)
game.Text("UP/DOWN = เป้า  A = BAND", HUD_X, 300, game.GB_LIGHT)

ball_y = 180.0                  # มุมบนของลูก
ai_y = 60.0                     # มุมบนของไม้
band = BAND_ON
flips, last_dir = 0, 0
shown = {"ball": None, "ai": None, "band": None, "err": "", "out": None, "flips": None}

# ---- 5) วงวนหลัก ----
def on_frame():
    global ball_y, ai_y, band, flips, last_dir
    keys = game.keys()

    # รับปุ่ม (วัด)
    if keys.up:   ball_y = max(TOP + PADDLE_H / 2 - BALL / 2, ball_y - BALL_STEP)
    if keys.down: ball_y = min(BOTTOM - PADDLE_H / 2 - BALL / 2, ball_y + BALL_STEP)
    if game.pressed_once("a", keys):
        band = 0 if band else BAND_ON
        game.sfx("select")
    if game.pressed_once("b", keys):
        flips = 0

    # คิด (ตัดสิน): ค่าคลาดที่วัดได้ -> สามระดับ -> ไม้ขยับเท่า SPEED หรือไม่ขยับเลย
    target = ball_y + BALL / 2 - PADDLE_H / 2          # อยากให้กลางไม้ตรงกลางลูก
    error = target - ai_y + random.randint(-NOISE, NOISE)
    out = controller(error, band)
    ai_y += out * SPEED
    if out != 0:
        if last_dir != 0 and out != last_dir:        # กลับทิศ = วาล์วสลับหนึ่งครั้ง
            flips += 1
            game.tone(96, ms=12)
        last_dir = out

    # ออก (โชว์): อัปเดตเฉพาะที่เปลี่ยน
    cy = int(ball_y + BALL / 2)
    if cy != shown["ball"]:
        ball.move_to(BALL_X, int(ball_y))
        line.move_to(BALL_X, cy - 1)
        band_box.move_to(AI_X - 14, cy - BAND_ON)
        shown["ball"] = cy
    if band != shown["band"]:
        band_box.show() if band else band_box.hide()
        hud_band.set("BAND = %d px   (A)" % band)
        shown["band"] = band
    if int(ai_y) != shown["ai"]:
        paddle.move_to(AI_X, int(ai_y))
        mark.move_to(AI_X - 8, int(ai_y) + PADDLE_H // 2 - 1)
        shown["ai"] = int(ai_y)
    text = "ERROR = %s px" % signed(int(error))
    if text != shown["err"]:
        hud_error.set(text)
        shown["err"] = text
    if out != shown["out"]:
        if shown["out"] is not None:
            lamps[shown["out"]].set_color(LAMP_OFF)
        lamps[out].set_color(LAMP_ON[out])
        hud_out.set("OUT = %s   %s" % (signed(out), MEANING[out]))
        shown["out"] = out
    if flips != shown["flips"]:
        hud_flips.set("FLIPS = %d   (B = 0)" % flips)
        shown["flips"] = flips
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) กด A ให้ BAND = 0 แล้วปล่อยจอย : ไม้สั่นไม่หยุด FLIPS ขึ้นหลายสิบครั้งในไม่กี่วินาที
#    วาล์วจริงที่ถูกสั่งกลับทิศถี่ขนาดนี้ สึกพังในไม่กี่วัน
# 2) SPEED = 5 -> 2 : ไม้ไล่ช้าลง แต่ตอน BAND 0 สั่นเบาลง (ก้าวสั้น) — เร็วกับนิ่งต้องแลกกัน
# 3) NOISE = 1 -> 8 : ค่าวัดสั่นแรงกว่าช่องว่าง FLIPS ขึ้นเองทั้งที่ BAND 6 — เพิ่ม BAND_ON จนนิ่ง

การ์ด 6 · เวลาคือการนับเฟรม (คาบ 4, 11, 12)

Nเฟรม=tวินาที×fpsc=max⁡(0,c−1)  ทุกเฟรมN_{\text{เฟรม}} = t_{\text{วินาที}} \times \text{fps} \qquad c = \max(0, c - 1) \;\text{ทุกเฟรม}

fire_cooldown = max(0, fire_cooldown - 1)            # (shooter_step3.py)
if (keys.a or keys.up) and fire_cooldown == 0:
    ...
    fire_cooldown = 8        # 8 เฟรม -> ยิงได้ 30 ÷ 8 ≈ 3.75 นัด/วินาที
ในเกม ในงานจริง
คูลดาวน์กันยิงรัว · ปล่อยศัตรูทุก N เฟรม · นับถอยหลังหมดเวลา กันกดปุ่มสั่งปั๊มซ้ำ · อ่านเซนเซอร์ทุก 1 วินาที · ส่งสัญญาณ "ยังอยู่" ทุก N รอบ

กฎ: ในวงวน 30 fps อย่าใช้ sleep ให้ นับเฟรม แทน

ลองเล่น · การ์ด 6: เวลาคือการนับเฟรม (ตัวจับเวลาสามตัวบนจอเดียว)

คัดลอกไฟล์ s13_07_frame_timer.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยซ้าย/ขวา = ขยับยาน · กด A ค้าง = ยิงรัว · ดู c นับ 8 ลงถึง 0 แล้วจึงยิงนัดใหม่
  • shots/s วัดจริงได้ 3 กับ 4 สลับกัน (เฉลี่ย 3.75) · 5 วินาทีสุดท้ายแถบแดงและมีเสียงนับ

ลองแก้ แล้วรันใหม่

  1. COOLDOWN = 8 -> 2 : ทายก่อนว่าได้ 15 นัดต่อวินาทีไหม แล้วกด A ค้างดู shots/s ทำไมยิง 6 นัดแล้วสะดุด? (ใบ้: กระสุนมีแค่ 6 นัดในชุด ต้องรอนัดเก่าพ้นจอก่อน — การ์ดถัดไป)
  2. SPAWN = 45 -> 15 : ศัตรูหล่นถี่ขึ้น 3 เท่า แต่บนจอมีไม่เกิน 4 ตัว เพราะชุดศัตรูมี 4 ตัว
  3. ROUND_SEC = 30 -> 10 : แถบเวลาสั้นลงเร็วขึ้น ตัวเลขเฟรมเริ่มที่ 300 = 10 x 30

โค้ดเต็ม · s13_07_frame_timer.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_07_frame_timer.py — คาบ 13 การ์ด 6: เวลาคือการนับเฟรม (ตัวจับเวลาสามตัวบนจอเดียว)
#
# หลักการ   : การ์ด 6 เวลาคือการนับเฟรม   N = t x fps   และ   c = max(0, c - 1) ทุกเฟรม   (คาบ 4, 11, 12)
#             30 วินาที = 900 เฟรม · คูลดาวน์ 8 เฟรม = 30 / 8 = 3.75 นัด/วินาที · ศัตรูทุก 45 เฟรม · ไม่มี sleep
# ลองเล่น   : กด Start · จอยซ้าย/ขวา = ขยับยาน · กด A ค้าง = ยิงรัว · ดู c นับ 8 ลงถึง 0 แล้วจึงยิงนัดใหม่
#             shots/s วัดจริงได้ 3 กับ 4 สลับกัน (เฉลี่ย 3.75) · 5 วินาทีสุดท้ายแถบแดงและมีเสียงนับ
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : คูลดาวน์กันยิงรัว · ปล่อยศัตรูทุก N เฟรม · นับถอยหลังหมดเวลา
# ในงานจริง : กันกดสั่งปั๊มซ้ำ (คูลดาวน์) · อ่านเซนเซอร์ทุก 1 วินาที · นับถอยหลังรอบการผลิต · ค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s04_catch.py (นับถอยหลัง) · shooter_step3.py (คูลดาวน์) · shooter_step5.py (ตัวจับเวลาปล่อยศัตรู)
# งบวิดเจ็ต : 23 ชิ้น (ยาน 1 + กระสุน 6 + ศัตรู 4 + เส้นแบ่ง 1 + แถบเวลา 2 + ข้อความ 8 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   4) หน้าจอ: สร้างครั้งเดียว   5) วงวนหลัก
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30
ROUND_SEC = 30            # นาฬิกานับถอยหลัง 30 วินาที
COOLDOWN = 8              # ยิงแล้วพัก 8 เฟรม
SPAWN = 45                # ปล่อยศัตรูทุก 45 เฟรม
BULLETS, ENEMIES = 6, 4   # ขนาดชุดหมุนเวียน (คาบ 11) สร้างครั้งเดียว
BULLET_SPEED, ENEMY_SPEED, SHIP_SPEED = 12, 3, 6
FIELD_W, SHIP_Y = 470, 326          # สนามยิงซ้ายมือ · ขวามือเป็นแผงตัวจับเวลา
PANEL_X, BAR_W = 494, 280
DIM = 0x9AA8B8

# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ ----
def frames_for(seconds):
    return seconds * FPS                    # N = t x fps

def tick(counter):
    return max(0, counter - 1)              # c = max(0, c - 1) ไม่ติดลบ

def seconds_left(frames):
    return (frames + FPS - 1) // FPS        # ปัดขึ้น: เหลือ 1 เฟรมก็ยังขึ้น 1 วินาที

def find_free(items):
    for item in items:                      # จอดรอเหนือจอ (y < -20) = ว่าง
        if item.y < -20:
            return item
    return None

# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("FRAME TIMER")
enemies = game.pool("enemy", ENEMIES)
for e in enemies: e.w, e.h = 16, 14         # สไปรต์ไม่มีขนาดในตัว ต้องบอกเองก่อนใช้ game.hit
bullets = [game.Box(0, -50, 5, 12, game.CYAN, border=game.WHITE, radius=2) for _ in range(BULLETS)]
for b in bullets: b.hide()
ship = game.Sprite("ship", 200, SHIP_Y)
game.Box(478, 10, 2, 350, 0x3A4A5A)
game.Text("CLOCK  %d x %d = %d f" % (ROUND_SEC, FPS, frames_for(ROUND_SEC)), PANEL_X, 12, DIM)
game.Box(PANEL_X, 42, BAR_W, 16, 0x1E2A36, border=0x8899AA)
bar = game.Box(PANEL_X + 2, 44, BAR_W - 4, 12, game.GREEN)
hud_time = game.Text("", PANEL_X, 66, game.WHITE)
game.Text("FIRE  c = max(0, c-1)", PANEL_X, 112, DIM)
hud_cool = game.Text("", PANEL_X, 140, game.WHITE)
hud_rate = game.Text("", PANEL_X, 168, game.WHITE)
game.Text("SPAWN  every %d f" % SPAWN, PANEL_X, 214, DIM)
hud_spawn = game.Text("", PANEL_X, 242, game.WHITE)
hud_score = game.Text("", PANEL_X, 296, game.YELLOW)

ship_x, frame = 200, 0
time_left, cool, spawn_left = frames_for(ROUND_SEC), 0, SPAWN
score, best, shots = 0, 0, []               # shots = เลขเฟรมของนัดที่ยิงในหนึ่งวินาทีล่าสุด
shown = {"sec": None, "cool": None, "rate": None, "score": None}
IDEAL = FPS * 100 // COOLDOWN               # 375 = 3.75 นัด/วินาที (เก็บเป็นร้อยส่วน)

# ---- 5) วงวนหลัก ----
def on_frame():
    global ship_x, frame, time_left, cool, spawn_left, score, best
    keys = game.keys()
    frame += 1
    # ตัวที่ 1: นาฬิกานับถอยหลัง หมดแล้วเริ่มรอบใหม่
    time_left = tick(time_left)
    if time_left == 0:
        best, score = max(best, score), 0
        time_left = frames_for(ROUND_SEC)
        game.sfx("win")
    # ตัวที่ 2: คูลดาวน์ — ลดก่อน แล้วยิงได้เมื่อถึง 0 และมีกระสุนว่าง
    cool = tick(cool)
    if keys.left or keys.right:
        ship_x = max(0, min(FIELD_W - 62, ship_x + (SHIP_SPEED if keys.right else -SHIP_SPEED)))
        ship.move_to(ship_x, SHIP_Y)
    if keys.a and cool == 0:
        b = find_free(bullets)
        if b:
            b.move_to(ship_x + 29, SHIP_Y - 12)
            b.show()
            game.sfx("fire")
            cool = COOLDOWN
            shots.append(frame)
    while shots and frame - shots[0] >= FPS: shots.pop(0)    # ทิ้งนัดที่เก่ากว่า 1 วินาที
    # ตัวที่ 3: ตัวจับเวลาปล่อยศัตรู
    spawn_left = tick(spawn_left)
    if spawn_left == 0:
        spawn_left = SPAWN
        e = find_free(enemies)
        if e:                                    # ชุดเต็ม = รอบนี้ข้ามไป
            e.move_to(random.randint(10, FIELD_W - 30), -14)
            e.show()
    # ขยับของที่ใช้อยู่ + ชน (คืนของเข้าชุดเมื่อพ้นจอหรือชน)
    for b in bullets:
        if b.y >= -20:
            b.move_to(b.x, b.y - BULLET_SPEED)
            if b.y < -20: b.hide()             # พ้นจอ = คืนเข้าชุด
    for e in enemies:
        if e.y >= -20:
            e.move_to(e.x, e.y + ENEMY_SPEED)
            shot_down = False
            for b in bullets:
                if b.y >= -20 and game.hit(b, e):
                    b.move_to(b.x, -50); b.hide()
                    score += 1; shot_down = True
                    game.sfx("hit")
                    break
            if shot_down or e.y > 360:           # ชนหรือหล่นพ้นจอ -> คืนเข้าชุด
                e.hide(); e.move_to(-100, -100)
    # โชว์: ตัวเลขเฟรมเปลี่ยนทุกเฟรม ส่วนแถบเวลาเปลี่ยนแค่วินาทีละครั้ง
    sec = seconds_left(time_left)
    hud_time.set("%d s   %d f" % (sec, time_left))
    if sec != shown["sec"]:
        bar.resize(max(2, (BAR_W - 4) * time_left // frames_for(ROUND_SEC)), 12)
        if sec <= 5: game.tone(72 if sec > 1 else 84, ms=80)
        if sec == 5 or sec == ROUND_SEC: bar.set_color(game.RED if sec == 5 else game.GREEN)
        shown["sec"] = sec
    if cool != shown["cool"]:
        hud_cool.set("c = %d   (hold A)" % cool)
        shown["cool"] = cool
    if len(shots) != shown["rate"]:
        hud_rate.set("%d shots/s  ideal %d.%02d" % (len(shots), IDEAL // 100, IDEAL % 100))
        shown["rate"] = len(shots)
    hud_spawn.set("next in %d f" % spawn_left)
    if (score, best) != shown["score"]:
        hud_score.set("SCORE %d   BEST %d" % (score, best))
        shown["score"] = (score, best)
    return True

game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) COOLDOWN = 8 -> 2 : ทายก่อนว่าได้ 15 นัดต่อวินาทีไหม แล้วกด A ค้างดู shots/s
#    ทำไมยิง 6 นัดแล้วสะดุด? (ใบ้: กระสุนมีแค่ 6 นัดในชุด ต้องรอนัดเก่าพ้นจอก่อน — การ์ดถัดไป)
# 2) SPAWN = 45 -> 15 : ศัตรูหล่นถี่ขึ้น 3 เท่า แต่บนจอมีไม่เกิน 4 ตัว เพราะชุดศัตรูมี 4 ตัว
# 3) ROUND_SEC = 30 -> 10 : แถบเวลาสั้นลงเร็วขึ้น ตัวเลขเฟรมเริ่มที่ 300 = 10 x 30

ส่วนที่ 3 — ชุดหมุนเวียน (คาบ 11)

def find_free_bullet():                  # (shooter_step3.py)
    for bullet in bullets:
        if bullet.y < -20:               # จอดรอเหนือจอ = ว่าง
            return bullet
    return None                          # ใช้อยู่ครบ -> เฟรมนี้ยิงไม่ออก
ในเกม ในงานจริง
กระสุน · ศัตรู · ระเบิด กล่องบนสายพาน · รถในลานจอด · ข้อความในบัฟเฟอร์เครือข่าย

กฎ: บนบอร์ดหน่วยความจำมีจำกัด จองครั้งเดียว ยืม-คืน ไม่สร้างใหม่ระหว่างเล่น

ลองเล่น · ส่วนที่ 3: ชุดหมุนเวียน (ยืม-คืน ไม่สร้างใหม่ระหว่างเล่น)

คัดลอกไฟล์ s13_08_pool.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · กด A = ยืม 1 ตัว ปล่อยขึ้นจากช่องของมันเอง
  • ช่องเปลี่ยนเขียว -> ส้ม ตอนยืม และกลับเป็นเขียวตอนของบินพ้นจอ · ขีดขาว = ช่องที่จะยืมต่อไป
  • ของแต่ละช่องบินเร็วไม่เท่ากัน ช่องกลาง ๆ จึงว่างก่อนช่องซ้ายได้ ดูขีดขาวกระโดดไปหา
  • กด A รัว ๆ จนครบ 8 แล้วกดอีก = เสียงปฏิเสธ (ชุดเต็ม ต้องรอคืนก่อน)

ลองแก้ แล้วรันใหม่

  1. SPEEDS = (2, 3, 4) -> (3,) : ทุกตัวบินเร็วเท่ากัน ของที่ยืมก่อนคืนก่อนเสมอ (เข้าก่อน ออกก่อน)
  2. POOL = 8 -> 4 : เต็มเร็วขึ้นมาก ได้ยินเสียงปฏิเสธบ่อย แต่คืนงบจอได้ 8 ชิ้น (ของ 4 + ช่อง 4) กลับกัน POOL = 10 จะเกินงบ 26 ชิ้น เพราะของหนึ่งตัวกินสองชิ้น: ตัวมันกับช่องของมัน
  3. ลบ item.hide() ในส่วนคืน : เกมยังทำงานเหมือนเดิม เพราะที่ทำให้ "ว่าง" คือค่า y < -20 ไม่ใช่การซ่อน

โค้ดเต็ม · s13_08_pool.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_08_pool.py — คาบ 13 ส่วนที่ 3: ชุดหมุนเวียน (ยืม-คืน ไม่สร้างใหม่ระหว่างเล่น)
#
# หลักการ   : ชุดหมุนเวียน (คาบ 11) สร้างของ 8 ตัวครั้งเดียวตอนเริ่ม แล้วยืม-คืนใช้ซ้ำ
#             ว่าง ⇔ y < -20 (จอดรอนอกจอ) · ยืม = หาตัวว่างตัวแรก · คืน = บินพ้นขอบบนแล้วนับว่าว่าง
#             ช่องเก็บของ (ล็อกเกอร์) 8 ช่องด้านล่างคือ "ภาพ" ของรายการ items ทั้ง 8 ช่อง
# ลองเล่น   : กด Start · กด A = ยืม 1 ตัว ปล่อยขึ้นจากช่องของมันเอง
#             ช่องเปลี่ยนเขียว -> ส้ม ตอนยืม และกลับเป็นเขียวตอนของบินพ้นจอ · ขีดขาว = ช่องที่จะยืมต่อไป
#             ของแต่ละช่องบินเร็วไม่เท่ากัน ช่องกลาง ๆ จึงว่างก่อนช่องซ้ายได้ ดูขีดขาวกระโดดไปหา
#             กด A รัว ๆ จนครบ 8 แล้วกดอีก = เสียงปฏิเสธ (ชุดเต็ม ต้องรอคืนก่อน)
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : กระสุน · ศัตรู · ระเบิด (shooter_step3.py ใช้กระสุน 6 นัดวนใช้ซ้ำ)
# ในงานจริง : ลานจอดรถ 8 ช่องที่ป้ายบอกช่องว่าง · ถาดพัสดุบนสายพานที่วนกลับมารับของใหม่
#             จองครั้งเดียวไม่ขอหน่วยความจำเพิ่มระหว่างทำงาน · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : shooter_step3.py (ชุดกระสุนหมุนเวียน) · shooter_step5.py (ชุดศัตรู)
# งบวิดเจ็ต : 24 ชิ้น (ของ 8 + ช่อง 8 + ขีดขาว 1 + ข้อความ 6 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   4) หน้าจอ: สร้างครั้งเดียว   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
POOL = 8                    # ขนาดชุด (งบวิดเจ็ตจำกัด: ของ 1 ตัวกิน 2 ชิ้น คือตัวมันกับช่องของมัน)
SPEEDS = (2, 3, 4)          # ความเร็วบินขึ้น (px ต่อเฟรม) วนตามเลขช่อง 0, 1, 2, 0, 1, ...
LOCK_X0, LOCK_Y, LOCK_W, LOCK_H, LOCK_GAP = 34, 316, 44, 30, 54
LAUNCH_Y = LOCK_Y - 22      # จุดปล่อยของ เหนือช่องของตัวเอง
FREE_COLOR, USED_COLOR = game.GREEN, game.ORANGE
HUD_X = 500


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ ----
def is_free(item):
    return item.y < -20                 # จอดรอนอกจอ = ว่าง


def find_free(items):
    for i in range(len(items)):         # ไล่จากช่องซ้ายสุด เจอตัวว่างตัวแรกก็ใช้เลย
        if is_free(items[i]):
            return i
    return None                         # ใช้อยู่ครบทุกตัว


def count_free(items):
    n = 0
    for item in items:
        if is_free(item):
            n += 1
    return n


def locker_x(i):
    return LOCK_X0 + i * LOCK_GAP


# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("POOL")
lockers = [game.Box(locker_x(i), LOCK_Y, LOCK_W, LOCK_H, FREE_COLOR, border=game.WHITE, radius=4)
           for i in range(POOL)]
cursor = game.Box(locker_x(0), LOCK_Y + LOCK_H + 5, LOCK_W, 4, game.WHITE)
items = []
for i in range(POOL):                   # สร้างครบ 8 ตัวตรงนี้ที่เดียว ในวงวนหลักไม่สร้างเพิ่มเลย
    item = game.Sprite("enemy" if i % 2 == 0 else "enemy2", -100, -100)
    item.w = 16 if i % 2 == 0 else 21
    item.hide()
    items.append(item)

game.Text("POOL %d    free = y < -20" % POOL, HUD_X, 20, game.CYAN)
hud_count = game.Text("", HUD_X, 58, game.WHITE)
hud_msg = game.Text("A = ยืม 1 ตัว", HUD_X, 96, game.YELLOW)
game.Text("เขียว = ว่าง    ส้ม = ใช้อยู่", HUD_X, 150, game.WHITE)
game.Text("ขีดขาว = ช่องที่จะยืมต่อไป", HUD_X, 186, game.WHITE)
game.Text("ลานจอด %d ช่อง / ถาดบนสายพาน" % POOL, HUD_X, 240, 0x9AA8B8)

shown = {"free": None, "next": 0}

# ---- 5) วงวนหลัก ----
def on_frame():
    keys = game.keys()

    # ยืม: กด A หนึ่งครั้ง = ขอหนึ่งตัว
    if game.pressed_once("a", keys):
        i = find_free(items)
        if i is None:
            hud_msg.set("เต็ม! รอของบินพ้นจอก่อน")
            game.sfx("deny")
        else:
            item = items[i]
            item.move_to(locker_x(i) + (LOCK_W - item.w) // 2, LAUNCH_Y)
            item.show()
            lockers[i].set_color(USED_COLOR)
            hud_msg.set("ยืมช่อง %d ไปแล้ว" % (i + 1))
            game.sfx("select")

    # อัปเดตเฉพาะตัวที่ใช้อยู่ + คืนเมื่อพ้นขอบบน
    for i in range(POOL):
        item = items[i]
        if not is_free(item):
            item.move_to(item.x, item.y - SPEEDS[i % len(SPEEDS)])
            if is_free(item):               # เพิ่งพ้น -20 = คืนเข้าชุดแล้ว
                item.hide()
                lockers[i].set_color(FREE_COLOR)
                hud_msg.set("คืนช่อง %d แล้ว" % (i + 1))
                game.tone(76, ms=40)

    # โชว์: เปลี่ยนเฉพาะเมื่อจำนวนว่างหรือช่องถัดไปเปลี่ยน
    free = count_free(items)
    if free != shown["free"]:
        hud_count.set("FREE %d    USED %d" % (free, POOL - free))
        shown["free"] = free
    nxt = find_free(items)
    if nxt != shown["next"]:
        if nxt is None:
            cursor.hide()
        else:
            cursor.move_to(locker_x(nxt), LOCK_Y + LOCK_H + 5)
            cursor.show()
        shown["next"] = nxt
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) SPEEDS = (2, 3, 4) -> (3,) : ทุกตัวบินเร็วเท่ากัน ของที่ยืมก่อนคืนก่อนเสมอ (เข้าก่อน ออกก่อน)
# 2) POOL = 8 -> 4 : เต็มเร็วขึ้นมาก ได้ยินเสียงปฏิเสธบ่อย แต่คืนงบจอได้ 8 ชิ้น (ของ 4 + ช่อง 4)
#    กลับกัน POOL = 10 จะเกินงบ 26 ชิ้น เพราะของหนึ่งตัวกินสองชิ้น: ตัวมันกับช่องของมัน
# 3) ลบ item.hide() ในส่วนคืน : เกมยังทำงานเหมือนเดิม เพราะที่ทำให้ "ว่าง" คือค่า y < -20 ไม่ใช่การซ่อน

ชนแบบกล่อง (คาบ 4, 12)

ax<bx+bw  ∧  ax+aw>bx  ∧  ay<by+bh  ∧  ay+ah>bya_x < b_x + b_w \;\wedge\; a_x + a_w > b_x \;\wedge\; a_y < b_y + b_h \;\wedge\; a_y + a_h > b_y

def hit(a, b):                           # (bentogame.py)
    return (a.x < b.x + b.w and a.x + a.w > b.x and
            a.y < b.y + b.h and a.y + a.h > b.y)

ในเกม ในงานจริง
กระสุนโดนศัตรู · ลูกโดนไม้ · ของตกลงตะกร้า รถยกเข้าโซนอันตราย · กล่องผ่านเซนเซอร์ลำแสง · นิ้วแตะโดนปุ่มบนจอสัมผัส

กฎ: สี่เงื่อนไข "และ" กัน — ผิดข้อเดียว = ไม่ชน

ลองเล่น · ชนแบบกล่อง เห็นสี่เงื่อนไขเป็น True/False สด ๆ บนจอ

คัดลอกไฟล์ s13_09_aabb.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอย = ขับกล่องเหลือง (A) ไปรอบ ๆ สามโซน
  • แผงขวาแสดงตัวเลขจริงของ A กับโซนที่ใกล้ที่สุด (B) และสี่ข้อเป็น True / False พร้อมไฟ
  • ขับเข้าใกล้ทีละแกน ดูไฟเขียวทีละดวง ครบสี่ดวงเมื่อไร โซนกลายเป็นแดงและมีเสียงเตือน

ลองแก้ แล้วรันใหม่

  1. ขับ A ไปอยู่ใต้ ZONE 2 (แนว x ซ้อนกัน แต่ y ไม่ซ้อน) : ไฟข้อ 1-2 เขียว ข้อ 3-4 ไม่ครบ = ไม่ชน
  2. จอด A ชิดขอบซ้ายของ ZONE 2 พอดี (ข้อ 2 อ่าน 290>290 False = แตะขอบพอดียังไม่ชน) แล้วเปลี่ยน a.x + a.w > b.x ใน aabb_parts เป็น >= : จอดที่เดิม ข้อ 2 กลายเป็น True ทันที
  3. A_W, A_H = 46, 30 -> 120, 80 : รถใหญ่ขึ้น ชนง่ายขึ้นมาก ไม่ต้องขยับเข้าไปลึกก็ครบสี่ข้อ

โค้ดเต็ม · s13_09_aabb.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_09_aabb.py — คาบ 13: ชนแบบกล่อง เห็นสี่เงื่อนไขเป็น True/False สด ๆ บนจอ
#
# หลักการ   : ชนแบบกล่อง (คาบ 4, 12) กล่อง A ชนกล่อง B เมื่อสี่ข้อนี้จริง "พร้อมกัน" (และ / and)
#             ax < bx+bw   และ   ax+aw > bx   และ   ay < by+bh   และ   ay+ah > by
#             ข้อ 1-2 ดูแกน x (ซ้าย-ขวาซ้อนกันไหม) · ข้อ 3-4 ดูแกน y (บน-ล่างซ้อนกันไหม)
# ลองเล่น   : กด Start · จอย = ขับกล่องเหลือง (A) ไปรอบ ๆ สามโซน
#             แผงขวาแสดงตัวเลขจริงของ A กับโซนที่ใกล้ที่สุด (B) และสี่ข้อเป็น True / False พร้อมไฟ
#             ขับเข้าใกล้ทีละแกน ดูไฟเขียวทีละดวง ครบสี่ดวงเมื่อไร โซนกลายเป็นแดงและมีเสียงเตือน
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : กระสุนโดนศัตรู · ลูกโดนไม้ · ของตกลงตะกร้า (game.hit ใน bentogame.py)
# ในงานจริง : หุ่นยนต์ส่งของ (AGV) เข้าโซนห้ามเข้า · กล่องผ่านเซนเซอร์ลำแสง · นิ้วแตะโดนปุ่มบนจอสัมผัส
#             ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s04_catch.py (ของตกลงตะกร้า) · shooter_step4.py (ชนหลายคู่)
# งบวิดเจ็ต : 24 ชิ้น (สนาม 1 + โซน 3 + ป้ายโซน 3 + กล่อง A 1 + ไฟ 5 + ข้อความ 10 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   4) หน้าจอ: สร้างครั้งเดียว   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
STEP = 4                                  # กดจอยค้าง กล่อง A ขยับเฟรมละ 4 px
A_W, A_H = 46, 30                         # ขนาดกล่อง A (หุ่นยนต์ส่งของ AGV)
FIELD_X, FIELD_Y, FIELD_W, FIELD_H = 12, 10, 450, 350   # ขอบสนามลงตัวกับก้าว 4 px จอดชิดขอบโซนได้พอดี
ZONES = [                                 # (ชื่อ, x, y, w, h, สีปกติ)
    ("ZONE 1", 40, 40, 140, 90, 0x2D5F8A),
    ("ZONE 2", 290, 70, 150, 100, 0x2E7D4F),
    ("ZONE 3", 130, 220, 170, 90, 0x8A3D6E),
]
HUD_X = 478
LAMP_OFF = 0x3A4450
NAMES = ("ax < bx+bw", "ax+aw > bx", "ay < by+bh", "ay+ah > by")
OPS = ("<", ">", "<", ">")


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ ----
def aabb_parts(a, b):
    # สี่ข้อของการชนแบบกล่อง เรียงเหมือน game.hit() ใน bentogame.py ทุกตัวอักษร
    return (a.x < b.x + b.w, a.x + a.w > b.x, a.y < b.y + b.h, a.y + a.h > b.y)


def part_numbers(a, b):
    # ตัวเลขซ้าย-ขวาของแต่ละข้อ เอาไว้โชว์ว่าเทียบอะไรกับอะไร
    return ((a.x, b.x + b.w), (a.x + a.w, b.x), (a.y, b.y + b.h), (a.y + a.h, b.y))


def nearest(a, boxes):
    # โซนที่จุดกลางอยู่ใกล้จุดกลางของ A ที่สุด (เทียบระยะยกกำลังสอง ไม่ต้องถอดราก)
    ax, ay = a.center()
    best, best_d = 0, None
    for i, b in enumerate(boxes):
        bx, by = b.center()
        d = (ax - bx) * (ax - bx) + (ay - by) * (ay - by)
        if best_d is None or d < best_d:
            best, best_d = i, d
    return best


# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("AABB")
game.Box(FIELD_X, FIELD_Y, FIELD_W, FIELD_H, 0x18202A, border=0x3A4A5A)
zones = []
for name, x, y, w, h, color in ZONES:
    zones.append(game.Box(x, y, w, h, color, border=game.WHITE, radius=4))
    game.Text(name, x + 8, y + 6, game.WHITE)
agv = game.Box(332, 270, A_W, A_H, game.YELLOW, border=game.BLACK, border_w=2, radius=3)

hud_a = game.Text("", HUD_X, 12, game.YELLOW)
hud_name = game.Text("", HUD_X, 40, game.WHITE)
hud_b = game.Text("", HUD_X, 66, game.WHITE)
lamps, lines = [], []
for i in range(4):
    lamps.append(game.Box(HUD_X, 110 + i * 32, 14, 14, LAMP_OFF, radius=7))
    lines.append(game.Text("", HUD_X + 22, 104 + i * 32, game.WHITE))
hit_lamp = game.Box(HUD_X, 242, 30, 30, LAMP_OFF, radius=15)
hud_hit = game.Text("", HUD_X + 40, 246, game.WHITE)
game.Text("ผิดข้อเดียว = ไม่ชน", HUD_X, 290, 0x9AA8B8)
game.Text("จอย = ขับกล่อง A", HUD_X, 322, game.GB_LIGHT)

shown = {"lines": [""] * 4, "parts": [None] * 4, "near": None, "a": "", "hits": None}

# ---- 5) วงวนหลัก ----
def on_frame():
    keys = game.keys()

    # รับปุ่ม (วัด): ขับกล่อง A อยู่ในสนาม
    dx = (STEP if keys.right else 0) - (STEP if keys.left else 0)
    dy = (STEP if keys.down else 0) - (STEP if keys.up else 0)
    if dx or dy:
        x = max(FIELD_X, min(FIELD_X + FIELD_W - A_W, agv.x + dx))
        y = max(FIELD_Y, min(FIELD_Y + FIELD_H - A_H, agv.y + dy))
        agv.move_to(x, y)

    # คิด (ตัดสิน): โซนไหนใกล้สุด แล้วสี่ข้อของคู่นั้นเป็นอย่างไร · โซนไหนชนบ้าง
    near = nearest(agv, zones)
    b = zones[near]
    parts = aabb_parts(agv, b)
    nums = part_numbers(agv, b)
    hits = tuple(all(aabb_parts(agv, z)) for z in zones)    # ผลเดียวกับ game.hit(agv, z)

    # ออก (โชว์): อัปเดตเฉพาะที่เปลี่ยน
    text = "A x=%d y=%d w=%d h=%d" % (agv.x, agv.y, A_W, A_H)
    if text != shown["a"]:
        hud_a.set(text)
        shown["a"] = text
    if near != shown["near"]:
        hud_name.set("B = %s (ใกล้สุด)" % ZONES[near][0])
        hud_b.set("B x=%d y=%d w=%d h=%d" % (b.x, b.y, b.w, b.h))
        shown["near"] = near
    for i in range(4):
        text = "%s  %d%s%d  %s" % (NAMES[i], nums[i][0], OPS[i], nums[i][1],
                                   "True" if parts[i] else "False")
        if text != shown["lines"][i]:
            lines[i].set(text)
            shown["lines"][i] = text
        if parts[i] != shown["parts"][i]:
            lamps[i].set_color(game.GREEN if parts[i] else LAMP_OFF)
            shown["parts"][i] = parts[i]
    if hits != shown["hits"]:
        for i in range(len(zones)):
            if shown["hits"] is None or hits[i] != shown["hits"][i]:
                zones[i].set_color(game.RED if hits[i] else ZONES[i][5])
        if True in hits:
            hud_hit.set("HIT = True  " + ZONES[hits.index(True)][0])
            hit_lamp.set_color(game.RED)
            game.sfx("deny")
        else:
            hud_hit.set("HIT = False")
            hit_lamp.set_color(LAMP_OFF)
        shown["hits"] = hits
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) ขับ A ไปอยู่ใต้ ZONE 2 (แนว x ซ้อนกัน แต่ y ไม่ซ้อน) : ไฟข้อ 1-2 เขียว ข้อ 3-4 ไม่ครบ = ไม่ชน
# 2) จอด A ชิดขอบซ้ายของ ZONE 2 พอดี (ข้อ 2 อ่าน 290>290 False = แตะขอบพอดียังไม่ชน)
#    แล้วเปลี่ยน a.x + a.w > b.x ใน aabb_parts เป็น >= : จอดที่เดิม ข้อ 2 กลายเป็น True ทันที
# 3) A_W, A_H = 46, 30 -> 120, 80 : รถใหญ่ขึ้น ชนง่ายขึ้นมาก ไม่ต้องขยับเข้าไปลึกก็ครบสี่ข้อ

งบของบนจอ — 32 ชิ้น

ใช้ไปกับ ชิ้น
บอร์ดรับได้ทั้งหมด 32
game.run() (ป้ายคำใบ้ล่างจอ) −1
game.game_over() (ป้ายจบเกมชั่วคราว) −4
เผื่อไว้ −1
เหลือให้เกมของน้อง 26
  • กล่อง ข้อความ และภาพ นับชิ้นละหนึ่ง · ทุก move_to() และ set() คือหนึ่งข้อความที่ส่งข้ามแกนประมวลผลไปวาดจอ
  • Shooter ตัวเต็มเคยมีศัตรู 8 + กระสุน 6 แล้วลดเหลือ 5 + 4 เพื่อให้ลื่น (คาบ 15 เบื้องหลัง)
  • เกมตัวเต็มของคอร์สใช้: Shooter 25 · Pong 29 · Snake (ภาพสไปรต์) 32 ชิ้น

กฎ: วาดเฉพาะที่เปลี่ยน ขยับเฉพาะที่ต้องขยับ — จอหน้าเครื่องจักรจริงก็ใช้กฎเดียวกัน

ส่วนที่ 4 — สถานะ: ไฟจราจร → เกม → เครื่องจักร

ที่ไหน สถานะ เปลี่ยนเมื่อ
ไฟจราจร (คาบ 2) เขียว → เหลือง → แดง → เขียว กดปุ่ม · state = (state + 1) % 3
เกม (คาบ 7, 12) หน้าเริ่ม → เล่น → GAME OVER → เล่นใหม่ กด Start · ชีวิตหมด
จอเฝ้าโซนรถยก (โจทย์กลางภาค คลังสินค้า) NORMAL → WARNING → DANGER → LOCKOUT รถเข้าใกล้ · LOCKOUT ค้างจนหัวหน้ากดปลด
if pressed and not prev_pressed:     # จับเฉพาะจังหวะเพิ่งกด (edge)   (s02_button_fsm.py)
    state = (state + 1) % 3          # เลื่อนไปสถานะถัดไปแบบวนรอบ
  • กดหนึ่งครั้ง = นับหนึ่งครั้ง (edge) ใช้กับปุ่ม รับทราบสัญญาณเตือน · ในเกมใช้ game.pressed_once()
  • ปุ่ม หยุด ทำทันทีไม่ต้องถาม เพราะการหยุดปลอดภัยเสมอ

กฎ: ระบบอยู่ในสถานะเดียวเสมอ และรู้ชัดว่าอะไรทำให้ย้ายสถานะ

ลองเล่น · ส่วนที่ 4: เครื่องสถานะของเครื่องซักผ้า (อยู่ในสถานะเดียวเสมอ)

คัดลอกไฟล์ s13_10_fsm_machine.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · A = เริ่มซัก · B ตอนไหนก็ได้ = หยุดทันที · ทุกการย้ายสถานะมีเสียงไม่ซ้ำกัน
  • FILL น้ำขึ้นทีละแถบ · WASH ผ้าโยกไปมา · SPIN น้ำระบายออกแล้วผ้าหมุนเร็วขึ้นเรื่อย ๆ

ลองแก้ แล้วรันใหม่

  1. SECONDS = (0, 3, 5, 4, 3) -> (0, 3, 15, 4, 3) : ซักนานขึ้น 3 เท่า ตัวเลขเฟรมของ WASH เริ่มที่ 450
  2. NEXT = (IDLE, WASH, SPIN, DONE, IDLE) -> (IDLE, WASH, SPIN, WASH, IDLE) : ปั่นเสร็จวนกลับไปซักไม่จบ เครื่องจริงที่ตารางสถานะผิดหนึ่งช่องก็ทำแบบนี้ — ตาราง NEXT คือแผนที่ของทั้งเครื่อง
  3. ใน fsm_step ลบสองบรรทัดแรก (if stop ...) : กด B แล้วไม่มีอะไรเกิดขึ้น — ปุ่มหยุดต้องมาก่อนกฎอื่นเสมอ

โค้ดเต็ม · s13_10_fsm_machine.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_10_fsm_machine.py — คาบ 13 ส่วนที่ 4: เครื่องสถานะของเครื่องซักผ้า (อยู่ในสถานะเดียวเสมอ)
#
# หลักการ   : เครื่องสถานะ (คาบ 2, 7, 12)   IDLE -> FILL -> WASH -> SPIN -> DONE -> IDLE
#             ทุกสถานะมีตัวจับเวลาของตัวเอง N = t x fps นับถอยหลังทีละเฟรม ถึง 0 แล้วย้ายไปสถานะถัดไป
#             A = เริ่ม (จับจังหวะกด pressed_once ครั้งเดียว) · B = หยุด กลับ IDLE ทันทีจากทุกสถานะ
# ลองเล่น   : กด Start · A = เริ่มซัก · B ตอนไหนก็ได้ = หยุดทันที · ทุกการย้ายสถานะมีเสียงไม่ซ้ำกัน
#             FILL น้ำขึ้นทีละแถบ · WASH ผ้าโยกไปมา · SPIN น้ำระบายออกแล้วผ้าหมุนเร็วขึ้นเรื่อย ๆ
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : หน้าเริ่ม -> เล่น -> GAME OVER -> เล่นใหม่ (สถานะแมตช์ของ Pong และ Shooter)
# ในงานจริง : หน้าจอเครื่องซักผ้า เครื่องบรรจุขวด เตาอบอุตสาหกรรม ทำงานเป็นขั้น ๆ ตามเวลา
#             ปุ่มหยุดฉุกเฉินทำทันทีไม่ต้องยืนยัน เพราะการหยุดปลอดภัยเสมอ · เวลาทุกขั้นเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s02_button_fsm.py (จับจังหวะกด + ไฟจราจร) · s02_traffic_v2.py · pong_step5.py (สถานะแมตช์)
# งบวิดเจ็ต : 26 ชิ้น รวม game.run 1 (ตัวเครื่อง 1 + จอเครื่อง 1 + ประตู 1 + น้ำ 4 + ผ้า 3 + ป้ายใหญ่ 2
#             + ไฟ 5 + ป้ายไฟ 5 + ข้อความ 3 + game.run 1) · ไฟล์นี้ไม่ใช้ game_over() จึงไม่ต้องเผื่ออีก 4
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   4) หน้าจอ: สร้างครั้งเดียว   5) วงวนหลัก
import bentogame as game
import math

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30
IDLE, FILL, WASH, SPIN, DONE = 0, 1, 2, 3, 4
NAMES = ("IDLE", "FILL", "WASH", "SPIN", "DONE")
THAI = ("รอเริ่ม", "เติมน้ำ", "ซัก", "ปั่นหมาด", "เสร็จแล้ว")
SECONDS = (0, 3, 5, 4, 3)                     # อยู่ในแต่ละสถานะกี่วินาที (IDLE รอปุ่ม ไม่มีเวลา)
NEXT = (IDLE, WASH, SPIN, DONE, IDLE)         # หมดเวลาแล้วไปสถานะไหน
SOUNDS = ("back", "start", "select", "fire", "win")    # เสียงตอนเข้าแต่ละสถานะ (หยุดด้วย B = "deny")
BANNER = (0x3A4450, 0x1F5FAF, 0x0F7F8F, 0xB0601A, 0x2E8B57)   # สีป้ายใหญ่ (เข้มพอให้ตัวขาวอ่านง่าย)
LAMP = (0x8899AA, game.BLUE, game.CYAN, game.ORANGE, game.GREEN)
LAMP_OFF = 0x2A3440
CX, CY, R = 175, 200, 80                      # จุดกลางและรัศมีของประตูกลม
LR = 46                                       # รัศมีวงที่ผ้าวิ่ง (อยู่ในถังเสมอ)

# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ ----
def frames_for(state):
    return SECONDS[state] * FPS                # N = t x fps

def fsm_step(state, left, start, stop):
    # กฎทั้งหมดของเครื่องอยู่ที่นี่ที่เดียว คืน (สถานะใหม่, เฟรมที่เหลือ)
    if stop and state != IDLE:
        return IDLE, 0                         # หยุดไม่ถาม ไม่สนว่าอยู่สถานะไหน
    if state == IDLE:
        return (FILL, frames_for(FILL)) if start and not stop else (IDLE, 0)   # กดหยุดค้าง = เริ่มไม่ได้
    left -= 1                                  # นับเฟรมถอยหลัง (ไม่มี sleep)
    if left > 0:
        return state, left
    return NEXT[state], frames_for(NEXT[state])

def water_level(state, left):
    # จำนวนแถบน้ำ 0..4 : FILL ขึ้นทีละแถบ · WASH เต็ม · SPIN ระบายหมดในครึ่งแรก
    total = frames_for(state)
    if state == FILL: return min(4, (total - left) * 4 // total + 1)
    if state == WASH: return 4
    if state == SPIN: return max(0, 4 - (total - left) * 8 // total)
    return 0

# ---- 4) หน้าจอ: สร้างครั้งเดียว ----
game.title("WASHER FSM")
game.Box(50, 20, 250, 330, 0xD8DEE6, border=0x8899AA, border_w=2, radius=14)
game.Box(66, 34, 218, 40, 0x2A3440, radius=8)                  # จอเล็กของเครื่อง: เฟรมที่เหลือ
hud_time = game.Text("", 84, 44, game.GREEN)
game.Box(CX - R, CY - R, 2 * R, 2 * R, 0x22303C, border=0x8899AA, border_w=6, radius=R)
water = []
for i in range(4):                             # แถบล่างสุดคือแถบที่ 0 กว้างตามโค้งของถัง
    d = R - 14 - i * 18 - 9                    # ระยะจากจุดกลางถึงกลางแถบ
    w = int(2 * math.sqrt((R - 8) ** 2 - d * d))
    water.append(game.Box(CX - w // 2, CY + d - 9, w, 18, 0x3D8BD9, radius=6))
    water[i].hide()
clothes = [game.Box(0, 0, 16, 16, c, border=game.WHITE, radius=5)
           for c in (game.RED, game.YELLOW, game.PINK)]
banner = game.Box(330, 20, 440, 64, BANNER[IDLE], radius=10)
hud_state = game.Text("", 352, 40, game.WHITE)
lamps = []
for i in range(5):
    lamps.append(game.Box(344 + i * 88, 112, 30, 30, LAMP_OFF, border=0x556070, radius=15))
    game.Text(NAMES[i], 338 + i * 88, 150, game.WHITE)
hud_rule = game.Text("", 340, 200, game.WHITE)
game.Text("A = START (กดครั้งเดียว)    B = STOP ทันที", 340, 250, game.GB_LIGHT)

def place(cloth, angle, radius):               # วางผ้าบนวงกลมรอบจุดกลางถัง ที่มุม angle (เรเดียน)
    cloth.move_to(int(CX + radius * math.cos(angle)) - 8, int(CY + radius * math.sin(angle)) - 8)

def enter(new, old, by_stop):
    # ทำครั้งเดียวตอนย้ายสถานะ: ป้าย ไฟ เสียง (อัปเดตเฉพาะที่เปลี่ยน)
    banner.set_color(BANNER[new])
    hud_state.set("%s   %s" % (NAMES[new], THAI[new]))
    lamps[old].set_color(LAMP_OFF)
    lamps[new].set_color(LAMP[new])
    if new != old:                             # ตอนเปิดเครื่องครั้งแรกไม่มีเสียง
        game.sfx("deny" if by_stop else SOUNDS[new])
    if new in (IDLE, FILL, DONE):              # ผ้ากองนิ่งที่ก้นถัง
        for i, cloth in enumerate(clothes): place(cloth, math.pi / 2 + (i - 1) * 0.5, 52)
    if new == IDLE:
        hud_time.set("STOP" if by_stop else "READY")
        hud_rule.set("หยุดแล้ว (B)  กด A เริ่มใหม่" if by_stop else "กด A เพื่อเริ่มซัก")
    else:                                      # ตัวจับเวลาของสถานะนี้: N = t x fps
        hud_rule.set("%s = %d s x %d = %d f" % (NAMES[new], SECONDS[new], FPS, frames_for(new)))


state, left, frame, spin, omega, level = IDLE, 0, 0, 0.0, 0.0, 0
enter(IDLE, IDLE, False)

# ---- 5) วงวนหลัก ----
def on_frame():
    global state, left, frame, spin, omega, level
    keys = game.keys()
    frame += 1
    # รับปุ่ม (วัด) + คิด (ตัดสิน): A จับจังหวะกด · B ดูแค่ว่ากดอยู่ไหม (หยุดต้องไวที่สุด)
    start = game.pressed_once("a", keys)
    new, left = fsm_step(state, left, start, keys.b)
    if new != state:
        enter(new, state, keys.b and state != IDLE)
        state, spin, omega = new, 0.0, 0.0
    # ออก (โชว์): น้ำเปลี่ยนเฉพาะตอนจำนวนแถบเปลี่ยน · ผ้าขยับเฉพาะตอน WASH กับ SPIN
    lv = water_level(state, left)
    if lv != level:
        for i in range(4):
            if (i < lv) != (i < level):
                water[i].show() if i < lv else water[i].hide()
        level = lv
    if state == WASH:                          # โยกไปมาใต้ถัง (ซักแบบตีน้ำ)
        for i, cloth in enumerate(clothes):
            place(cloth, math.pi / 2 + 0.9 * math.sin(frame * 0.18 + i * 0.9), LR)
    elif state == SPIN:                        # หมุนรอบถัง เร่งขึ้นทีละนิดจนถึงเพดาน 0.5 เรเดียนต่อเฟรม
        omega = min(0.5, omega + 0.01)
        spin += omega
        for i, cloth in enumerate(clothes):
            place(cloth, spin + i * 2.094, LR)  # ห่างกัน 120 องศา
    if state != IDLE:
        hud_time.set("เหลือ %d f" % left)
    return True


game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) SECONDS = (0, 3, 5, 4, 3) -> (0, 3, 15, 4, 3) : ซักนานขึ้น 3 เท่า ตัวเลขเฟรมของ WASH เริ่มที่ 450
# 2) NEXT = (IDLE, WASH, SPIN, DONE, IDLE) -> (IDLE, WASH, SPIN, WASH, IDLE) : ปั่นเสร็จวนกลับไปซักไม่จบ
#    เครื่องจริงที่ตารางสถานะผิดหนึ่งช่องก็ทำแบบนี้ — ตาราง NEXT คือแผนที่ของทั้งเครื่อง
# 3) ใน fsm_step ลบสองบรรทัดแรก (if stop ...) : กด B แล้วไม่มีอะไรเกิดขึ้น — ปุ่มหยุดต้องมาก่อนกฎอื่นเสมอ

ส่วนที่ 5 — HUD ในเกม

hud = game.Text("Score: 0   Lives: 3", 10, 8, game.WHITE)       # (shooter_step4.py)
...
lives -= 1
hud.set("Score: %d   Lives: %d" % (score, lives))   # อัปเดตเฉพาะตอนค่าเปลี่ยน
  1. ตัวเลขสำคัญอยู่ มุมเดิมเสมอ ผู้เล่นไม่ต้องหา
  2. อัปเดต เฉพาะตอนค่าเปลี่ยน ไม่ใช่ทุกเฟรม
  3. ข่าวใหญ่ (GAME OVER · ชนะ) อยู่ กลางจอ ตัวใหญ่

กฎ: HUD ตอบคำถามเดียว — "ตอนนี้ฉันเป็นยังไง" ภายในครึ่งวินาที

ลองเล่น · HUD ในเกม

คัดลอกไฟล์ s13_11_game_hud.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · รอป้าย READY หายไป · จอยซ้าย/ขวา = เลื่อนตะกร้ารับผลไม้
  • รับได้ = คะแนนซ้ายบนขึ้น · พลาด = ยานขวาบนหายไปหนึ่งลำ · 5 วินาทีสุดท้ายมีเสียงนับ
  • ชีวิตหมดหรือเวลาหมด = GAME OVER กลางจอ · กด A เล่นใหม่

ลองแก้ แล้วรันใหม่

  1. ย้าย hud_score.set(...) ออกมานอก if ให้เขียนทุกเฟรม : ตัวเลขเหมือนเดิม แต่บอร์ดต้องส่งข้อความวาดจอ เพิ่ม 30 ครั้งต่อวินาทีโดยไม่จำเป็น (งบจริงราว 10-15 ข้อความต่อเฟรม) เกมเริ่มหน่วงบนบอร์ด
  2. LIVES = 3 -> 5 : ยานมุมขวาบนเพิ่มเป็น 5 ลำเอง เพราะตำแหน่งคิดจากสูตร ไม่ได้พิมพ์ทีละลำ
  3. READY_FRAMES = 90 -> 20 : ป้าย READY แวบเดียว ผู้เล่นตั้งตัวไม่ทัน ข่าวใหญ่ต้องค้างนานพอให้อ่านทัน

โค้ดเต็ม · s13_11_game_hud.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_11_game_hud.py — คาบ 13 (คาบพิเศษ): HUD ในเกม = ตอบ "ตอนนี้ฉันเป็นยังไง" ภายในครึ่งวินาที
#
# หลักการ   : กฎ HUD 3 ข้อ (สไลด์ส่วนที่ 5)
#             1) ตัวเลขสำคัญอยู่มุมเดิมเสมอ   คะแนนซ้ายบน · เวลากลางบน · ชีวิต (ยาน) ขวาบน
#             2) อัปเดตเฉพาะตอนค่าเปลี่ยน      จำข้อความล่าสุดไว้ ต่างจากเดิมค่อย set()   (คาบ 4, 12)
#             3) ข่าวใหญ่อยู่กลางจอ ตัวใหญ่     READY ตอนเริ่ม · GAME OVER ตอนจบ   (เครื่องสถานะ คาบ 2, 7)
#             เวลาคือการนับเฟรม  วินาทีที่เหลือ = (เฟรมทั้งหมด - เฟรมที่เล่นไป) / fps ปัดขึ้น  (คาบ 4, 11)
# ลองเล่น   : กด Start · รอป้าย READY หายไป · จอยซ้าย/ขวา = เลื่อนตะกร้ารับผลไม้
#             รับได้ = คะแนนซ้ายบนขึ้น · พลาด = ยานขวาบนหายไปหนึ่งลำ · 5 วินาทีสุดท้ายมีเสียงนับ
#             ชีวิตหมดหรือเวลาหมด = GAME OVER กลางจอ · กด A เล่นใหม่
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : แถบคะแนน ชีวิต เวลา ของทุกเกม (shooter_step4.py ใช้ป้าย Score / Lives มุมซ้ายบน)
# ในงานจริง : แถบสถานะของหน้าจอควบคุม ยอดผลิตอยู่มุมเดิม · เวลากะที่เหลืออยู่กลาง
#             เหตุใหญ่ขึ้นกลางจอ · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s04_catch.py (ตะกร้ารับของ) · shooter_step4.py (HUD คะแนน + ชีวิต)
# งบวิดเจ็ต : 17 ชิ้น (ผลไม้ 3 + แถบ HUD 1 + พื้น 1 + ตะกร้า 1 + ยานบอกชีวิต 3 + ข้อความ HUD 2
#                    + ป้ายกลางจอ 1 + ข้อความในป้าย 4 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30
GAME_SECONDS = 30   # เล่นรอบละกี่วินาที
LIVES = 3           # ชีวิต = จำนวนยานมุมขวาบน
READY_FRAMES = 90   # ป้าย READY ค้าง 90 เฟรม = 3 วินาที
FRUITS = 3          # ผลไม้ที่ตกพร้อมกัน (ชุดหมุนเวียน สร้างครั้งเดียว)
FALL_MIN, FALL_MAX = 18, 30        # ความเร็วตก หน่วย 0.1 px ต่อเฟรม (1.8 ถึง 3.0)
BASKET_W, BASKET_SPEED = 110, 9
GROUND_Y = 356      # พื้น (ผลไม้ตกถึงตรงนี้ = พลาด)
SHIP_W, FRUIT = 62, 16             # ขนาดภาพ ship กับ snake_food (ภาพไม่รู้ขนาดตัวเอง)
TOTAL = GAME_SECONDS * FPS

# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def seconds_left(frame, total, fps):
    # ปัดขึ้น: เฟรมแรกยังเห็น 30 ไม่ใช่ 29 และเห็น 0 ตอนหมดเวลาจริงเท่านั้น
    return max(0, (total - frame + fps - 1) // fps)

def next_phase(phase, ready_count, lives, secs):
    # เครื่องสถานะของรอบเกม  ready -> play -> over
    if phase == "ready" and ready_count <= 0:
        return "play"
    if phase == "play" and (lives <= 0 or secs <= 0):
        return "over"
    return phase

def centre_x(msg):
    # ตัวอักษรกว้างราว 10 px  กลางจอ = กึ่งกลางจอ - ครึ่งความยาวข้อความ
    return game.WIDTH // 2 - len(msg) * 5

# ---- 3) ตรวจสมองก่อนเปิดจอ ----
def self_test():
    cases = [("seconds_left", seconds_left(0, 900, 30), 30), ("seconds_left", seconds_left(29, 900, 30), 30),
             ("seconds_left", seconds_left(899, 900, 30), 1), ("seconds_left", seconds_left(900, 900, 30), 0),
             ("next_phase", next_phase("ready", 0, 3, 30), "play"), ("next_phase", next_phase("play", 0, 0, 12), "over"),
             ("next_phase", next_phase("play", 0, 2, 0), "over"), ("next_phase", next_phase("play", 0, 2, 5), "play")]
    for name, got, want in cases:
        if got != want:
            print("ยังไม่ผ่าน:", name, "ได้", got, "ควรได้", want)
            return False
    print("ผ่าน! สมองของ HUD ถูกทั้ง", len(cases), "กรณี")
    return True

if not self_test():
    raise SystemExit

# ---- 4) หน้าจอ: สร้างครั้งเดียว (ลำดับการวาด = ลำดับการสร้าง) ----
game.title("GAME HUD")
fruits = game.pool("snake_food", FRUITS)                      # สร้างก่อนแถบ HUD = ผลไม้โผล่จากใต้แถบ
for f in fruits:
    f.w, f.h = FRUIT, FRUIT
game.Box(0, 0, game.WIDTH, 40, 0x1E2A36)                      # แถบ HUD: บ้านประจำของตัวเลข
game.Box(0, GROUND_Y, game.WIDTH, 8, game.GB_DARK)            # พื้น
basket = game.Box(340, GROUND_Y - 18, BASKET_W, 14, game.ORANGE, border=game.WHITE, radius=4)
ships = [game.Sprite("ship", game.WIDTH - 12 - (LIVES - i) * (SHIP_W + 6), 6) for i in range(LIVES)]
hud_score = game.Text("", 12, 11, game.WHITE)                 # กฎ 1: ซ้ายบนเสมอ
hud_time = game.Text("", centre_x("TIME 30"), 11, game.YELLOW)  # กฎ 1: กลางบนเสมอ
banner = game.Box(236, 150, 320, 90, 0x10122C, border=game.YELLOW, radius=8, border_w=3)

def line(msg, y, color):
    return (game.Text("", centre_x(msg), y, color), msg)      # สร้างว่างไว้ จะโชว์ค่อย set

ready_lines = [line("READY", 172, game.YELLOW), line("catch the fruit", 204, game.WHITE)]
over_lines = [line("GAME OVER", 172, game.RED), line("A = play again", 204, game.WHITE)]

def show_banner(lines, on):
    # กฎ 3: ข่าวใหญ่ขึ้นกลางจอในกรอบเด่น แล้วหายไปเมื่อหมดข่าว
    if on: banner.show()
    else:  banner.hide()
    for text, msg in lines:
        text.set(msg if on else "")

st = {"phase": "ready", "count": 0, "frame": 0, "score": 0, "lives": 0}
shown = {"score": None, "time": None}                         # กฎ 2: จำค่าที่โชว์ล่าสุด
fall = [0.0] * FRUITS

def drop(i):
    fruits[i].move_to(random.randint(20, game.WIDTH - 36), -random.randint(20, 400))
    fall[i] = random.randint(FALL_MIN, FALL_MAX) / 10

def new_round():
    st.update({"phase": "ready", "count": READY_FRAMES, "frame": 0, "score": 0, "lives": LIVES})
    for s in ships: s.show()
    for i in range(FRUITS):
        drop(i)
        fruits[i].show()
    show_banner(over_lines, False)
    show_banner(ready_lines, True)

new_round()

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    if st["phase"] == "over":                                 # จบแล้ว รอ A อย่างเดียว
        if game.pressed_once("a", k): new_round()
        return True
    if k.left:  basket.move(-BASKET_SPEED, 0)
    if k.right: basket.move(BASKET_SPEED, 0)
    if st["phase"] == "ready":
        st["count"] -= 1
    else:
        st["frame"] += 1
        for i in range(FRUITS):
            f = fruits[i]
            f.move_to(f.x, f.y + fall[i])
            if game.hit(f, basket):
                st["score"] += 1
                game.sfx("eat")
                drop(i)
            elif f.y > GROUND_Y - FRUIT:
                if st["lives"] > 0:
                    st["lives"] -= 1
                    ships[st["lives"]].hide()                 # ชีวิตหาย = ยานหายหนึ่งลำ ที่มุมเดิม
                    game.sfx("lose_life")
                drop(i)

    secs = seconds_left(st["frame"], TOTAL, FPS)
    if st["score"] != shown["score"]:                         # กฎ 2: เปลี่ยนเมื่อไรค่อยเขียน
        hud_score.set("SCORE %d" % st["score"])
        shown["score"] = st["score"]
    if secs != shown["time"]:                                 # วินาทีละครั้ง ไม่ใช่ทุกเฟรม
        hud_time.set("TIME %d" % secs)
        shown["time"] = secs
        if st["phase"] == "play" and 0 < secs <= 5:
            game.sfx("move")                                  # 5 วินาทีสุดท้าย นับด้วยเสียง

    phase = next_phase(st["phase"], st["count"], st["lives"], secs)
    if phase != st["phase"]:
        st["phase"] = phase
        if phase == "play":
            show_banner(ready_lines, False)
            game.sfx("select")
        else:
            show_banner(over_lines, True)
            game.sfx("gameover")
    return True

game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) ย้าย hud_score.set(...) ออกมานอก if ให้เขียนทุกเฟรม : ตัวเลขเหมือนเดิม แต่บอร์ดต้องส่งข้อความวาดจอ
#    เพิ่ม 30 ครั้งต่อวินาทีโดยไม่จำเป็น (งบจริงราว 10-15 ข้อความต่อเฟรม) เกมเริ่มหน่วงบนบอร์ด
# 2) LIVES = 3 -> 5 : ยานมุมขวาบนเพิ่มเป็น 5 ลำเอง เพราะตำแหน่งคิดจากสูตร ไม่ได้พิมพ์ทีละลำ
# 3) READY_FRAMES = 90 -> 20 : ป้าย READY แวบเดียว ผู้เล่นตั้งตัวไม่ทัน ข่าวใหญ่ต้องค้างนานพอให้อ่านทัน

HUD ในโรงงาน — หน้าจอควบคุม (HMI)

ภาพจริงจาก BENTO Emulator: ถังน้ำตอนเตือน น้ำต่ำ (ซ้าย) · สายพานตอน ผิดอีก 1 ครั้งจะหยุด (ขวา) — แถบสถานะบนสุด · การ์ดค่า · บรรทัดเตือน

  • ปกติ = สีเรียบ (เทา) · ผิดปกติ = สีเด่น (เหลือง แดง) — สีแดงมีความหมายเดียว อย่าใช้แดงบอก "ปั๊มปิด"
  • มาตรฐาน ISA-101 ให้แต่ละโรงงานเขียนกติกาสีของตัวเอง แล้วใช้ให้ตรงกันทุกจอ แนวที่นิยมคือพื้นเทา ค่าปกติสีเรียบ และสีสดเฉพาะตอนผิดปกติ (high-performance HMI)

กฎ: เกมใช้สีให้ตื่นเต้น หน้าจอควบคุมใช้สีให้ คนเห็นปัญหาก่อน

ลองเล่น · หน้าจอควบคุมถังน้ำโรงเรือน

คัดลอกไฟล์ s13_tank_hmi.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยขวา = ดึงน้ำออกมากขึ้น · จอยซ้าย = ดึงน้ำออกน้อยลง
  • A = เป้าหมายสูงขึ้น 5% · B = เป้าหมายต่ำลง 5%
  • ดูวาล์วไล่ระดับน้ำให้เข้าใกล้เส้นเป้าหมาย (เส้นเหลือง) แล้วหยุดนิ่งในช่องว่าง (HOLD)
  • น้ำในท่อเข้าไหลเฉพาะตอนวาล์วเปิด · จุดน้ำในท่อออกวิ่งเร็วตามค่า OUT
  • ลองดึงน้ำออกจนเกินที่วาล์วเติมทัน แล้วดูแถบบนกลายเป็นสัญญาณเตือน LOW WATER พร้อมเสียง

ลองแก้ แล้วรันใหม่

  1. BAND = 3 -> 0 : วาล์วเปิด-ปิดสลับไม่หยุด (วงกลมวาล์วกะพริบเขียว-ส้ม การ์ด VALVE สลับ OPEN+ / CLOSE-)
  2. VALVE_RATE = 0.02 -> 0.2 : วาล์วตอบสนองไวขึ้น แต่ระดับน้ำแกว่งเกินเส้นเป้ามากขึ้นไหม
  3. OUT_MAX = 0.80 -> 0.50 : น้ำยังลดจนเตือนได้ไหม เมื่อน้ำออกสูงสุดน้อยกว่าที่วาล์วเติมทัน

โค้ดเต็ม · s13_tank_hmi.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_tank_hmi.py — คาบ 13 (คาบพิเศษ): หน้าจอควบคุมถังน้ำโรงเรือน = เทคนิคจาก Pong ในงานจริง
#
# หลักการ   : การ์ด 1 สะสมค่าทีละเฟรม   ระดับน้ำ = เดิม + น้ำเข้า - น้ำออก      (คาบ 5)
#             การ์ด 4 จำกัดขอบ          ระดับน้ำอยู่ระหว่าง 0 ถึง 100 เสมอ        (คาบ 3)
#             การ์ด 5 ตัวควบคุมสามระดับ  วาล์วเปิดเพิ่ม / หยุด / ปิดลง มีช่องว่าง  (คาบ 7)
#             แบบเดียวกับไม้ AI ใน pong_step5.py ที่ขยับขึ้น หยุด หรือลง ตามลูก
# ลองเล่น   : กด Start · จอยขวา = ดึงน้ำออกมากขึ้น · จอยซ้าย = ดึงน้ำออกน้อยลง
#             A = เป้าหมายสูงขึ้น 5% · B = เป้าหมายต่ำลง 5%
#             ดูวาล์วไล่ระดับน้ำให้เข้าใกล้เส้นเป้าหมาย (เส้นเหลือง) แล้วหยุดนิ่งในช่องว่าง (HOLD)
#             น้ำในท่อเข้าไหลเฉพาะตอนวาล์วเปิด · จุดน้ำในท่อออกวิ่งเร็วตามค่า OUT
#             ลองดึงน้ำออกจนเกินที่วาล์วเติมทัน แล้วดูแถบบนกลายเป็นสัญญาณเตือน LOW WATER พร้อมเสียง
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : ลูกบอลวิ่ง (สะสมค่า) · ไม้ไม่หลุดจอ (จำกัดขอบ) · ไม้ AI ไล่ลูก (สามระดับ + ช่องว่าง)
# ในงานจริง : หน้าจอถังน้ำในโรงเรือน วาล์วมอเตอร์เติมน้ำเองให้ระดับใกล้เป้าหมาย
#             ช่องว่างกันวาล์วเปิด-ปิดสลับไปมาจนพัง · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และเครื่องจำลอง
# ต่อยอดจาก : pong_step1.py (สะสมค่า) · pong_step2.py (จำกัดขอบ) · pong_step5.py (ช่องว่าง)
# งบวิดเจ็ต : 26 ชิ้น (แถบสถานะ 3 + ถังกับน้ำ 4 + เส้นเป้า 2 + ท่อเข้า 3 + วาล์ว 2 + ท่อออก 3
#             + การ์ดค่า 8 + game.run 1) · ต่อเฟรมส่งคำสั่งราว 5-9 ครั้ง (ตอนน้ำข้ามขั้น 10% เพิ่มอีก 3)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
IN_MAX = 0.6        # วาล์วเปิดสุด เติมน้ำได้กี่ % ต่อเฟรม
OUT_START = 0.20    # น้ำออกตอนเริ่ม (% ต่อเฟรม)
OUT_MAX = 0.80      # ดึงน้ำออกได้สูงสุด — มากกว่า IN_MAX จึงทำให้น้ำลดได้แม้วาล์วเปิดสุด
OUT_STEP = 0.01     # กดจอยค้าง 1 เฟรม น้ำออกเปลี่ยนเท่านี้
VALVE_RATE = 0.02   # วาล์วมอเตอร์ขยับได้เฟรมละ 2% (มอเตอร์จริงหมุนช้า ไม่ได้เปิดทันที)
BAND = 3            # ช่องว่าง: ห่างเป้าไม่เกิน 3% = วาล์วหยุดนิ่ง
TARGET_START = 60   # เป้าหมายระดับน้ำ (%)
LOW_ALARM = 20      # ต่ำกว่านี้ = เตือน LOW WATER
ALARM_EVERY = 20    # เตือนดังทุก 20 เฟรม (ไม่ใช้ sleep ในวงวน)

TANK_X, TANK_Y, TANK_W, TANK_H = 160, 52, 196, 294
SEG_H = 28          # น้ำหนึ่งขั้น 10% สูง 28 px (10 ขั้น = 280 px = 100%)
BOTTOM = TANK_Y + TANK_H - 8       # ก้นถังด้านใน (y)
PIPE_Y, OUT_Y = 72, 324            # ท่อน้ำเข้า (บนซ้าย) · ท่อน้ำออก (ล่างขวา)
CARD_X, CARD_W = 548, 232          # คอลัมน์การ์ดค่าด้านขวา
PANEL, EDGE, GRAY = 0x1B2633, 0x3D5266, 0x556070   # แผง · ขอบ · วาล์วหยุด
PIPE, TANK_BG = 0x4A5868, 0x101A24
DEEP, LIGHT = 0x1E5FA8, 0x5DB4F0   # น้ำสองเฉด: เนื้อน้ำเข้ม · ผิวน้ำอ่อน


# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def clamp(value, low, high):
    return max(low, min(high, value))


def next_level(level, inflow, outflow):
    # การ์ด 1 + การ์ด 4: สะสมค่าทีละเฟรม แล้วจำกัดให้อยู่ใน 0..100
    return clamp(level + inflow - outflow, 0, 100)


def valve_step(level, target, band):
    # การ์ด 5: ตัวควบคุมสามระดับ คืน +1 = เปิดเพิ่ม · 0 = หยุด · -1 = ปิดลง
    if level < target - band:
        return 1                 # น้ำน้อยกว่าเป้า -> เปิดวาล์วเพิ่ม
    if level > target + band:
        return -1                # น้ำมากกว่าเป้า -> ปิดวาล์วลง
    return 0                     # อยู่ในช่องว่าง -> หยุดนิ่ง


def bars_on(level):
    # แถบ 10 ก้อน ก้อนละ 10% (น้ำ 0-9.9% = ยังไม่ขึ้นสักก้อน)
    return int(level) // 10


# ---- 3) ตรวจสมองก่อนเปิดจอ ----
def self_test():
    cases = [
        ("next_level", next_level(50, 1.0, 0.5), 50.5),
        ("next_level", next_level(99.8, 1.0, 0.0), 100),   # ล้นไม่ได้
        ("next_level", next_level(0.2, 0.0, 1.0), 0),      # ติดลบไม่ได้
        ("valve_step", valve_step(40, 60, 3), 1),
        ("valve_step", valve_step(58, 60, 3), 0),
        ("valve_step", valve_step(63, 60, 3), 0),          # ขอบช่องว่างพอดี = ยังหยุด
        ("valve_step", valve_step(70, 60, 3), -1),
        ("bars_on", bars_on(9.9), 0),
        ("bars_on", bars_on(55), 5),
        ("bars_on", bars_on(100), 10),
    ]
    for name, got, want in cases:
        if abs(got - want) > 1e-6:
            print("ยังไม่ผ่าน:", name, "ได้", got, "ควรได้", want)
            return False
    print("ผ่าน! สมองของถังน้ำถูกทั้ง", len(cases), "กรณี")
    return True


if not self_test():
    raise SystemExit

# ---- 4) หน้าจอ: สร้างครั้งเดียว (สร้างก่อน = อยู่ล่าง สร้างทีหลัง = ทับข้างบน) ----
game.title("TANK HMI")
bar = game.Box(-4, -4, 800, 44, PANEL, border=EDGE, radius=4)        # แถบสถานะบนสุด (ชิดขอบจอ)
game.Text("ถังน้ำโรงเรือน   TANK HMI", 14, 9, game.WHITE)
hud_state = game.Text("ปกติ  NORMAL", 560, 9, game.WHITE)

game.Box(20, PIPE_Y, TANK_X - 16, 14, PIPE, border=EDGE)                # ท่อน้ำเข้า
flow_in = [game.Box(26 + i * 58, PIPE_Y + 4, 14, 6, LIGHT) for i in range(2)]
valve_sym = game.Box(62, PIPE_Y - 12, 38, 38, GRAY, border=game.WHITE, radius=19, border_w=3)
valve_tag = game.Text("HOLD", 54, PIPE_Y + 34, game.WHITE)
game.Box(TANK_X + TANK_W - 6, OUT_Y, 170, 14, PIPE, border=EDGE)       # ท่อน้ำออก
drops = [game.Box(TANK_X + TANK_W + 10 + i * 71, OUT_Y + 4, 10, 6, LIGHT) for i in range(2)]

tank = game.Box(TANK_X, TANK_Y, TANK_W, TANK_H, TANK_BG, border=0x8899AA, radius=6, border_w=3)
water = game.Box(TANK_X + 6, BOTTOM, TANK_W - 12, SEG_H, DEEP)      # เนื้อน้ำสีเข้ม สูงทีละขั้น 10%
water.hide()
surface = game.Box(TANK_X + 6, BOTTOM, TANK_W - 12, SEG_H, LIGHT)   # ผิวน้ำสีอ่อน ขยับทุก 1%
game.Box(TANK_X - 14, BOTTOM, TANK_W + 28, SEG_H + 2, 0x2A3644, border=EDGE, radius=4)  # ฐานถัง บังผิวน้ำที่เลยก้น


def level_to_y(level):
    return int(BOTTOM - level * (10 * SEG_H) / 100)


target_line = game.Box(TANK_X - 6, level_to_y(TARGET_START), TANK_W + 12, 3, game.YELLOW)
target_tag = game.Text("เป้า %d%%" % TARGET_START, TANK_X + TANK_W + 14, level_to_y(TARGET_START) - 11, game.YELLOW)


def card(row, accent, title):
    # การ์ดค่า: แผงมืดขอบสี + ป้ายสองบรรทัด (บรรทัดบน = ชื่อ · บรรทัดล่าง = ค่า)
    y = 52 + row * 76
    game.Box(CARD_X, y, CARD_W, 66, PANEL, border=accent, radius=8, border_w=2)
    return game.Text(title, CARD_X + 16, y + 9, game.WHITE)


cards = {"level": card(0, LIGHT, "ระดับน้ำ  LEVEL"), "target": card(1, game.YELLOW, "เป้าหมาย  TARGET"),
         "out": card(2, game.CYAN, "น้ำออก  OUT"), "valve": card(3, game.GREEN, "วาล์ว  VALVE")}

level, target, outflow, valve = 50.0, TARGET_START, OUT_START, 0.0
shown = {"bars": 0, "move": None, "low": False, "open": True}
alarm_count = 0
VALVE_LOOK = {1: (game.GREEN, "OPEN+"), 0: (GRAY, "HOLD"), -1: (game.ORANGE, "CLOSE-")}


def show(key, widget, text):
    # อัปเดตป้ายเฉพาะตอนข้อความเปลี่ยน (ไม่เปลี่ยน = ไม่ส่งคำสั่งไปจอ)
    if shown.get(key) != text:
        widget.set(text)
        shown[key] = text


def slide(items, step, start, end):
    # เลื่อนกล่องเล็กไปทางขวา ถึงปลายท่อแล้ววนกลับต้นท่อ · ส่งคำสั่งเฉพาะตอนพิกเซลเปลี่ยน
    for it in items:
        x = it.x + step
        if x > end:
            x -= end - start
        if int(x) != int(it.x):
            it.move_to(x, it.y)
        else:
            it.x = x


# ---- 5) วงวนหลัก ----
def on_frame():
    global level, target, outflow, valve, alarm_count
    keys = game.keys()

    # รับปุ่ม (วัด)
    if keys.right: outflow = clamp(outflow + OUT_STEP, 0, OUT_MAX)
    if keys.left:  outflow = clamp(outflow - OUT_STEP, 0, OUT_MAX)
    if game.pressed_once("a", keys): target = clamp(target + 5, 10, 90)
    if game.pressed_once("b", keys): target = clamp(target - 5, LOW_ALARM + BAND + 2, 90)

    # คิด (ตัดสิน): วาล์วขยับตามตัวควบคุมสามระดับ แล้วระดับน้ำสะสมค่า
    move = valve_step(level, target, BAND)
    valve = clamp(valve + move * VALVE_RATE, 0, 1)
    level = next_level(level, valve * IN_MAX, outflow)
    low = level < LOW_ALARM

    # ออก (ทำ + โชว์) — น้ำในถัง: เนื้อน้ำเปลี่ยนเฉพาะตอนข้ามขั้น 10% (ไม่ resize ทุกเฟรม)
    bars = bars_on(level)
    if bars != shown["bars"]:
        if bars == 0:
            water.hide()
        else:
            water.move_to(water.x, BOTTOM - bars * SEG_H)
            water.resize(water.w, bars * SEG_H)
            water.show()
        shown["bars"] = bars
    y = level_to_y(level)
    if y != surface.y:                                # ผิวน้ำ: ขยับตามระดับจริงทุก 1 px
        surface.move_to(surface.x, y)
    y = level_to_y(target)
    if y != target_line.y:                            # เส้นเป้า + ป้าย ย้ายเฉพาะตอนกด A / B
        target_line.move_to(target_line.x, y)
        target_tag.set("เป้า %d%%" % target)
        target_tag._label.pos(TANK_X + TANK_W + 14, y - 11)   # game.Text ไม่มี move_to จึงย้ายป้ายข้างในตรง ๆ
    # น้ำไหลในท่อ: ท่อเข้าไหลเฉพาะตอนวาล์วเปิด (เร็วตามองศาวาล์ว) · ท่อออกเร็วตาม OUT
    opened = valve > 0
    if opened != shown["open"]:
        for f in flow_in:
            if opened: f.show()
            else:      f.hide()
        shown["open"] = opened
    if opened:
        slide(flow_in, 1 + valve * 5, 26, TANK_X - 18)
    slide(drops, outflow * 12, TANK_X + TANK_W + 10, TANK_X + TANK_W + 152)
    # วาล์ว + การ์ดค่า
    if move != shown["move"]:
        valve_sym.set_color(VALVE_LOOK[move][0])
        valve_tag.set(VALVE_LOOK[move][1])
        shown["move"] = move
    show("level", cards["level"], "ระดับน้ำ  LEVEL\n%d %%" % int(level))
    show("target", cards["target"], "เป้าหมาย  TARGET\n%d %%" % target)
    show("out", cards["out"], "น้ำออก  OUT\n%d.%02d %% ต่อเฟรม" % (int(outflow), int(outflow * 100 + 0.5) % 100))
    show("valve", cards["valve"], "วาล์ว  VALVE\n%d %%   %s" % (int(valve * 100), VALVE_LOOK[move][1]))
    # สถานะ: น้ำต่ำ = แถบบนกลายเป็นป้ายเตือนสีแดง กะพริบพร้อมเสียงทุก ALARM_EVERY เฟรม
    if low != shown["low"]:
        hud_state.set("น้ำต่ำ  LOW WATER" if low else "ปกติ  NORMAL")
        tank.set_color(0x3A1216 if low else TANK_BG)
        if not low:
            bar.set_color(PANEL)
        shown["low"] = low
        alarm_count = 0
    if low:
        if alarm_count == 0:
            bar.set_color(0xC62828)
            game.sfx("deny")
        elif alarm_count == ALARM_EVERY // 2:
            bar.set_color(0x7A1C1C)
        alarm_count = (alarm_count + 1) % ALARM_EVERY
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) BAND = 3 -> 0 : วาล์วเปิด-ปิดสลับไม่หยุด (วงกลมวาล์วกะพริบเขียว-ส้ม การ์ด VALVE สลับ OPEN+ / CLOSE-)
# 2) VALVE_RATE = 0.02 -> 0.2 : วาล์วตอบสนองไวขึ้น แต่ระดับน้ำแกว่งเกินเส้นเป้ามากขึ้นไหม
# 3) OUT_MAX = 0.80 -> 0.50 : น้ำยังลดจนเตือนได้ไหม เมื่อน้ำออกสูงสุดน้อยกว่าที่วาล์วเติมทัน

เกมกับหน้าจอควบคุม ใช้หัวใจเดียวกัน

ในเกม ในหน้าจอควบคุม เทคนิคเดียวกัน (คาบ)
คะแนน จำนวนชิ้นงานที่ผลิตได้ ป้าย HUD อัปเดตเมื่อเปลี่ยน (4, 12)
ชีวิตที่เหลือ ของในคลังที่เหลือ · โควตาของเสีย ตัวนับ + เงื่อนไขจบ (12)
กระสุนชุดหมุนเวียน กล่องพัสดุบนสายพาน · รถในลานจอด ชุดหมุนเวียน (11)
ยิงโดนศัตรู กล่องผ่านประตูคัดแยก · รถข้ามเส้นหยุด ชนแบบกล่อง (4, 12)
GAME OVER สัญญาณเตือน หรือหยุดเครื่อง เครื่องสถานะ (2, 7)
ยานมีน้ำหนัก เร่งแล้วไหลต่อ สายพานเร่งแล้วค่อย ๆ หยุด เร่ง + แรงเสียดทาน (6, 10)
ลูกบอลเคลื่อนทีละเฟรม ระดับน้ำ = เดิม + น้ำเข้า − น้ำออก สะสมค่าทีละเฟรม (5)
ไม้ AI ขึ้น หยุด ลง มีช่องว่าง วาล์วเปิด หยุด ปิด มีช่องว่าง ตัวควบคุมสามระดับ (7)

สิ่งที่เปลี่ยนคือคนที่อยู่หน้าจอ — ผู้เล่น หรือ พนักงาน

ส่วนที่ 6 — ภาพจากเกมตัวเต็ม 20 แบบ

ชื่อภาพ เกม
ship · enemy enemy2 enemy3 · boom1–boom6 Shooter
ball · paddle Pong
bird · pipe_cap Flappy
snake_head_r _d _l _u · snake_body · snake_food Snake
boom = game.Sprite("boom1", 0, -50)    # สร้างครั้งเดียว จอดไว้นอกจอ
boom.move_to(x, y); boom.frame("boom2")  # ย้ายไปจุดที่โดน แล้วเปลี่ยนเฟรม
enemies = game.pool("enemy", 5)        # ชุดหมุนเวียนของภาพ (ซ่อนไว้ก่อน)

ภาพไม่รู้ขนาดตัวเอง ตั้ง s.w s.h ก่อนใช้ game.hit() · สร้างพื้นหลังก่อนเสมอ (ลำดับการวาด = ลำดับการสร้าง) · ดูการตั้ง .w .h ใน full_games/pong_full.py↗ · ดู pool + frame ใน full_games/shooter_full.py↗

ลองเล่น · ห้องแสดงภาพจากเกมตัวเต็มครบ 20 แบบ

คัดลอกไฟล์ s13_12_sprite_gallery.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยซ้าย/ขวา = เลื่อนดูภาพทีละแบบ (ภาพกลางกรอบเหลือง คือภาพที่เลือก)
  • boom1-boom6 จะระเบิดสลับ 2 เฟรม · snake_head_* จะหันหัวครบ 4 ทิศ
  • กด A = ฟังเสียงที่คู่กับภาพนั้นในเกมของมัน

ลองแก้ แล้วรันใหม่

  1. ANIM_EVERY = 8 -> 2 : ระเบิดกับหัวงูสลับเฟรมรัวจนดูสั่น · -> 20 : ช้าจนไม่เหมือนระเบิด ลองหาค่าที่ "ใช่"
  2. แก้ ("ship", "Shooter", 62, 28, ...) เป็นกว้าง 20 : ยานไม่อยู่กลางกรอบอีกต่อไป เพราะการจัดกลางเชื่อตัวเลขในตาราง ไม่ได้วัดจากภาพจริง
  3. เปลี่ยนเสียงของ "snake_body" จาก "die" เป็น "eat" : ฟังแล้วความหมายเปลี่ยนไหม (หนึ่งเสียง หนึ่งความหมาย)

โค้ดเต็ม · s13_12_sprite_gallery.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗

ลองเล่น · ชุดตัวละครสำเร็จรูป ยกมาจากเกมเต็มของจริง หยิบไปใส่เกมของน้องได้ทันที

คัดลอกไฟล์ s13_16_kit_characters.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · ซ้าย/ขวา = เปลี่ยนตัวละคร (โชว์ทีละตัว) · A = ท่าประจำตัว
  • ยาน: บินซ้าย-ขวาเอง (จำลองกดค้าง/ปล่อย) UP/DOWN ขับเอง A ยิง ดูกระสุนพุ่งเฉียงตามแรงยาน
  • ศัตรู: A = ระเบิดตัวล่างสุด ดูสีระเบิดตามชนิด · นก: A/UP กระพือ ดูช่วง 4 วินาทีที่ตกพื้นไม่ตาย
  • งู: A เลี้ยวตามเข็มนาฬิกา UP/DOWN เลี้ยวแบบเกมจริง ดูหัวหันตามทิศ · ไม้: UP/DOWN ขยับ A เสิร์ฟ

ลองแก้ แล้วรันใหม่

  1. FRICTION = 0.80 -> 0.97 : ยานไถลยาวเหมือนบนน้ำแข็ง กว่าจะหยุดต้องรอนาน (0.5 = หยุดแทบทันที ไม่มีน้ำหนัก)
  2. BULLET_INHERIT = 0.55 -> 0 : กระสุนพุ่งตรงขึ้นเสมอแม้ยานกำลังไถล · ลอง 1.5 แล้วกระสุนจะเฉียงแรงจนหลุดข้างจอ
  3. GRACE = 120 -> 0 : นกตกพื้นตายทันทีตั้งแต่กระพือครั้งแรก — เห็นเลยว่าช่วงออกตัวช่วยผู้เล่นใหม่แค่ไหน

หยิบไปใช้ · ยานมีน้ำหนัก + กระสุนติดแรงยาน

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/shooter_full.py:15-20, 71-74, 98-113, 137-148, 178-214 · ไฟล์ตัวอย่าง s13_16_kit_characters.py↗
# ===== หยิบไปใช้: ยานมีน้ำหนัก + กระสุนติดแรงยาน (จาก full_games/shooter_full.py:15-20, 71-74, 98-113, 137-148, 178-214) =====
SHIP_W, SHIP_H = 62, 28
ACCEL, MAXV, FRICTION = 1.4, 13.0, 0.80      # ฟิสิกส์ยานชุดเดียวกับเกม C เป๊ะ
BULLET_VY, BULLET_INHERIT = 8.0, 0.55        # กระสุนพุ่งขึ้น + พกแรงแนวนอนของยานไป 55%
LASER = 0x80F4FF

def make_ship(x, y, bullets=4):
    s = {"spr": game.Sprite("ship", x, y), "x": float(x), "y": float(y), "vx": 0.0, "vy": 0.0,
         "b": [], "on": [False] * bullets, "bx": [0.0] * bullets, "by": [0.0] * bullets, "bvx": [0.0] * bullets}
    for _ in range(bullets):                 # กระสุนแท่งฟ้าขอบขาวมุมมน สร้างครั้งเดียวแล้วซ่อน
        b = game.Box(-20, -50, 5, 12, LASER, border=game.WHITE, radius=2)
        b.hide()
        s["b"].append(b)
    return s

def axis(v, minus, plus):
    # กดค้าง = เร่งทีละ ACCEL · ปล่อย = ไถลด้วยแรงเสียดทาน · แล้วตัดที่เพดานความเร็ว
    if minus and not plus:
        v -= ACCEL
    elif plus and not minus:
        v += ACCEL
    else:
        v *= FRICTION
    return max(-MAXV, min(MAXV, v))

def ship_step(s, left, right, up, down, y_min=24, y_max=game.HEIGHT - SHIP_H - 32):
    s["vx"], s["vy"] = axis(s["vx"], left, right), axis(s["vy"], up, down)
    x, y = s["x"] + s["vx"], s["y"] + s["vy"]
    if x < 0 or x > game.WIDTH - SHIP_W:     # ชนขอบ = หยุดความเร็วแกนนั้น ยานจะได้ไม่อัดขอบค้าง
        x, s["vx"] = max(0.0, min(float(game.WIDTH - SHIP_W), x)), 0.0
    if y < y_min or y > y_max:
        y, s["vy"] = max(float(y_min), min(float(y_max), y)), 0.0
    s["x"], s["y"] = x, y
    s["spr"].move_to(x, y)
    for j in range(len(s["b"])):             # กระสุน: ขึ้น + เฉียงตามแรงที่ติดมา หลุดจอเก็บคืนพูล
        if s["on"][j]:
            s["by"][j] -= BULLET_VY
            s["bx"][j] += s["bvx"][j]
            if s["by"][j] < -12 or s["bx"][j] < -12 or s["bx"][j] > game.WIDTH:
                s["on"][j] = False
                s["b"][j].hide()
            else:
                s["b"][j].move_to(s["bx"][j], s["by"][j])

def ship_fire(s):
    for j in range(len(s["b"])):             # หานัดที่ว่างในพูล ไม่มีก็ยิงไม่ออก (ไม่สร้างนัดใหม่)
        if not s["on"][j]:
            s["on"][j] = True
            game.sfx("fire")
            s["bx"][j], s["by"][j] = s["x"] + SHIP_W / 2.0 - 2.0, s["y"] - 8.0
            s["bvx"][j] = s["vx"] * BULLET_INHERIT
            s["b"][j].move_to(s["bx"][j], s["by"][j])
            s["b"][j].show()
            return
# ===== จบชิ้น =====

หยิบไปใช้ · ศัตรู 3 แบบ + ระเบิด 2 เฟรมสีตามชนิด

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/shooter_full.py:21, 42-45, 75-76, 123-135, 150-170 · ไฟล์ตัวอย่าง s13_16_kit_characters.py↗
# ===== หยิบไปใช้: ศัตรู 3 แบบ + ระเบิด 2 เฟรมสีตามชนิด (จาก full_games/shooter_full.py:21, 42-45, 75-76, 123-135, 150-170) =====
ENEMY_SPR = ("enemy", "enemy2", "enemy3")                  # invader ม่วง / UFO เงิน / demon แดง
BOOM_FRAMES = (("boom1", "boom2"), ("boom3", "boom4"), ("boom5", "boom6"))
BOOM_TICKS = 6                                             # ค้างกี่เฟรม ครึ่งหลังสลับเป็นวงใหญ่

def make_enemy(kind, x, y):
    e = game.Sprite("enemy", x, y)
    e.frame(ENEMY_SPR[kind])          # เกมเต็มสร้างพูลเป็น "enemy" แล้วเปลี่ยนหน้าตาตอนเกิด
    e.w, e.h, e.kind = 26, 16, kind   # กล่องชนที่เกมเต็มใช้ (สไปรต์ไม่รู้ขนาดตัวเอง)
    return e

def make_booms(n=3):
    return {"spr": game.pool("boom1", n), "t": [0] * n, "k": [0] * n}

def boom(bm, cx, cy, kind):
    game.sfx("explode")
    for b in range(len(bm["t"])):
        if bm["t"][b] == 0:
            bm["t"][b], bm["k"][b] = BOOM_TICKS, kind
            bm["spr"][b].frame(BOOM_FRAMES[kind][0])
            bm["spr"][b].move_to(cx - 15, cy - 12)    # จัดกึ่งกลางภาพระเบิด 29x24
            bm["spr"][b].show()
            return

def booms_step(bm):                                    # เรียกทุกเฟรม
    for b in range(len(bm["t"])):
        if bm["t"][b] == 0:
            continue
        bm["t"][b] -= 1
        if bm["t"][b] == 0:
            bm["spr"][b].hide()
        elif bm["t"][b] == BOOM_TICKS // 2:            # ครึ่งหลังสลับเป็นเฟรมบึ้มวงใหญ่
            bm["spr"][b].frame(BOOM_FRAMES[bm["k"][b]][1])
# ===== จบชิ้น =====

หยิบไปใช้ · นกเงือก แรงโน้มถ่วง + กระพือ + ออกตัว 4 วินาทีตกไม่ตาย

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/flappy_full.py:17-19, 29, 145-200 · ไฟล์ตัวอย่าง s13_16_kit_characters.py↗
# ===== หยิบไปใช้: นกเงือก แรงโน้มถ่วง + กระพือ + ออกตัว 4 วินาทีตกไม่ตาย (จาก full_games/flappy_full.py:17-19, 29, 145-200) =====
BIRD_W, BIRD_H, BIRD_BOX = 44, 29, 36        # ภาพนก 44x29 · กล่องชน 36 (ใจดีกว่าภาพนิดหน่อย)
GRAVITY, FLAP_VEL = 1050.0, -340.0           # โหมด NORMAL ของเกม C: px/s^2 และ px/s
GRACE = 120                                  # 4 วินาทีแรก ที่ 30 fps (เกมเต็มจับเวลาเป็น ms ที่นี่นับเฟรม)

def make_bird(x, play_h):
    b = {"spr": game.Sprite("bird", 0, 0), "x": x, "play_h": play_h}
    bird_reset(b)
    return b

def bird_reset(b):                            # นกลอยรอกลางจอ จนกว่าจะกระพือครั้งแรก
    b["y"], b["vy"], b["started"], b["grace"] = float(b["play_h"] // 2), 0.0, False, 0
    b["spr"].move_to(b["x"] - BIRD_W // 2, b["play_h"] // 2 - BIRD_H // 2)

def bird_step(b, flap, dt):                   # คืน False เมื่อนกตกพื้นหลังหมดช่วง grace
    if flap:
        b["vy"] = FLAP_VEL
        game.sfx("flap")
    if not b["started"]:                      # ยังไม่ออกตัว: ไม่มีแรงโน้มถ่วง ไม่ตก
        if not flap:
            return True
        b["started"], b["grace"] = True, GRACE
    b["grace"] = max(0, b["grace"] - 1)
    b["vy"] += GRAVITY * dt
    b["y"] += b["vy"] * dt
    half = BIRD_BOX / 2
    if b["y"] < half:                         # ชนเพดาน: ค้างไว้ ไม่ตาย
        b["y"], b["vy"] = half, 0.0
    if b["y"] > b["play_h"] - half:           # ถึงพื้น
        if b["grace"] == 0:
            game.sfx("fall")
            return False
        b["y"], b["vy"] = b["play_h"] - half, 0.0   # ช่วง grace: หยุดที่พื้นเฉย ๆ
    b["spr"].move_to(b["x"] - BIRD_W // 2, int(b["y"]) - BIRD_H // 2)
    return True
# ===== จบชิ้น =====

หยิบไปใช้ · งูสไปรต์ หัวหันตามทิศ

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/snake_sprite_full.py:14-15, 38-39, 61-64, 78-95, 109-138 · ไฟล์ตัวอย่าง s13_16_kit_characters.py↗
# ===== หยิบไปใช้: งูสไปรต์ หัวหันตามทิศ (จาก full_games/snake_sprite_full.py:14-15, 38-39, 61-64, 78-95, 109-138) =====
# (เกมของน้องต้องมี import random ด้วย)
CELL = 16
COLS, ROWS = game.WIDTH // CELL, game.HEIGHT // CELL          # 49 x 24 ช่อง
HEAD_BY_DIR = {(1, 0): "snake_head_r", (-1, 0): "snake_head_l",
               (0, -1): "snake_head_u", (0, 1): "snake_head_d"}

def make_snake(col, row, length=5, pool=8):
    sn = {"body": [[col - i, row] for i in range(length)], "dx": 1, "dy": 0, "shown": 0,
          "seg": game.pool("snake_body", pool), "head": game.Sprite("snake_head_r", 0, 0),
          "food": game.Sprite("snake_food", 0, 0), "fc": [0, 0]}
    place_food(sn)
    snake_draw(sn)
    return sn

def snake_turn(sn, up, down, left, right):    # เลี้ยวได้ทีละ 90 องศา ห้ามกลับหลังหัน
    if up and sn["dy"] == 0:      sn["dx"], sn["dy"] = 0, -1
    elif down and sn["dy"] == 0:  sn["dx"], sn["dy"] = 0, 1
    elif left and sn["dx"] == 0:  sn["dx"], sn["dy"] = -1, 0
    elif right and sn["dx"] == 0: sn["dx"], sn["dy"] = 1, 0

def place_food(sn):                           # สุ่มจนได้ช่องที่ไม่ทับตัวงู
    while True:
        c = [random.randint(0, COLS - 1), random.randint(0, ROWS - 1)]
        if c not in sn["body"]:
            sn["fc"] = c
            sn["food"].move_to(c[0] * CELL, c[1] * CELL)
            return

def snake_draw(sn):                           # ต่างจากเกมเต็มนิดเดียว: โชว์/ซ่อนเฉพาะท่อนที่เปลี่ยน ประหยัดข้อความวาดจอ
    name = HEAD_BY_DIR[(sn["dx"], sn["dy"])]
    if name != sn["head"].name:               # เปลี่ยนภาพหัวเฉพาะตอนทิศเปลี่ยน
        sn["head"].frame(name)
    sn["head"].move_to(sn["body"][0][0] * CELL, sn["body"][0][1] * CELL)
    n = min(len(sn["body"]) - 1, len(sn["seg"]))
    for i in range(len(sn["seg"])):           # body[1:] -> สไปรต์ในพูล ที่เหลือซ่อน
        if i < n:
            sn["seg"][i].move_to(sn["body"][i + 1][0] * CELL, sn["body"][i + 1][1] * CELL)
            if i >= sn["shown"]:
                sn["seg"][i].show()
        elif i < sn["shown"]:
            sn["seg"][i].hide()
    sn["shown"] = n

def snake_step(sn):                           # ก้าว 1 ช่อง คืน "wall" / "self" / "eat" / ""
    nh = [sn["body"][0][0] + sn["dx"], sn["body"][0][1] + sn["dy"]]
    if not (0 <= nh[0] < COLS and 0 <= nh[1] < ROWS):
        game.sfx("die")                       # ชนกำแพง
        return "wall"
    if nh in sn["body"]:
        game.sfx("lose")                      # กัดตัวเอง คนละเสียงกับชนกำแพง
        return "self"
    sn["body"].insert(0, nh)
    ate = nh == sn["fc"]
    if ate:
        game.sfx("eat")
        place_food(sn)
    if not ate or len(sn["body"]) > len(sn["seg"]) + 1:     # ไม่ได้กิน หรือพูลเต็ม = ไม่โต
        sn["body"].pop()
    snake_draw(sn)
    return "eat" if ate else ""
# ===== จบชิ้น =====

หยิบไปใช้ · ไม้ปิงปองมีน้ำหนัก + ตีแล้วใส่สปิน

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/pong_full.py:18-27, 36-53, 82-125, 179-185 · ไฟล์ตัวอย่าง s13_16_kit_characters.py↗
# ===== หยิบไปใช้: ไม้ปิงปองมีน้ำหนัก + ตีแล้วใส่สปิน (จาก full_games/pong_full.py:18-27, 36-53, 82-125, 179-185) =====
# (เกมของน้องต้องมี import random ด้วย)
PADDLE_W, PADDLE_H, PADDLE_X = 14, 90, 14
BALL_SIZE, BALL_SPEED0, BALL_MAXV, BALL_SPEEDUP = 14, 6.2, 14.0, 0.35
PADDLE_ACCEL, PADDLE_MAXV, PADDLE_FRICTION, PADDLE_SPIN = 1.5, 13.0, 0.78, 0.28

def make_pong():
    y0 = game.HEIGHT // 2 - PADDLE_H // 2
    p = {"pad": game.Sprite("paddle", PADDLE_X, y0), "ball": game.Sprite("ball", 0, 0),
         "py": float(y0), "pvy": 0.0, "spin": 0.0}
    p["pad"].w, p["pad"].h = PADDLE_W, PADDLE_H          # บอกขนาดให้ game.hit() ใช้
    p["ball"].w = p["ball"].h = BALL_SIZE
    pong_serve(p, False)
    return p

def pong_serve(p, to_right):                  # วางลูกกลางโต๊ะ เสิร์ฟมุมสุ่มเล็กน้อย
    p["bx"], p["by"] = game.WIDTH / 2 - BALL_SIZE / 2, game.HEIGHT / 2 - BALL_SIZE / 2
    p["bvx"] = BALL_SPEED0 if to_right else -BALL_SPEED0
    p["bvy"] = random.randint(-22, 22) / 10.0

def paddle_step(p, up, down):
    if up and not down:
        p["pvy"] -= PADDLE_ACCEL
    elif down and not up:
        p["pvy"] += PADDLE_ACCEL
    else:
        p["pvy"] *= PADDLE_FRICTION
    p["pvy"] = max(-PADDLE_MAXV, min(PADDLE_MAXV, p["pvy"]))
    p["py"] += p["pvy"]
    if p["py"] <= 0 or p["py"] >= game.HEIGHT - PADDLE_H:   # ชนขอบแล้วหยุดนิ่ง ไม่เด้ง
        p["py"], p["pvy"] = max(0.0, min(float(game.HEIGHT - PADDLE_H), p["py"])), 0.0
    p["pad"].move_to(PADDLE_X, p["py"])

def ball_step(p):                             # คืน True เมื่อตีโดนไม้เฟรมนี้
    p["bx"] += p["bvx"]
    p["by"] += p["bvy"]
    if p["by"] <= 0 or p["by"] >= game.HEIGHT - BALL_SIZE:  # เด้งขอบบน/ล่าง
        p["by"] = max(0.0, min(float(game.HEIGHT - BALL_SIZE), p["by"]))
        p["bvy"] = -p["bvy"]
        game.sfx("wall")
    p["ball"].x, p["ball"].y = p["bx"], p["by"]      # ให้กล่องชนรู้ตำแหน่งลูกของเฟรมนี้ก่อนเช็ค
    hit = p["bvx"] < 0 and game.hit(p["ball"], p["pad"])
    if hit:                                   # สะท้อน + เร่ง + มุมตามจุดที่โดน + สปินจากไม้ที่กำลังวิ่ง
        norm = ((p["by"] + BALL_SIZE / 2) - (p["py"] + PADDLE_H / 2)) / (PADDLE_H / 2)
        p["bx"] = float(PADDLE_X + PADDLE_W)
        p["bvx"] = abs(p["bvx"]) + BALL_SPEEDUP
        p["spin"] = p["pvy"] * PADDLE_SPIN
        p["bvy"] += norm * 2.0 + p["spin"]
        speed = (p["bvx"] ** 2 + p["bvy"] ** 2) ** 0.5
        if speed > BALL_MAXV:                 # จำกัดความเร็ว "รวม" ไม่ให้ทะลุเพดาน
            p["bvx"], p["bvy"] = p["bvx"] * BALL_MAXV / speed, p["bvy"] * BALL_MAXV / speed
        game.sfx("paddle")
    p["ball"].move_to(p["bx"], p["by"])       # วาดหลังเคลียร์เรื่องชนแล้ว ลูกจะไม่จมในไม้
    return hit
# ===== จบชิ้น =====

โค้ดเต็ม · s13_16_kit_characters.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_16_kit_characters.py — คาบ 13 (คาบพิเศษ): ชุดตัวละครสำเร็จรูป ยกมาจากเกมเต็มของจริง หยิบไปใส่เกมของน้องได้ทันที
#
# หลักการ   : ตัวละคร 1 ตัว = ภาพ (Sprite) + สถานะ (dict) + ฟังก์ชันก้าวทีละเฟรม          (คาบ 4, 11)
#             ยานมีน้ำหนัก  กดค้าง v = v + a · ปล่อย v = v * f · มีเพดานความเร็ว          (คาบ 6, 10)
#             นกเงือก      v = v + g*dt · y = y + v*dt · กระพือ = ตั้ง v ใหม่ทันที         (คาบ 5, 6)
#             ไม้ปิงปอง    vy ลูก += 2*จุดที่โดน + vy ไม้ * 0.28 (สปิน)                     (คาบ 7)
#             หัวงูเปลี่ยนภาพตามทิศด้วย .frame() · ระเบิด 2 เฟรมด้วยตัวนับถอยหลัง          (คาบ 4, 12)
# ลองเล่น   : กด Start · ซ้าย/ขวา = เปลี่ยนตัวละคร (โชว์ทีละตัว) · A = ท่าประจำตัว
#             ยาน: บินซ้าย-ขวาเอง (จำลองกดค้าง/ปล่อย) UP/DOWN ขับเอง A ยิง ดูกระสุนพุ่งเฉียงตามแรงยาน
#             ศัตรู: A = ระเบิดตัวล่างสุด ดูสีระเบิดตามชนิด · นก: A/UP กระพือ ดูช่วง 4 วินาทีที่ตกพื้นไม่ตาย
#             งู: A เลี้ยวตามเข็มนาฬิกา UP/DOWN เลี้ยวแบบเกมจริง ดูหัวหันตามทิศ · ไม้: UP/DOWN ขยับ A เสิร์ฟ
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : shooter_full · flappy_full · snake_sprite_full · pong_full ใช้ตัวละครชุดนี้ทั้งตัว
# ในงานจริง : ยาน = โดรนพ่นยา/รถ AGV ที่เร่งแล้วไหลต่อ · ระเบิด = ชิ้นงานถูกคัดทิ้ง · ไม้ = แขนกลคัดแยก
#             ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : full_games/shooter_full.py · flappy_full.py · snake_sprite_full.py · pong_full.py
# งบวิดเจ็ต : 25 ชิ้น สร้างครบทุกตัวครั้งเดียว โชว์ทีละตัว ตัวอื่นซ่อนไว้ (ไม่สร้างไม่ลบระหว่างเล่น)
#             พื้นหลัง 1 + ยาน 1 กระสุน 3 + ศัตรู 3 ระเบิด 3 + นก 1 พื้นหญ้า 1 + งู 4+หัว 1+อาหาร 1
#             + ไม้ 1 ลูก 1 + ป้าย 3 + game.run 1 (โชว์ใช้กระสุน 3 นัด พูลงู 4 ท่อน ให้พองบ 26)
#
# วิธีหยิบไปใช้: คัดลอกทั้งกล่อง "หยิบไปใช้ ... จบชิ้น" ไปวางในเกมของน้อง (บนสุด ใต้ import)
#   สร้างตัวละครครั้งเดียวก่อน game.run() แล้วเรียกฟังก์ชันก้าวใน on_frame ทุกเฟรม — ตัวอย่างอยู่ในส่วนที่ 4
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30
DT = 1.0 / FPS          # flappy_full วัด dt จริงด้วย time.ticks_ms · ชุดนี้ใช้ dt คงที่ จะได้ import แค่ bentogame
SPACE = 0x10122C        # สีอวกาศ (shooter_full.py:67)

# ---- 2) สมอง + ชิ้นสำเร็จรูป (หยิบไปใช้ได้ทีละกล่อง) ----
# ===== หยิบไปใช้: ยานมีน้ำหนัก + กระสุนติดแรงยาน (จาก full_games/shooter_full.py:15-20, 71-74, 98-113, 137-148, 178-214) =====
SHIP_W, SHIP_H = 62, 28
ACCEL, MAXV, FRICTION = 1.4, 13.0, 0.80      # ฟิสิกส์ยานชุดเดียวกับเกม C เป๊ะ
BULLET_VY, BULLET_INHERIT = 8.0, 0.55        # กระสุนพุ่งขึ้น + พกแรงแนวนอนของยานไป 55%
LASER = 0x80F4FF

def make_ship(x, y, bullets=4):
    s = {"spr": game.Sprite("ship", x, y), "x": float(x), "y": float(y), "vx": 0.0, "vy": 0.0,
         "b": [], "on": [False] * bullets, "bx": [0.0] * bullets, "by": [0.0] * bullets, "bvx": [0.0] * bullets}
    for _ in range(bullets):                 # กระสุนแท่งฟ้าขอบขาวมุมมน สร้างครั้งเดียวแล้วซ่อน
        b = game.Box(-20, -50, 5, 12, LASER, border=game.WHITE, radius=2)
        b.hide()
        s["b"].append(b)
    return s

def axis(v, minus, plus):
    # กดค้าง = เร่งทีละ ACCEL · ปล่อย = ไถลด้วยแรงเสียดทาน · แล้วตัดที่เพดานความเร็ว
    if minus and not plus:
        v -= ACCEL
    elif plus and not minus:
        v += ACCEL
    else:
        v *= FRICTION
    return max(-MAXV, min(MAXV, v))

def ship_step(s, left, right, up, down, y_min=24, y_max=game.HEIGHT - SHIP_H - 32):
    s["vx"], s["vy"] = axis(s["vx"], left, right), axis(s["vy"], up, down)
    x, y = s["x"] + s["vx"], s["y"] + s["vy"]
    if x < 0 or x > game.WIDTH - SHIP_W:     # ชนขอบ = หยุดความเร็วแกนนั้น ยานจะได้ไม่อัดขอบค้าง
        x, s["vx"] = max(0.0, min(float(game.WIDTH - SHIP_W), x)), 0.0
    if y < y_min or y > y_max:
        y, s["vy"] = max(float(y_min), min(float(y_max), y)), 0.0
    s["x"], s["y"] = x, y
    s["spr"].move_to(x, y)
    for j in range(len(s["b"])):             # กระสุน: ขึ้น + เฉียงตามแรงที่ติดมา หลุดจอเก็บคืนพูล
        if s["on"][j]:
            s["by"][j] -= BULLET_VY
            s["bx"][j] += s["bvx"][j]
            if s["by"][j] < -12 or s["bx"][j] < -12 or s["bx"][j] > game.WIDTH:
                s["on"][j] = False
                s["b"][j].hide()
            else:
                s["b"][j].move_to(s["bx"][j], s["by"][j])

def ship_fire(s):
    for j in range(len(s["b"])):             # หานัดที่ว่างในพูล ไม่มีก็ยิงไม่ออก (ไม่สร้างนัดใหม่)
        if not s["on"][j]:
            s["on"][j] = True
            game.sfx("fire")
            s["bx"][j], s["by"][j] = s["x"] + SHIP_W / 2.0 - 2.0, s["y"] - 8.0
            s["bvx"][j] = s["vx"] * BULLET_INHERIT
            s["b"][j].move_to(s["bx"][j], s["by"][j])
            s["b"][j].show()
            return
# ===== จบชิ้น =====

# ===== หยิบไปใช้: ศัตรู 3 แบบ + ระเบิด 2 เฟรมสีตามชนิด (จาก full_games/shooter_full.py:21, 42-45, 75-76, 123-135, 150-170) =====
ENEMY_SPR = ("enemy", "enemy2", "enemy3")                  # invader ม่วง / UFO เงิน / demon แดง
BOOM_FRAMES = (("boom1", "boom2"), ("boom3", "boom4"), ("boom5", "boom6"))
BOOM_TICKS = 6                                             # ค้างกี่เฟรม ครึ่งหลังสลับเป็นวงใหญ่

def make_enemy(kind, x, y):
    e = game.Sprite("enemy", x, y)
    e.frame(ENEMY_SPR[kind])          # เกมเต็มสร้างพูลเป็น "enemy" แล้วเปลี่ยนหน้าตาตอนเกิด
    e.w, e.h, e.kind = 26, 16, kind   # กล่องชนที่เกมเต็มใช้ (สไปรต์ไม่รู้ขนาดตัวเอง)
    return e

def make_booms(n=3):
    return {"spr": game.pool("boom1", n), "t": [0] * n, "k": [0] * n}

def boom(bm, cx, cy, kind):
    game.sfx("explode")
    for b in range(len(bm["t"])):
        if bm["t"][b] == 0:
            bm["t"][b], bm["k"][b] = BOOM_TICKS, kind
            bm["spr"][b].frame(BOOM_FRAMES[kind][0])
            bm["spr"][b].move_to(cx - 15, cy - 12)    # จัดกึ่งกลางภาพระเบิด 29x24
            bm["spr"][b].show()
            return

def booms_step(bm):                                    # เรียกทุกเฟรม
    for b in range(len(bm["t"])):
        if bm["t"][b] == 0:
            continue
        bm["t"][b] -= 1
        if bm["t"][b] == 0:
            bm["spr"][b].hide()
        elif bm["t"][b] == BOOM_TICKS // 2:            # ครึ่งหลังสลับเป็นเฟรมบึ้มวงใหญ่
            bm["spr"][b].frame(BOOM_FRAMES[bm["k"][b]][1])
# ===== จบชิ้น =====

# ===== หยิบไปใช้: นกเงือก แรงโน้มถ่วง + กระพือ + ออกตัว 4 วินาทีตกไม่ตาย (จาก full_games/flappy_full.py:17-19, 29, 145-200) =====
BIRD_W, BIRD_H, BIRD_BOX = 44, 29, 36        # ภาพนก 44x29 · กล่องชน 36 (ใจดีกว่าภาพนิดหน่อย)
GRAVITY, FLAP_VEL = 1050.0, -340.0           # โหมด NORMAL ของเกม C: px/s^2 และ px/s
GRACE = 120                                  # 4 วินาทีแรก ที่ 30 fps (เกมเต็มจับเวลาเป็น ms ที่นี่นับเฟรม)

def make_bird(x, play_h):
    b = {"spr": game.Sprite("bird", 0, 0), "x": x, "play_h": play_h}
    bird_reset(b)
    return b

def bird_reset(b):                            # นกลอยรอกลางจอ จนกว่าจะกระพือครั้งแรก
    b["y"], b["vy"], b["started"], b["grace"] = float(b["play_h"] // 2), 0.0, False, 0
    b["spr"].move_to(b["x"] - BIRD_W // 2, b["play_h"] // 2 - BIRD_H // 2)

def bird_step(b, flap, dt):                   # คืน False เมื่อนกตกพื้นหลังหมดช่วง grace
    if flap:
        b["vy"] = FLAP_VEL
        game.sfx("flap")
    if not b["started"]:                      # ยังไม่ออกตัว: ไม่มีแรงโน้มถ่วง ไม่ตก
        if not flap:
            return True
        b["started"], b["grace"] = True, GRACE
    b["grace"] = max(0, b["grace"] - 1)
    b["vy"] += GRAVITY * dt
    b["y"] += b["vy"] * dt
    half = BIRD_BOX / 2
    if b["y"] < half:                         # ชนเพดาน: ค้างไว้ ไม่ตาย
        b["y"], b["vy"] = half, 0.0
    if b["y"] > b["play_h"] - half:           # ถึงพื้น
        if b["grace"] == 0:
            game.sfx("fall")
            return False
        b["y"], b["vy"] = b["play_h"] - half, 0.0   # ช่วง grace: หยุดที่พื้นเฉย ๆ
    b["spr"].move_to(b["x"] - BIRD_W // 2, int(b["y"]) - BIRD_H // 2)
    return True
# ===== จบชิ้น =====

# ===== หยิบไปใช้: งูสไปรต์ หัวหันตามทิศ (จาก full_games/snake_sprite_full.py:14-15, 38-39, 61-64, 78-95, 109-138) =====
# (เกมของน้องต้องมี import random ด้วย)
CELL = 16
COLS, ROWS = game.WIDTH // CELL, game.HEIGHT // CELL          # 49 x 24 ช่อง
HEAD_BY_DIR = {(1, 0): "snake_head_r", (-1, 0): "snake_head_l",
               (0, -1): "snake_head_u", (0, 1): "snake_head_d"}

def make_snake(col, row, length=5, pool=8):
    sn = {"body": [[col - i, row] for i in range(length)], "dx": 1, "dy": 0, "shown": 0,
          "seg": game.pool("snake_body", pool), "head": game.Sprite("snake_head_r", 0, 0),
          "food": game.Sprite("snake_food", 0, 0), "fc": [0, 0]}
    place_food(sn)
    snake_draw(sn)
    return sn

def snake_turn(sn, up, down, left, right):    # เลี้ยวได้ทีละ 90 องศา ห้ามกลับหลังหัน
    if up and sn["dy"] == 0:      sn["dx"], sn["dy"] = 0, -1
    elif down and sn["dy"] == 0:  sn["dx"], sn["dy"] = 0, 1
    elif left and sn["dx"] == 0:  sn["dx"], sn["dy"] = -1, 0
    elif right and sn["dx"] == 0: sn["dx"], sn["dy"] = 1, 0

def place_food(sn):                           # สุ่มจนได้ช่องที่ไม่ทับตัวงู
    while True:
        c = [random.randint(0, COLS - 1), random.randint(0, ROWS - 1)]
        if c not in sn["body"]:
            sn["fc"] = c
            sn["food"].move_to(c[0] * CELL, c[1] * CELL)
            return

def snake_draw(sn):                           # ต่างจากเกมเต็มนิดเดียว: โชว์/ซ่อนเฉพาะท่อนที่เปลี่ยน ประหยัดข้อความวาดจอ
    name = HEAD_BY_DIR[(sn["dx"], sn["dy"])]
    if name != sn["head"].name:               # เปลี่ยนภาพหัวเฉพาะตอนทิศเปลี่ยน
        sn["head"].frame(name)
    sn["head"].move_to(sn["body"][0][0] * CELL, sn["body"][0][1] * CELL)
    n = min(len(sn["body"]) - 1, len(sn["seg"]))
    for i in range(len(sn["seg"])):           # body[1:] -> สไปรต์ในพูล ที่เหลือซ่อน
        if i < n:
            sn["seg"][i].move_to(sn["body"][i + 1][0] * CELL, sn["body"][i + 1][1] * CELL)
            if i >= sn["shown"]:
                sn["seg"][i].show()
        elif i < sn["shown"]:
            sn["seg"][i].hide()
    sn["shown"] = n

def snake_step(sn):                           # ก้าว 1 ช่อง คืน "wall" / "self" / "eat" / ""
    nh = [sn["body"][0][0] + sn["dx"], sn["body"][0][1] + sn["dy"]]
    if not (0 <= nh[0] < COLS and 0 <= nh[1] < ROWS):
        game.sfx("die")                       # ชนกำแพง
        return "wall"
    if nh in sn["body"]:
        game.sfx("lose")                      # กัดตัวเอง คนละเสียงกับชนกำแพง
        return "self"
    sn["body"].insert(0, nh)
    ate = nh == sn["fc"]
    if ate:
        game.sfx("eat")
        place_food(sn)
    if not ate or len(sn["body"]) > len(sn["seg"]) + 1:     # ไม่ได้กิน หรือพูลเต็ม = ไม่โต
        sn["body"].pop()
    snake_draw(sn)
    return "eat" if ate else ""
# ===== จบชิ้น =====

# ===== หยิบไปใช้: ไม้ปิงปองมีน้ำหนัก + ตีแล้วใส่สปิน (จาก full_games/pong_full.py:18-27, 36-53, 82-125, 179-185) =====
# (เกมของน้องต้องมี import random ด้วย)
PADDLE_W, PADDLE_H, PADDLE_X = 14, 90, 14
BALL_SIZE, BALL_SPEED0, BALL_MAXV, BALL_SPEEDUP = 14, 6.2, 14.0, 0.35
PADDLE_ACCEL, PADDLE_MAXV, PADDLE_FRICTION, PADDLE_SPIN = 1.5, 13.0, 0.78, 0.28

def make_pong():
    y0 = game.HEIGHT // 2 - PADDLE_H // 2
    p = {"pad": game.Sprite("paddle", PADDLE_X, y0), "ball": game.Sprite("ball", 0, 0),
         "py": float(y0), "pvy": 0.0, "spin": 0.0}
    p["pad"].w, p["pad"].h = PADDLE_W, PADDLE_H          # บอกขนาดให้ game.hit() ใช้
    p["ball"].w = p["ball"].h = BALL_SIZE
    pong_serve(p, False)
    return p

def pong_serve(p, to_right):                  # วางลูกกลางโต๊ะ เสิร์ฟมุมสุ่มเล็กน้อย
    p["bx"], p["by"] = game.WIDTH / 2 - BALL_SIZE / 2, game.HEIGHT / 2 - BALL_SIZE / 2
    p["bvx"] = BALL_SPEED0 if to_right else -BALL_SPEED0
    p["bvy"] = random.randint(-22, 22) / 10.0

def paddle_step(p, up, down):
    if up and not down:
        p["pvy"] -= PADDLE_ACCEL
    elif down and not up:
        p["pvy"] += PADDLE_ACCEL
    else:
        p["pvy"] *= PADDLE_FRICTION
    p["pvy"] = max(-PADDLE_MAXV, min(PADDLE_MAXV, p["pvy"]))
    p["py"] += p["pvy"]
    if p["py"] <= 0 or p["py"] >= game.HEIGHT - PADDLE_H:   # ชนขอบแล้วหยุดนิ่ง ไม่เด้ง
        p["py"], p["pvy"] = max(0.0, min(float(game.HEIGHT - PADDLE_H), p["py"])), 0.0
    p["pad"].move_to(PADDLE_X, p["py"])

def ball_step(p):                             # คืน True เมื่อตีโดนไม้เฟรมนี้
    p["bx"] += p["bvx"]
    p["by"] += p["bvy"]
    if p["by"] <= 0 or p["by"] >= game.HEIGHT - BALL_SIZE:  # เด้งขอบบน/ล่าง
        p["by"] = max(0.0, min(float(game.HEIGHT - BALL_SIZE), p["by"]))
        p["bvy"] = -p["bvy"]
        game.sfx("wall")
    p["ball"].x, p["ball"].y = p["bx"], p["by"]      # ให้กล่องชนรู้ตำแหน่งลูกของเฟรมนี้ก่อนเช็ค
    hit = p["bvx"] < 0 and game.hit(p["ball"], p["pad"])
    if hit:                                   # สะท้อน + เร่ง + มุมตามจุดที่โดน + สปินจากไม้ที่กำลังวิ่ง
        norm = ((p["by"] + BALL_SIZE / 2) - (p["py"] + PADDLE_H / 2)) / (PADDLE_H / 2)
        p["bx"] = float(PADDLE_X + PADDLE_W)
        p["bvx"] = abs(p["bvx"]) + BALL_SPEEDUP
        p["spin"] = p["pvy"] * PADDLE_SPIN
        p["bvy"] += norm * 2.0 + p["spin"]
        speed = (p["bvx"] ** 2 + p["bvy"] ** 2) ** 0.5
        if speed > BALL_MAXV:                 # จำกัดความเร็ว "รวม" ไม่ให้ทะลุเพดาน
            p["bvx"], p["bvy"] = p["bvx"] * BALL_MAXV / speed, p["bvy"] * BALL_MAXV / speed
        game.sfx("paddle")
    p["ball"].move_to(p["bx"], p["by"])       # วาดหลังเคลียร์เรื่องชนแล้ว ลูกจะไม่จมในไม้
    return hit
# ===== จบชิ้น =====

# ---- 4) หน้าจอ: สร้างครบทุกตัวครั้งเดียว แล้วโชว์ทีละตัว (ตัวอื่นซ่อนไว้) ----
# เปลี่ยนตัว = ซ่อนชุดเก่า + โชว์ชุดใหม่ ไม่สร้างไม่ลบระหว่างเล่นเลย (สไปรต์ใช้ซ้ำได้ตลอด)
def enter_ship(s):
    bg.set_color(SPACE)
    s["x"], s["y"], s["vx"], s["vy"], s["t"] = 365.0, float(game.HEIGHT - SHIP_H - 32), 0.0, 0.0, 0
    s["on"] = [False] * len(s["b"])
    s["spr"].move_to(s["x"], s["y"])

def play_ship(s, k, a):
    s["t"] += 1
    phase = (s["t"] // 24) % 4                # ขวา 24 เฟรม · ปล่อย · ซ้าย 24 เฟรม · ปล่อย
    ship_step(s, phase == 2, phase == 0, k.up, k.down)
    if a:
        ship_fire(s)
    say("vx %d  vy %d  (เพดาน 13)   กระสุนพก vx ไป 55%%" % (int(s["vx"]), int(s["vy"])))

def make_enemies():
    d = {"e": [make_enemy(i, 0, 0) for i in range(3)], "bm": make_booms(3)}   # ระเบิดสร้างทีหลัง อยู่ชั้นบน
    d["all"], d["base"] = d["e"] + d["bm"]["spr"], d["e"]
    return d

def enter_enemy(d):
    bg.set_color(SPACE)
    for i, e in enumerate(d["e"]):
        e.move_to(220 + i * 160, 100 + i * 60)
    d["bm"]["t"] = [0] * len(d["bm"]["t"])
    d["vy"], d["n"] = [random.randint(18, 34) / 20.0 for _ in range(3)], 0   # ช้ากว่าเกมเต็มครึ่งหนึ่ง

def play_enemy(d, k, a):
    for i, e in enumerate(d["e"]):
        e.move_to(e.x, (e.y + d["vy"][i]) if e.y < game.HEIGHT else -20)   # หลุดล่าง = เกิดใหม่บนสุด
    if a:                                     # ยิงโดนตัวล่างสุด: บึ้ม + เสียงโดน + เกิดใหม่บนสุด
        e = max(d["e"], key=lambda en: en.y)
        boom(d["bm"], e.x + 13, e.y + 8, e.kind)
        game.sfx("hit")
        e.move_to(random.randint(20, game.WIDTH - 46), -20)
        d["n"] += 1
    booms_step(d["bm"])
    say("invader / UFO / demon   ระเบิดไป %d ตัว" % d["n"])

def enter_bird(b):
    bg.set_color(0x88C1F5)                    # ฟ้าแถบกลางของ flappy_full.py:35
    bird_reset(b)

def play_bird(b, k, a):
    up = game.pressed_once("up", k)           # อ่านทุกปุ่มก่อนค่อย or (pressed_once ต้องเห็นทุกเฟรม)
    if not bird_step(b, a or up, DT):
        bird_reset(b)
    if not b["started"]:
        say("ลอยรอ  กด A หรือ UP เพื่อออกตัว")
    elif b["grace"] > 0:
        say("grace อีก %d วินาที: ตกพื้นยังไม่ตาย" % ((b["grace"] + FPS - 1) // FPS))
    else:
        say("บินจริงแล้ว: ตกถึงพื้น = ตาย")

def enter_snake(sn):
    bg.set_color(0x4CA838)                    # สนามหญ้า (snake_sprite_full.py:25)
    sn["body"], sn["dx"], sn["dy"], sn["shown"] = [[10 - i, 12] for i in range(5)], 1, 0, 0
    sn["t"], sn["want"], sn["score"] = 0, None, 0
    snake_draw(sn)

def play_snake(sn, k, a):
    up, down = game.pressed_once("up", k), game.pressed_once("down", k)
    if a or up or down:                       # จำปุ่มไว้ เลี้ยวตอนก้าว (ก้าวละครั้ง เหมือนเกมเต็ม)
        sn["want"] = "a" if a else ("up" if up else "down")
    sn["t"] += 1
    if sn["t"] % 4:                           # ก้าวทุก 4 เฟรม = 133 ms (เกมเต็ม CLASSIC 115 ms)
        return
    if sn["want"] == "a":                     # ทางลัดของโชว์: เลี้ยวตามเข็มนาฬิกา ขวา-ลง-ซ้าย-ขึ้น
        sn["dx"], sn["dy"] = -sn["dy"], sn["dx"]
    else:
        snake_turn(sn, sn["want"] == "up", sn["want"] == "down", False, False)
    sn["want"] = None
    got = snake_step(sn)
    if got == "eat":
        sn["score"] += 1
    if got in ("wall", "self"):               # ตาย: เริ่มตัวใหม่ (เกมเต็มขึ้นป้าย GAME OVER)
        score = sn["score"]
        enter_snake(sn)
        sn["score"] = score
    say("หัว: %s   กินไป %d   ยาว %d" % (sn["head"].name, sn["score"], len(sn["body"])))

def enter_pong(p):
    bg.set_color(0x1C7A34)                    # สักหลาดเขียว FELT (pong_full.py:28)
    p["py"], p["pvy"] = float(game.HEIGHT // 2 - PADDLE_H // 2), 0.0
    p["pad"].move_to(PADDLE_X, p["py"])
    pong_serve(p, False)
    say("ตีตอนไม้กำลังวิ่ง = ใส่สปิน")

def play_pong(p, k, a):
    if a:
        pong_serve(p, False)
    paddle_step(p, k.up, k.down)
    if ball_step(p):
        say("ตีโดน! vy ไม้ %d -> สปิน %.1f -> vy ลูก %.1f" % (int(p["pvy"]), p["spin"], p["bvy"]))
    if p["bx"] > game.WIDTH - BALL_SIZE:      # โชว์นี้ใช้ผนังขวาแทนไม้ AI ของเกมเต็ม
        p["bx"], p["bvx"] = float(game.WIDTH - BALL_SIZE), -abs(p["bvx"])
        p["ball"].move_to(p["bx"], p["by"])
        game.sfx("wall")
    elif p["bx"] < -25:                       # พลาด: ลูกเลยขอบไป 25 px แล้วเสิร์ฟใหม่
        game.sfx("pong_score")
        pong_serve(p, False)

def say(msg):                                 # เขียนป้ายเฉพาะตอนข้อความเปลี่ยน
    if msg != view["last"]:
        info.set(msg)
        view["last"] = msg

def open_piece(i):                            # ซ่อนตัวเก่าทั้งชุด โชว์ตัวใหม่ แล้วเริ่มสถานะใหม่
    for w in pieces[view["idx"]]["all"]:
        w.hide()
    view["idx"] = i
    say("")                                   # ล้างป้ายข้อมูลของตัวก่อน
    for w in pieces[i]["base"]:
        w.show()
    PIECES[i][1](pieces[i])
    caption.set("%d/%d  %s   (ซ้าย/ขวา = ตัวอื่น)" % (i + 1, len(PIECES), PIECES[i][0]))
    helper.set(PIECES[i][3])

PIECES = (("SHIP  shooter_full", enter_ship, play_ship, "UP/DOWN = ขับ   A = ยิง   (ซ้าย-ขวาบินเอง)"),
          ("ENEMY + BOOM  shooter_full", enter_enemy, play_enemy, "A = ยิงตัวล่างสุดให้ระเบิด"),
          ("BIRD  flappy_full", enter_bird, play_bird, "A หรือ UP = กระพือ"),
          ("SNAKE HEAD  snake_sprite_full", enter_snake, play_snake, "A = เลี้ยวตามเข็ม   UP/DOWN = เลี้ยว"),
          ("PADDLE + SPIN  pong_full", enter_pong, play_pong, "UP/DOWN = ขยับไม้   A = เสิร์ฟ"))

game.title("KIT CHARACTERS")
bg = game.background(SPACE)                   # พื้นหลังชิ้นเดียวใช้ทุกตัว เปลี่ยนแค่สี
ship = make_ship(0, 0, bullets=3)             # โชว์ใช้ 3 นัดให้พองบ (เกมเต็มใช้ 4)
grass = game.grass(20)                        # พื้นหญ้าของนก = เส้นตาย (flappy_full.py:60)
bird = make_bird(game.WIDTH // 2, game.HEIGHT - 20)
snake = make_snake(10, 12, pool=4)            # พูล 4 = ยาว 5 ท่อนพอดี กินแล้วไม่โต (แบบพูลเต็มของเกมเต็ม)
pong = make_pong()
# "all" = ทุกชิ้นของตัวนั้น (ไว้ซ่อนตอนออก) · "base" = ชิ้นที่ต้องโชว์ตอนเข้า (กระสุน/ระเบิด/ลำตัว โชว์เองตามเกม)
ship["all"], ship["base"] = [ship["spr"]] + ship["b"], [ship["spr"]]
bird["all"] = bird["base"] = [grass, bird["spr"]]
snake["all"], snake["base"] = snake["seg"] + [snake["head"], snake["food"]], [snake["head"], snake["food"]]
pong["all"] = pong["base"] = [pong["pad"], pong["ball"]]
pieces = [ship, make_enemies(), bird, snake, pong]
caption = game.Text("", 12, 8, game.YELLOW)   # ป้ายสร้างหลังสุด จะได้อยู่ชั้นบน
helper = game.Text("", 12, 32, game.WHITE)
info = game.Text("", 12, 56, game.CYAN)
view = {"idx": 0, "last": None}
for d in pieces:                              # เริ่มต้น: ซ่อนทุกตัวก่อน
    for w in d["all"]:
        w.hide()
open_piece(0)

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    go = int(game.pressed_once("right", k)) - int(game.pressed_once("left", k))
    if go:                                    # เปลี่ยนตัวเฉพาะเฟรมที่เพิ่งกด
        open_piece((view["idx"] + go) % len(PIECES))
        game.sfx("move")
        return True
    PIECES[view["idx"]][2](pieces[view["idx"]], k, game.pressed_once("a", k))
    return True

game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) FRICTION = 0.80 -> 0.97 : ยานไถลยาวเหมือนบนน้ำแข็ง กว่าจะหยุดต้องรอนาน (0.5 = หยุดแทบทันที ไม่มีน้ำหนัก)
# 2) BULLET_INHERIT = 0.55 -> 0 : กระสุนพุ่งตรงขึ้นเสมอแม้ยานกำลังไถล · ลอง 1.5 แล้วกระสุนจะเฉียงแรงจนหลุดข้างจอ
# 3) GRACE = 120 -> 0 : นกตกพื้นตายทันทีตั้งแต่กระพือครั้งแรก — เห็นเลยว่าช่วงออกตัวช่วยผู้เล่นใหม่แค่ไหน

ลองเล่น · ชุดฉากสำเร็จรูป ยกมาจากเกมเต็มของจริง พร้อมราคาวิดเจ็ตของแต่ละฉาก

คัดลอกไฟล์ s13_17_kit_scenes.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · ซ้าย/ขวา = เปลี่ยนฉาก (โชว์ทีละฉาก ลบฉากเก่าทิ้งก่อน) · A = ลูกเล่นของฉาก
  • อวกาศ: A สลับดาว 5 / 12 ดวง ดูตัวเลข "ใช้ N ชิ้น" เปลี่ยน · โต๊ะ: A เปิด/ปิดเน็ต (14 ชิ้น)
  • ทุ่งหญ้า: A เปลี่ยน EASY/NORMAL/HARD ท่อต้นถัดไปช่องแคบลง เร็วขึ้น
  • Game Boy: A เปลี่ยนสีพื้นไล่ 4 โทน ดูว่าอะไรกลืนหายไปกับพื้น

ลองแก้ แล้วรันใหม่

  1. ใน make_meadow ลด SKY_BANDS เหลือ 4 สี : ฉากถูกลง 4 ชิ้น แต่ขอบแถบฟ้าเห็นชัดขึ้น (ยิ่งซอยถี่ยิ่งเนียน)
  2. PIPE_SPACING = 290 -> 400 : ท่อห่างขึ้น บินผ่านง่ายขึ้น (ถ้าต่ำกว่า 290 ท่อ 3 ต้นจะเรียงไม่พอความกว้างจอ แล้วท่อใหม่โผล่กลางจอ)
  3. make_box_snake(12, 16) -> make_box_snake(12, 16, 9) : งูยาวขึ้นเกือบเท่าตัว แต่แต่ละก้าวยังส่งแค่ 2 ข้อความเท่าเดิม

หยิบไปใช้ · ฉากอวกาศ

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/shooter_full.py:23-30, 67-68 · bentogame.py:846-861 · ไฟล์ตัวอย่าง s13_17_kit_scenes.py↗
# ===== หยิบไปใช้: ฉากอวกาศ (จาก full_games/shooter_full.py:23-30, 67-68 · bentogame.py:846-861) =====
SPACE_BG, STAR_DIM = 0x10122C, 0xB8C0E0

def make_space(stars=5):
    # shooter_full ใช้ดาว 5 ดวง (หักจากค่าปกติ 12 ไปให้ศัตรูกับกระสุนครบ) · เกมเต็มไม่เลื่อนดาว ฟ้านิ่ง
    return [game.background(SPACE_BG)] + game.starfield(stars, STAR_DIM)
# ===== จบชิ้น =====

หยิบไปใช้ · โต๊ะปิงปอง + เน็ต

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/pong_full.py:28-29, 169-175 · ไฟล์ตัวอย่าง s13_17_kit_scenes.py↗
# ===== หยิบไปใช้: โต๊ะปิงปอง + เน็ต (จาก full_games/pong_full.py:28-29, 169-175) =====
FELT, CHALK = 0x1C7A34, 0xF0F8F0             # สักหลาดเขียว + ชอล์กขาว สีเดียวกับเกม C

def make_court(with_net=True):
    w = [game.background(FELT),
         game.Box(0, 0, game.WIDTH, 3, CHALK), game.Box(0, game.HEIGHT - 3, game.WIDTH, 3, CHALK),
         game.Box(0, 0, 3, game.HEIGHT, CHALK), game.Box(game.WIDTH - 3, 0, 3, game.HEIGHT, CHALK)]
    if with_net:
        w += game.net(CHALK)                 # เน็ต 14 ขีด = 14 ชิ้น ชิ้นแพงที่สุดของฉากนี้
    return w
# ===== จบชิ้น =====

หยิบไปใช้ · ทุ่งหญ้าฟ้าไล่สี

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/flappy_full.py:23, 35-36, 55-60 · ไฟล์ตัวอย่าง s13_17_kit_scenes.py↗
# ===== หยิบไปใช้: ทุ่งหญ้าฟ้าไล่สี (จาก full_games/flappy_full.py:23, 35-36, 55-60) =====
GROUND_H = 20
PLAY_H = game.HEIGHT - GROUND_H              # ความสูงสนามเล่นจริง (เหนือพื้นหญ้า)
SKY_BANDS = (0x4898F0, 0x58A2F1, 0x68ADF2, 0x78B7F3, 0x88C1F5, 0x98CBF6, 0xA8D6F7, 0xB8E0F8)

def make_meadow():
    band_h, w = PLAY_H // len(SKY_BANDS), []
    for i, sky in enumerate(SKY_BANDS):      # ฟ้า 8 แถบ เข้มบน จางล่าง แทน gradient ของเกม C
        h = band_h if i < len(SKY_BANDS) - 1 else PLAY_H - band_h * i
        w.append(game.Box(0, i * band_h, game.WIDTH, h, sky))
    return w + [game.grass(GROUND_H)]
# ===== จบชิ้น =====

หยิบไปใช้ · ท่อ Flappy ขนาดคงที่ เลื่อนอย่างเดียว ไม่ resize

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/flappy_full.py:20-22, 27-33, 62-78, 97-128, 204-219 · ไฟล์ตัวอย่าง s13_17_kit_scenes.py↗
# ===== หยิบไปใช้: ท่อ Flappy ขนาดคงที่ เลื่อนอย่างเดียว ไม่ resize (จาก full_games/flappy_full.py:20-22, 27-33, 62-78, 97-128, 204-219) =====
# (เกมของน้องต้องมี import random ด้วย)
PIPE_SPACING, PIPE_W, CAP_W, CAP_H = 290, 64, 82, 18
PIPE_PLAY_H = game.HEIGHT - 20                # สนามเหนือพื้นหญ้า 20 px (เท่า PLAY_H ของฉากทุ่งหญ้า)
PIPE_TINTS, CAP_COLOR = (0x6CD84C, 0x4CB838, 0x84E060), 0x7CE85C   # เขียว 3 เฉด + ปากท่อสว่างกว่า
MODES = (("EASY", 175, 110.0), ("NORMAL", 130, 150.0), ("HARD", 105, 200.0))   # (ชื่อ, ช่องว่าง px, ท่อวิ่ง px/s)
CAP_SPRITE = False   # True = ปากท่อเป็นสไปรต์ pipe_cap ของเกม C (82x18 เท่ากล่องพอดี) ใช้ได้ในเกมที่สร้างท่อครั้งเดียว
                     # ไฟล์นี้ลบฉากทิ้งตอนเปลี่ยนฉาก และ BENTO Emulator ยังลบสไปรต์ไม่ได้ จึงตั้งไว้ False

def make_pipes(n=3, gap=130):
    pipes = []
    for _ in range(n):                       # ต้นละ 4 ชิ้น: ท่อบน/ล่าง "สูงเต็มสนาม" + ปากท่อบน/ล่าง
        p = {"top": game.Box(-200, 0, PIPE_W, PIPE_PLAY_H, game.GB_DARK),
             "bot": game.Box(-200, 0, PIPE_W, PIPE_PLAY_H, game.GB_DARK), "vis": True}
        for k in ("cap_t", "cap_b"):
            p[k] = game.Sprite("pipe_cap", -200, 0) if CAP_SPRITE else game.Box(-200, 0, CAP_W, CAP_H, CAP_COLOR)
        pipes.append(p)
    for i, p in enumerate(pipes):            # วางต้นแรกพ้นขอบขวา ต้นถัดไปห่างกันทีละ PIPE_SPACING
        reset_pipe(pipes, p, gap, game.WIDTH + i * PIPE_SPACING)
        render_pipe(p)
    return pipes

def reset_pipe(pipes, p, gap, first_x=None):
    # เกิดใหม่ทางขวา: สุ่มช่องว่าง ตำแหน่งช่อง เฉดสี — ไม่ resize (ท่อสูงเต็มสนามคงที่)
    p["x"] = float(first_x) if first_x is not None else max(q["x"] for q in pipes) + PIPE_SPACING
    p["gap"] = gap + random.randint(-12, 12)
    half = p["gap"] // 2
    p["gy"] = random.randint(half + 24, PIPE_PLAY_H - half - 24)
    p["gap_top"], p["gap_bot"] = max(p["gy"] - half, 20), min(p["gy"] + half, PIPE_PLAY_H - 20)
    tint = PIPE_TINTS[random.randint(0, 2)]
    p["top"].set_color(tint)
    p["bot"].set_color(tint)

def render_pipe(p):
    x = int(p["x"])
    on = -PIPE_W < x < game.WIDTH
    if on != p["vis"]:                       # โชว์/ซ่อนเฉพาะตอนเปลี่ยน
        for k in ("top", "bot", "cap_t", "cap_b"):
            if on:
                p[k].show()
            else:
                p[k].hide()
        p["vis"] = on
    if on:                                   # เลื่อนอย่างเดียว ส่วนที่พ้นขอบจอ ฮาร์ดแวร์ตัดทิ้งให้เอง
        p["top"].move_to(x, p["gap_top"] - PIPE_PLAY_H)   # ขอบล่างของท่อบน = ขอบบนของช่อง
        p["bot"].move_to(x, p["gap_bot"])            # ขอบบนของท่อล่าง = ขอบล่างของช่อง
        cap_x = x + PIPE_W // 2 - CAP_W // 2
        p["cap_t"].move_to(cap_x, p["gap_top"] - CAP_H)
        p["cap_b"].move_to(cap_x, p["gap_bot"])

def pipes_step(pipes, step_px, gap):         # เรียกทุกเฟรม step_px = ระยะเต็มพิกเซลของเฟรมนี้
    for p in pipes:
        p["x"] -= step_px
        if p["x"] + PIPE_W < 0:              # หลุดจอซ้าย: วนกลับไปเกิดใหม่ทางขวา
            reset_pipe(pipes, p, gap)
        render_pipe(p)
# ===== จบชิ้น =====

หยิบไปใช้ · พาเลต Game Boy 4 สี + งูกล่องเดินด้วยเคล็ดย้ายหาง

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก bentogame.py:37-41 · full_games/snake_full.py:12, 61-70, 137-147 · ไฟล์ตัวอย่าง s13_17_kit_scenes.py↗
# ===== หยิบไปใช้: พาเลต Game Boy 4 สี + งูกล่องเดินด้วยเคล็ดย้ายหาง (จาก bentogame.py:37-41 · full_games/snake_full.py:12, 61-70, 137-147) =====
GB = (("GB_DARKEST", game.GB_DARKEST), ("GB_DARK", game.GB_DARK),
      ("GB_LIGHT", game.GB_LIGHT), ("GB_LIGHTEST", game.GB_LIGHTEST))   # เข้มสุด -> สว่างสุด (ค่าเดียวกับเกม C)
CELL = 16

def make_box_snake(col, row, length=5):     # หัวเข้มสุด ตัวเข้ม แบบ snake_full
    body = [[col - i, row] for i in range(length)]
    head = game.Box(body[0][0] * CELL, body[0][1] * CELL, CELL - 2, CELL - 2, game.GB_DARKEST)
    boxes = [game.Box(c[0] * CELL, c[1] * CELL, CELL - 2, CELL - 2, game.GB_DARK) for c in body[1:]]
    return {"body": body, "head": head, "boxes": boxes}

def box_snake_step(sn, dx, dy):
    # ทั้งตัวดูเหมือนขยับ แต่ที่เปลี่ยนจริงมีแค่หัวกับหาง: ย้าย "กล่องหาง" ไปสวมตำแหน่งหัวเก่า
    old = sn["body"][0]
    new = [old[0] + dx, old[1] + dy]
    sn["body"].insert(0, new)
    sn["body"].pop()
    sn["head"].move_to(new[0] * CELL, new[1] * CELL)
    box = sn["boxes"].pop()
    box.move_to(old[0] * CELL, old[1] * CELL)
    sn["boxes"].insert(0, box)                # กลายเป็นคอ (ต่อจากหัว)
# ===== จบชิ้น =====

โค้ดเต็ม · s13_17_kit_scenes.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_17_kit_scenes.py — คาบ 13 (คาบพิเศษ): ชุดฉากสำเร็จรูป ยกมาจากเกมเต็มของจริง พร้อมราคาวิดเจ็ตของแต่ละฉาก
#
# หลักการ   : ฉาก = กล่องที่สร้างครั้งเดียวตอนจัดฉาก ไม่มีงานต่อเฟรม แต่ "กินงบวิดเจ็ต" ทุกชิ้น  (คาบ 4, 12)
#             ของที่สร้างก่อนอยู่ชั้นล่าง · พื้นหลังต้องสร้างเป็นอย่างแรกเสมอ               (คาบ 4)
#             ท่อ Flappy: ขนาดคงที่ เลื่อนอย่างเดียว x = x - ความเร็ว*dt ห้าม resize ทุกเฟรม  (คาบ 5, 11)
#             งูกล่อง: ย้ายกล่องหางไปเป็นคอ = 2 ข้อความต่อก้าว ไม่ว่างูยาวแค่ไหน            (คาบ 11)
# ลองเล่น   : กด Start · ซ้าย/ขวา = เปลี่ยนฉาก (โชว์ทีละฉาก ลบฉากเก่าทิ้งก่อน) · A = ลูกเล่นของฉาก
#             อวกาศ: A สลับดาว 5 / 12 ดวง ดูตัวเลข "ใช้ N ชิ้น" เปลี่ยน · โต๊ะ: A เปิด/ปิดเน็ต (14 ชิ้น)
#             ทุ่งหญ้า: A เปลี่ยน EASY/NORMAL/HARD ท่อต้นถัดไปช่องแคบลง เร็วขึ้น
#             Game Boy: A เปลี่ยนสีพื้นไล่ 4 โทน ดูว่าอะไรกลืนหายไปกับพื้น
# ของบนบอร์ด: จอ · จอย
# ในเกม     : shooter_full (อวกาศ) · pong_full (โต๊ะ+เน็ต) · flappy_full (ฟ้า+หญ้า+ท่อ) · snake_full (สี GB)
# ในงานจริง : ฉากหลังของหน้าจอควบคุม = ผังโรงงาน/ผังถัง วาดครั้งเดียว · ท่อเลื่อน = สายพานที่ของไหลผ่าน
#             พื้นเรียบสีเดียวอ่านง่ายกว่าพื้นลาย (แนว high-performance HMI) · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : full_games/shooter_full.py · pong_full.py · flappy_full.py · snake_full.py · bentogame.py
# งบวิดเจ็ต : สูงสุด 24 ชิ้น อยู่บนจอทีละฉาก (ทุ่งหญ้า: ฟ้า 8 + หญ้า 1 + ท่อ 3x4 = 21 + ป้าย 2 + game.run 1)
#             อวกาศ 6 หรือ 13 (ดาว 5/12) · โต๊ะ 19 หรือ 5 (เน็ต 14) · Game Boy 18 — ตัวเลขนี้นับจริงบนจอทุกครั้ง
#
# วิธีหยิบไปใช้: คัดลอกกล่อง "หยิบไปใช้ ... จบชิ้น" ไปวางใต้ import ของเกมน้อง แล้วเรียก make_... ครั้งเดียว
#   ก่อน game.run() เป็นอย่างแรก (ฉากต้องอยู่ชั้นล่างสุด) — ทุก make_ คืนลิสต์วิดเจ็ต len() ของมันคือราคาฉาก
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30
DT = 1.0 / FPS          # flappy_full วัด dt จริงด้วย time.ticks_ms · ชุดนี้ใช้ dt คงที่ จะได้ import แค่ bentogame

# ---- 2) สมอง + ชิ้นสำเร็จรูป (หยิบไปใช้ได้ทีละกล่อง) ----
# ===== หยิบไปใช้: ฉากอวกาศ (จาก full_games/shooter_full.py:23-30, 67-68 · bentogame.py:846-861) =====
SPACE_BG, STAR_DIM = 0x10122C, 0xB8C0E0

def make_space(stars=5):
    # shooter_full ใช้ดาว 5 ดวง (หักจากค่าปกติ 12 ไปให้ศัตรูกับกระสุนครบ) · เกมเต็มไม่เลื่อนดาว ฟ้านิ่ง
    return [game.background(SPACE_BG)] + game.starfield(stars, STAR_DIM)
# ===== จบชิ้น =====

# ===== หยิบไปใช้: โต๊ะปิงปอง + เน็ต (จาก full_games/pong_full.py:28-29, 169-175) =====
FELT, CHALK = 0x1C7A34, 0xF0F8F0             # สักหลาดเขียว + ชอล์กขาว สีเดียวกับเกม C

def make_court(with_net=True):
    w = [game.background(FELT),
         game.Box(0, 0, game.WIDTH, 3, CHALK), game.Box(0, game.HEIGHT - 3, game.WIDTH, 3, CHALK),
         game.Box(0, 0, 3, game.HEIGHT, CHALK), game.Box(game.WIDTH - 3, 0, 3, game.HEIGHT, CHALK)]
    if with_net:
        w += game.net(CHALK)                 # เน็ต 14 ขีด = 14 ชิ้น ชิ้นแพงที่สุดของฉากนี้
    return w
# ===== จบชิ้น =====

# ===== หยิบไปใช้: ทุ่งหญ้าฟ้าไล่สี (จาก full_games/flappy_full.py:23, 35-36, 55-60) =====
GROUND_H = 20
PLAY_H = game.HEIGHT - GROUND_H              # ความสูงสนามเล่นจริง (เหนือพื้นหญ้า)
SKY_BANDS = (0x4898F0, 0x58A2F1, 0x68ADF2, 0x78B7F3, 0x88C1F5, 0x98CBF6, 0xA8D6F7, 0xB8E0F8)

def make_meadow():
    band_h, w = PLAY_H // len(SKY_BANDS), []
    for i, sky in enumerate(SKY_BANDS):      # ฟ้า 8 แถบ เข้มบน จางล่าง แทน gradient ของเกม C
        h = band_h if i < len(SKY_BANDS) - 1 else PLAY_H - band_h * i
        w.append(game.Box(0, i * band_h, game.WIDTH, h, sky))
    return w + [game.grass(GROUND_H)]
# ===== จบชิ้น =====

# ===== หยิบไปใช้: ท่อ Flappy ขนาดคงที่ เลื่อนอย่างเดียว ไม่ resize (จาก full_games/flappy_full.py:20-22, 27-33, 62-78, 97-128, 204-219) =====
# (เกมของน้องต้องมี import random ด้วย)
PIPE_SPACING, PIPE_W, CAP_W, CAP_H = 290, 64, 82, 18
PIPE_PLAY_H = game.HEIGHT - 20                # สนามเหนือพื้นหญ้า 20 px (เท่า PLAY_H ของฉากทุ่งหญ้า)
PIPE_TINTS, CAP_COLOR = (0x6CD84C, 0x4CB838, 0x84E060), 0x7CE85C   # เขียว 3 เฉด + ปากท่อสว่างกว่า
MODES = (("EASY", 175, 110.0), ("NORMAL", 130, 150.0), ("HARD", 105, 200.0))   # (ชื่อ, ช่องว่าง px, ท่อวิ่ง px/s)
CAP_SPRITE = False   # True = ปากท่อเป็นสไปรต์ pipe_cap ของเกม C (82x18 เท่ากล่องพอดี) ใช้ได้ในเกมที่สร้างท่อครั้งเดียว
                     # ไฟล์นี้ลบฉากทิ้งตอนเปลี่ยนฉาก และ BENTO Emulator ยังลบสไปรต์ไม่ได้ จึงตั้งไว้ False

def make_pipes(n=3, gap=130):
    pipes = []
    for _ in range(n):                       # ต้นละ 4 ชิ้น: ท่อบน/ล่าง "สูงเต็มสนาม" + ปากท่อบน/ล่าง
        p = {"top": game.Box(-200, 0, PIPE_W, PIPE_PLAY_H, game.GB_DARK),
             "bot": game.Box(-200, 0, PIPE_W, PIPE_PLAY_H, game.GB_DARK), "vis": True}
        for k in ("cap_t", "cap_b"):
            p[k] = game.Sprite("pipe_cap", -200, 0) if CAP_SPRITE else game.Box(-200, 0, CAP_W, CAP_H, CAP_COLOR)
        pipes.append(p)
    for i, p in enumerate(pipes):            # วางต้นแรกพ้นขอบขวา ต้นถัดไปห่างกันทีละ PIPE_SPACING
        reset_pipe(pipes, p, gap, game.WIDTH + i * PIPE_SPACING)
        render_pipe(p)
    return pipes

def reset_pipe(pipes, p, gap, first_x=None):
    # เกิดใหม่ทางขวา: สุ่มช่องว่าง ตำแหน่งช่อง เฉดสี — ไม่ resize (ท่อสูงเต็มสนามคงที่)
    p["x"] = float(first_x) if first_x is not None else max(q["x"] for q in pipes) + PIPE_SPACING
    p["gap"] = gap + random.randint(-12, 12)
    half = p["gap"] // 2
    p["gy"] = random.randint(half + 24, PIPE_PLAY_H - half - 24)
    p["gap_top"], p["gap_bot"] = max(p["gy"] - half, 20), min(p["gy"] + half, PIPE_PLAY_H - 20)
    tint = PIPE_TINTS[random.randint(0, 2)]
    p["top"].set_color(tint)
    p["bot"].set_color(tint)

def render_pipe(p):
    x = int(p["x"])
    on = -PIPE_W < x < game.WIDTH
    if on != p["vis"]:                       # โชว์/ซ่อนเฉพาะตอนเปลี่ยน
        for k in ("top", "bot", "cap_t", "cap_b"):
            if on:
                p[k].show()
            else:
                p[k].hide()
        p["vis"] = on
    if on:                                   # เลื่อนอย่างเดียว ส่วนที่พ้นขอบจอ ฮาร์ดแวร์ตัดทิ้งให้เอง
        p["top"].move_to(x, p["gap_top"] - PIPE_PLAY_H)   # ขอบล่างของท่อบน = ขอบบนของช่อง
        p["bot"].move_to(x, p["gap_bot"])            # ขอบบนของท่อล่าง = ขอบล่างของช่อง
        cap_x = x + PIPE_W // 2 - CAP_W // 2
        p["cap_t"].move_to(cap_x, p["gap_top"] - CAP_H)
        p["cap_b"].move_to(cap_x, p["gap_bot"])

def pipes_step(pipes, step_px, gap):         # เรียกทุกเฟรม step_px = ระยะเต็มพิกเซลของเฟรมนี้
    for p in pipes:
        p["x"] -= step_px
        if p["x"] + PIPE_W < 0:              # หลุดจอซ้าย: วนกลับไปเกิดใหม่ทางขวา
            reset_pipe(pipes, p, gap)
        render_pipe(p)
# ===== จบชิ้น =====

# ===== หยิบไปใช้: พาเลต Game Boy 4 สี + งูกล่องเดินด้วยเคล็ดย้ายหาง (จาก bentogame.py:37-41 · full_games/snake_full.py:12, 61-70, 137-147) =====
GB = (("GB_DARKEST", game.GB_DARKEST), ("GB_DARK", game.GB_DARK),
      ("GB_LIGHT", game.GB_LIGHT), ("GB_LIGHTEST", game.GB_LIGHTEST))   # เข้มสุด -> สว่างสุด (ค่าเดียวกับเกม C)
CELL = 16

def make_box_snake(col, row, length=5):     # หัวเข้มสุด ตัวเข้ม แบบ snake_full
    body = [[col - i, row] for i in range(length)]
    head = game.Box(body[0][0] * CELL, body[0][1] * CELL, CELL - 2, CELL - 2, game.GB_DARKEST)
    boxes = [game.Box(c[0] * CELL, c[1] * CELL, CELL - 2, CELL - 2, game.GB_DARK) for c in body[1:]]
    return {"body": body, "head": head, "boxes": boxes}

def box_snake_step(sn, dx, dy):
    # ทั้งตัวดูเหมือนขยับ แต่ที่เปลี่ยนจริงมีแค่หัวกับหาง: ย้าย "กล่องหาง" ไปสวมตำแหน่งหัวเก่า
    old = sn["body"][0]
    new = [old[0] + dx, old[1] + dy]
    sn["body"].insert(0, new)
    sn["body"].pop()
    sn["head"].move_to(new[0] * CELL, new[1] * CELL)
    box = sn["boxes"].pop()
    box.move_to(old[0] * CELL, old[1] * CELL)
    sn["boxes"].insert(0, box)                # กลายเป็นคอ (ต่อจากหัว)
# ===== จบชิ้น =====


# ---- 4) หน้าจอ: โชว์ทีละฉาก ----
# เปลี่ยนฉาก = ลบวิดเจ็ตของฉากเก่าทิ้งทุกชิ้น แล้วสร้างฉากใหม่ เกิดเฉพาะเฟรมที่เพิ่งกด (ไม่ใช่ทุกเฟรม งบจึงไม่บาน)
# ไม่ใช้ game.clear() เพราะล้างทั้งจอแล้ว BENTO Emulator จะซ่อนจอราว 2 วินาทีกันภาพกระพริบ
# ฉากใหม่สร้างทีหลังป้ายปุ่มของ game.run() เลยทับป้ายนั้น — ป้ายบนสุดของไฟล์นี้จึงบอกปุ่มแทน
def build_space(opt):
    return make_space(12 if opt % 2 else 5), {}

def build_court(opt):
    return make_court(opt % 2 == 0), {}

def build_meadow(opt):
    w = make_meadow()
    pipes = make_pipes(3, MODES[opt % 3][1])
    return w + [p[k] for p in pipes for k in ("top", "bot", "cap_t", "cap_b")], {"pipes": pipes, "acc": 0.0}

def play_meadow(d, opt):
    name, gap, speed = MODES[opt % 3]
    d["acc"] += speed * DT                    # สะสมเศษพิกเซล ครบ 1 px ค่อยขยับ (flappy_full.py:204-206)
    step = int(d["acc"])
    d["acc"] -= step
    pipes_step(d["pipes"], step, gap)
    say("A = %s  ช่อง %d  ท่อ %d px/s   ซ้าย/ขวา = ฉากอื่น" % (name, gap, int(speed)))

def build_gb(opt):
    w = [game.background(GB[3 - opt % 4][1])]
    for i, (name, color) in enumerate(GB):   # แถบตัวอย่างสี 4 โทน + ชื่อ + ค่าสี
        w.append(game.Box(60 + i * 180, 96, 140, 56, color, border=game.GB_DARKEST, border_w=2, radius=6))
        w.append(game.Text(name, 60 + i * 180, 160, game.GB_DARKEST))
        w.append(game.Text("0x%06X" % color, 60 + i * 180, 184, game.GB_DARKEST))
    sn = make_box_snake(12, 16)
    return w + [sn["head"]] + sn["boxes"], {"sn": sn, "dir": (1, 0), "t": 0}

def play_gb(d, opt):
    say("A = สีพื้น: %s   ซ้าย/ขวา = ฉากอื่น" % GB[3 - opt % 4][0])
    d["t"] += 1
    if d["t"] % 3:                            # ก้าวทุก 3 เฟรม
        return
    c, r = d["sn"]["body"][0]                 # เดินวนสี่เหลี่ยมใต้แถบสี: ขวา ลง ซ้าย ขึ้น
    turn = {(1, 0): (c >= 42, (0, 1)), (0, 1): (r >= 21, (-1, 0)),
            (-1, 0): (c <= 6, (0, -1)), (0, -1): (r <= 16, (1, 0))}[d["dir"]]
    if turn[0]:
        d["dir"] = turn[1]
    box_snake_step(d["sn"], d["dir"][0], d["dir"][1])

SCENES = (("SPACE  shooter_full", build_space, None, game.YELLOW, "A = ดาว 5/12 ดวง   ซ้าย/ขวา = ฉากอื่น"),
          ("COURT  pong_full", build_court, None, game.WHITE, "A = เปิด/ปิดเน็ต   ซ้าย/ขวา = ฉากอื่น"),
          ("MEADOW + PIPES  flappy_full", build_meadow, play_meadow, game.WHITE, ""),
          ("GAME BOY  snake_full", build_gb, play_gb, game.GB_DARKEST, ""))
view = {"idx": 0, "made": [], "opt": [0, 0, 1, 0], "cur": None, "help": None, "last": None}

def open_scene(i):                            # ลบฉากเก่าทุกชิ้น สร้างฉากใหม่ แล้วสร้างป้ายทับไว้ชั้นบนสุด
    for w in view["made"]:
        w.delete()
    name, build, play, ink, hlp = SCENES[i]
    made, view["cur"] = build(view["opt"][i])
    cap = game.Text("%d/%d  %s   ใช้ %d ชิ้น" % (i + 1, len(SCENES), name, len(made)), 12, 8, ink)
    view["help"], view["last"], view["idx"] = game.Text(hlp, 12, 32, ink), hlp, i
    view["made"] = made + [cap, view["help"]]

def say(msg):                                 # เขียนป้ายเฉพาะตอนข้อความเปลี่ยน
    if msg != view["last"]:
        view["help"].set(msg)
        view["last"] = msg

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    go = int(game.pressed_once("right", k)) - int(game.pressed_once("left", k))
    i = view["idx"]
    if game.pressed_once("a", k):
        view["opt"][i] += 1
        game.sfx("select")
        if SCENES[i][2] is None:              # ฉากนิ่ง: ตัวเลือกเปลี่ยนจำนวนชิ้น ต้องสร้างฉากใหม่
            open_scene(i)
        elif i == 3:                          # Game Boy: เปลี่ยนแค่สีพื้น ไม่ต้องสร้างใหม่
            view["made"][0].set_color(GB[3 - view["opt"][i] % 4][1])
    if go:                                    # เปลี่ยนฉากเฉพาะเฟรมที่เพิ่งกด
        open_scene((i + go) % len(SCENES))
        game.sfx("move")
    elif SCENES[i][2] is not None:
        SCENES[i][2](view["cur"], view["opt"][i])
    return True

game.title("KIT SCENES")
open_scene(0)
game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) ใน make_meadow ลด SKY_BANDS เหลือ 4 สี : ฉากถูกลง 4 ชิ้น แต่ขอบแถบฟ้าเห็นชัดขึ้น (ยิ่งซอยถี่ยิ่งเนียน)
# 2) PIPE_SPACING = 290 -> 400 : ท่อห่างขึ้น บินผ่านง่ายขึ้น (ถ้าต่ำกว่า 290 ท่อ 3 ต้นจะเรียงไม่พอความกว้างจอ แล้วท่อใหม่โผล่กลางจอ)
# 3) make_box_snake(12, 16) -> make_box_snake(12, 16, 9) : งูยาวขึ้นเกือบเท่าตัว แต่แต่ละก้าวยังส่งแค่ 2 ข้อความเท่าเดิม

ลองเล่น · ชุดของประกอบ + ป้าย HUD สำเร็จรูป ยกมาจากเกมเต็มของจริง

คัดลอกไฟล์ s13_18_kit_props_hud.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · ซ้าย/ขวา = เปลี่ยนชิ้น (โชว์ทีละชิ้น) · A = ท่าของชิ้นนั้น
  • กระสุน: A ยิง กดรัวจนพูลหมดแล้วดูว่ายิงไม่ออก · อาหาร: A วางใหม่ ไม่เคยทับตัวงู
  • เมนู: UP/DOWN เลื่อน A ยืนยัน · จบเกม: คะแนนวิ่งเอง A = จบตา ดู Best · HUD: A ได้แต้ม B เสียชีวิต

ลองแก้ แล้วรันใหม่

  1. make_bullets(4) -> make_bullets(2) : กด A รัวแล้วได้เสียง deny บ่อยขึ้น เพราะนัดว่างในพูลหมดไวขึ้น
  2. ใน make_bullets เปลี่ยน radius=2 -> radius=6 และ border=game.WHITE -> game.PINK : กระสุนกลมมนขอบชมพู
  3. ใน hud_update ย้ายทั้ง h["t"].set(...) และ h["draws"] += 1 ออกมานอก if : ตัวเลขเหมือนเดิม แต่ป้ายล่างจะบอกว่าวาดใหม่ทุกเฟรม

หยิบไปใช้ · กระสุนเลเซอร์ขอบขาวมุมมน + ชุดหมุนเวียน

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/shooter_full.py:19, 32, 70-74, 115-117, 137-148, 205-214 · ไฟล์ตัวอย่าง s13_18_kit_props_hud.py↗
# ===== หยิบไปใช้: กระสุนเลเซอร์ขอบขาวมุมมน + ชุดหมุนเวียน (จาก full_games/shooter_full.py:19, 32, 70-74, 115-117, 137-148, 205-214) =====
LASER, BULLET_VY = 0x80F4FF, 8.0             # ฟ้าลำแสงสีเดียวกับเกม C · พุ่งขึ้นเฟรมละ 8 px

def make_bullets(n=4):
    bl = {"box": [], "on": [False] * n, "x": [0.0] * n, "y": [0.0] * n}
    for _ in range(n):                        # แท่ง 5x12 ขอบขาว มุมมน 2 — สร้างครั้งเดียวแล้วซ่อน
        b = game.Box(-20, -50, 5, 12, LASER, border=game.WHITE, radius=2)
        b.hide()
        bl["box"].append(b)
    return bl

def bullet_fire(bl, x, y):                    # คืน False เมื่อไม่มีนัดว่าง (ยิงไม่ออก ไม่สร้างนัดใหม่)
    for j in range(len(bl["on"])):
        if not bl["on"][j]:
            bl["on"][j], bl["x"][j], bl["y"][j] = True, x, y
            bl["box"][j].move_to(x, y)
            bl["box"][j].show()
            game.sfx("fire")
            return True
    return False

def bullets_step(bl):                         # เรียกทุกเฟรม: พุ่งขึ้น หลุดขอบบนแล้วเก็บคืนพูล
    for j in range(len(bl["on"])):
        if bl["on"][j]:
            bl["y"][j] -= BULLET_VY
            if bl["y"][j] < -12:
                bl["on"][j] = False
                bl["box"][j].hide()
            else:
                bl["box"][j].move_to(bl["x"][j], bl["y"][j])
# ===== จบชิ้น =====

หยิบไปใช้ · อาหาร/ผลไม้ วางสุ่มบนตาราง ไม่ทับตัวงู

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/snake_sprite_full.py:14-15, 64, 78-84 · snake_full.py:29, 71, 85-92 · ไฟล์ตัวอย่าง s13_18_kit_props_hud.py↗
# ===== หยิบไปใช้: อาหาร/ผลไม้ วางสุ่มบนตาราง ไม่ทับตัวงู (จาก full_games/snake_sprite_full.py:14-15, 64, 78-84 · snake_full.py:29, 71, 85-92) =====
# (เกมของน้องต้องมี import random ด้วย)
CELL = 16
COLS, ROWS = game.WIDTH // CELL, game.HEIGHT // CELL   # 49 x 24 ช่อง
APPLE = 0xD82818                                     # แอปเปิลกล่องแดงของ snake_full

def make_food(sprite=True):                   # True = แอปเปิลพิกเซลของเกม C · False = กล่องแดง 14x14
    w = game.Sprite("snake_food", 0, 0) if sprite else game.Box(0, 0, CELL - 2, CELL - 2, APPLE)
    return {"w": w, "cell": [0, 0]}

def place_food(food, body, rows=(0, ROWS)):   # rows = ช่วงแถวที่ยอมให้วาง (เว้นที่ให้ HUD ได้)
    while True:                               # สุ่มไปเรื่อย ๆ จนเจอช่องที่ไม่ทับตัวงู
        c = [random.randint(0, COLS - 1), random.randint(rows[0], rows[1] - 1)]
        if c not in body:
            food["cell"] = c
            food["w"].move_to(c[0] * CELL, c[1] * CELL)
            return
# ===== จบชิ้น =====

หยิบไปใช้ · กล่องเมนูแบบเกม C ที่ไม่หยุดเกม

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก bentogame.py:1099-1133, 1145-1155 ของ game.menu · ไฟล์ตัวอย่าง s13_18_kit_props_hud.py↗
# ===== หยิบไปใช้: กล่องเมนูแบบเกม C ที่ไม่หยุดเกม (จาก bentogame.py:1099-1133, 1145-1155 ของ game.menu) =====
MENU_BG, MENU_EDGE, MENU_DIM = 0x101A38, 0x80F4FF, 0x6C7CA8   # น้ำเงินเข้ม ขอบฟ้า มุมมน · แถวที่ไม่เลือกสีเทา

def make_menu(options, heading="SELECT MODE", sel=1):
    n = len(options)
    panel_h = 70 + 34 * n
    top = game.HEIGHT // 2 - panel_h // 2
    m = {"opt": options, "sel": sel, "heading": heading,
         "box": game.Box(game.WIDTH // 2 - 160, top, 320, panel_h, MENU_BG, border=MENU_EDGE, border_w=2, radius=10),
         "head": game.Text(heading, game.WIDTH // 2 - len(heading) * 4, top + 14, game.WHITE),
         "rows": [game.Text("", game.WIDTH // 2 - (len(options[i]) + 6) * 4, top + 50 + i * 34, game.WHITE)
                  for i in range(n)]}
    menu_paint(m)
    return m

def menu_paint(m):                            # ตัวที่เลือก = ">  ชื่อ  <" สีฟ้า ตัวอื่นสีเทา
    for i, row in enumerate(m["rows"]):
        row.set(">  %s  <" % m["opt"][i] if i == m["sel"] else "   %s" % m["opt"][i])
        try:
            row._label.color(MENU_EDGE if i == m["sel"] else MENU_DIM)   # ท่าเดียวกับ game.menu
        except Exception:
            pass

def menu_step(m, up, down):                   # ส่ง pressed_once ของ up/down มา · เลื่อนวนหัวท้าย
    n = len(m["opt"])
    if up:
        m["sel"] = (m["sel"] + n - 1) % n
    elif down:
        m["sel"] = (m["sel"] + 1) % n
    else:
        return
    game.sfx("move")
    menu_paint(m)
# ===== จบชิ้น =====

หยิบไปใช้ · ป้ายจบเกม + Best แบบไม่หยุดเกม

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก bentogame.py:1008-1019, 1043-1060 · shooter_full.py:270-277 · ไฟล์ตัวอย่าง s13_18_kit_props_hud.py↗
# ===== หยิบไปใช้: ป้ายจบเกม + Best แบบไม่หยุดเกม (จาก bentogame.py:1008-1019, 1043-1060 · shooter_full.py:270-277) =====
PANEL_BG, PANEL_EDGE = 0x101A38, 0x80F4FF    # สีกล่องเดียวกับ game.game_over (bentogame.py:1046-1047)

def make_over_panel():                        # กล่อง 360x140 น้ำเงินเข้ม ขอบฟ้า มุมมน
    x, y = game.WIDTH // 2 - 180, game.HEIGHT // 2 - 70
    p = {"box": game.Box(x, y, 360, 140, PANEL_BG, border=PANEL_EDGE, border_w=2, radius=10),
         "t": [game.Text("", x + 28, y + 22 + i * 36, c) for i, c in enumerate((game.CYAN, game.WHITE, game.GB_LIGHT))]}
    p["box"].hide()
    return p

def over_show(p, score, name, msg="GAME OVER", hint="A = เล่นใหม่"):
    best = game.save_best(score, name)        # บันทึกเข้าช่อง Best ก่อนแล้วค่อยโชว์ (game_over ทำแบบนี้)
    for t, text in zip(p["t"], (msg, "Score %d      Best %d" % (score, best), hint)):
        t.set(text)
    p["box"].show()

def over_hide(p):
    p["box"].hide()
    for t in p["t"]:                          # ป้ายข้อความซ่อนไม่ได้ จึงเขียนเป็นว่างแทน
        t.set("")
# ===== จบชิ้น =====

หยิบไปใช้ · HUD คะแนน/Best/ชีวิต วาดใหม่เฉพาะตอนเลขเปลี่ยน

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/shooter_full.py:77, 89-96 · ไฟล์ตัวอย่าง s13_18_kit_props_hud.py↗
# ===== หยิบไปใช้: HUD คะแนน/Best/ชีวิต วาดใหม่เฉพาะตอนเลขเปลี่ยน (จาก full_games/shooter_full.py:77, 89-96) =====
def make_hud(x=10, y=8):
    return {"t": game.Text("", x, y, game.WHITE), "last": None, "draws": 0}

def hud_update(h, score, lives, name="game"):
    row = (score, max(game.best(name), score), lives)    # Best ขยับตามทันทีเมื่อแซง แบบเกม C
    if row != h["last"]:
        h["last"] = row
        h["t"].set("Score: %d    Best: %d    Lives: %d" % row)
        h["draws"] += 1
# ===== จบชิ้น =====

โค้ดเต็ม · s13_18_kit_props_hud.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_18_kit_props_hud.py — คาบ 13 (คาบพิเศษ): ชุดของประกอบ + ป้าย HUD สำเร็จรูป ยกมาจากเกมเต็มของจริง
#
# หลักการ   : ชุดหมุนเวียน  สร้างกระสุนครบตั้งแต่แรกแล้วซ่อน ยิง = หยิบนัดว่าง หลุดจอ = เก็บคืน   (คาบ 11)
#             กล่องแต่งขอบ  game.Box(..., border=สีขอบ, radius=ความมน, border_w=ความหนา)        (คาบ 4)
#             สุ่มจนกว่าจะได้  วางอาหารซ้ำจนได้ช่องที่ไม่ทับตัวงู                               (คาบ 4)
#             เครื่องสถานะ   เมนูเลื่อนวนหัวท้าย sel = (sel + 1) % n · เล่นอยู่ / จบตา             (คาบ 2, 7)
#             HUD อัปเดตเมื่อเปลี่ยน  จำค่าชุดล่าสุดไว้ ต่างจากเดิมค่อย set()                 (คาบ 10, 12)
# ลองเล่น   : กด Start · ซ้าย/ขวา = เปลี่ยนชิ้น (โชว์ทีละชิ้น) · A = ท่าของชิ้นนั้น
#             กระสุน: A ยิง กดรัวจนพูลหมดแล้วดูว่ายิงไม่ออก · อาหาร: A วางใหม่ ไม่เคยทับตัวงู
#             เมนู: UP/DOWN เลื่อน A ยืนยัน · จบเกม: คะแนนวิ่งเอง A = จบตา ดู Best · HUD: A ได้แต้ม B เสียชีวิต
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : shooter_full (กระสุน HUD) · snake_full/snake_sprite_full (อาหาร) · ทุกเกมเต็ม (เมนู ป้ายจบเกม Best)
# ในงานจริง : กระสุนชุดหมุนเวียน = กล่องพัสดุบนสายพาน · เมนู = หน้าเลือกสูตรผลิต · ป้ายจบเกม = ป้ายหยุดเครื่อง
#             HUD = แถบยอดผลิต วาดเฉพาะตอนเปลี่ยน จอนิ่ง อ่านง่าย · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : full_games/shooter_full.py · snake_full.py · snake_sprite_full.py · bentogame.py (menu, game_over, best)
# งบวิดเจ็ต : 25 ชิ้น สร้างครบครั้งเดียว โชว์ทีละชิ้น (พื้นหลัง 1 + กระสุน 5 + อาหาร 5 + เมนู 5 + จบเกม 5
#             + HUD 1 + ป้ายล่าง 2 + game.run 1) — ไม่สร้างไม่ลบระหว่างเล่น
#
# วิธีหยิบไปใช้: คัดลอกกล่อง "หยิบไปใช้ ... จบชิ้น" ไปวางใต้ import ของเกมน้อง สร้างด้วย make_... ครั้งเดียวก่อน
#   game.run() แล้วเรียกฟังก์ชันที่เหลือใน on_frame — ตัวอย่างการต่อสายอยู่ในส่วนที่ 4
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30
SPACE = 0x10122C        # สีอวกาศ (shooter_full.py:67)
NAME = "kit"            # ชื่อช่อง Best ของไฟล์นี้ (เกมเต็มใช้ "shooter" "flappy" "snake" "pong")

# ---- 2) สมอง + ชิ้นสำเร็จรูป (หยิบไปใช้ได้ทีละกล่อง) ----
# ===== หยิบไปใช้: กระสุนเลเซอร์ขอบขาวมุมมน + ชุดหมุนเวียน (จาก full_games/shooter_full.py:19, 32, 70-74, 115-117, 137-148, 205-214) =====
LASER, BULLET_VY = 0x80F4FF, 8.0             # ฟ้าลำแสงสีเดียวกับเกม C · พุ่งขึ้นเฟรมละ 8 px

def make_bullets(n=4):
    bl = {"box": [], "on": [False] * n, "x": [0.0] * n, "y": [0.0] * n}
    for _ in range(n):                        # แท่ง 5x12 ขอบขาว มุมมน 2 — สร้างครั้งเดียวแล้วซ่อน
        b = game.Box(-20, -50, 5, 12, LASER, border=game.WHITE, radius=2)
        b.hide()
        bl["box"].append(b)
    return bl

def bullet_fire(bl, x, y):                    # คืน False เมื่อไม่มีนัดว่าง (ยิงไม่ออก ไม่สร้างนัดใหม่)
    for j in range(len(bl["on"])):
        if not bl["on"][j]:
            bl["on"][j], bl["x"][j], bl["y"][j] = True, x, y
            bl["box"][j].move_to(x, y)
            bl["box"][j].show()
            game.sfx("fire")
            return True
    return False

def bullets_step(bl):                         # เรียกทุกเฟรม: พุ่งขึ้น หลุดขอบบนแล้วเก็บคืนพูล
    for j in range(len(bl["on"])):
        if bl["on"][j]:
            bl["y"][j] -= BULLET_VY
            if bl["y"][j] < -12:
                bl["on"][j] = False
                bl["box"][j].hide()
            else:
                bl["box"][j].move_to(bl["x"][j], bl["y"][j])
# ===== จบชิ้น =====

# ===== หยิบไปใช้: อาหาร/ผลไม้ วางสุ่มบนตาราง ไม่ทับตัวงู (จาก full_games/snake_sprite_full.py:14-15, 64, 78-84 · snake_full.py:29, 71, 85-92) =====
# (เกมของน้องต้องมี import random ด้วย)
CELL = 16
COLS, ROWS = game.WIDTH // CELL, game.HEIGHT // CELL   # 49 x 24 ช่อง
APPLE = 0xD82818                                     # แอปเปิลกล่องแดงของ snake_full

def make_food(sprite=True):                   # True = แอปเปิลพิกเซลของเกม C · False = กล่องแดง 14x14
    w = game.Sprite("snake_food", 0, 0) if sprite else game.Box(0, 0, CELL - 2, CELL - 2, APPLE)
    return {"w": w, "cell": [0, 0]}

def place_food(food, body, rows=(0, ROWS)):   # rows = ช่วงแถวที่ยอมให้วาง (เว้นที่ให้ HUD ได้)
    while True:                               # สุ่มไปเรื่อย ๆ จนเจอช่องที่ไม่ทับตัวงู
        c = [random.randint(0, COLS - 1), random.randint(rows[0], rows[1] - 1)]
        if c not in body:
            food["cell"] = c
            food["w"].move_to(c[0] * CELL, c[1] * CELL)
            return
# ===== จบชิ้น =====

# ===== หยิบไปใช้: กล่องเมนูแบบเกม C ที่ไม่หยุดเกม (จาก bentogame.py:1099-1133, 1145-1155 ของ game.menu) =====
MENU_BG, MENU_EDGE, MENU_DIM = 0x101A38, 0x80F4FF, 0x6C7CA8   # น้ำเงินเข้ม ขอบฟ้า มุมมน · แถวที่ไม่เลือกสีเทา

def make_menu(options, heading="SELECT MODE", sel=1):
    n = len(options)
    panel_h = 70 + 34 * n
    top = game.HEIGHT // 2 - panel_h // 2
    m = {"opt": options, "sel": sel, "heading": heading,
         "box": game.Box(game.WIDTH // 2 - 160, top, 320, panel_h, MENU_BG, border=MENU_EDGE, border_w=2, radius=10),
         "head": game.Text(heading, game.WIDTH // 2 - len(heading) * 4, top + 14, game.WHITE),
         "rows": [game.Text("", game.WIDTH // 2 - (len(options[i]) + 6) * 4, top + 50 + i * 34, game.WHITE)
                  for i in range(n)]}
    menu_paint(m)
    return m

def menu_paint(m):                            # ตัวที่เลือก = ">  ชื่อ  <" สีฟ้า ตัวอื่นสีเทา
    for i, row in enumerate(m["rows"]):
        row.set(">  %s  <" % m["opt"][i] if i == m["sel"] else "   %s" % m["opt"][i])
        try:
            row._label.color(MENU_EDGE if i == m["sel"] else MENU_DIM)   # ท่าเดียวกับ game.menu
        except Exception:
            pass

def menu_step(m, up, down):                   # ส่ง pressed_once ของ up/down มา · เลื่อนวนหัวท้าย
    n = len(m["opt"])
    if up:
        m["sel"] = (m["sel"] + n - 1) % n
    elif down:
        m["sel"] = (m["sel"] + 1) % n
    else:
        return
    game.sfx("move")
    menu_paint(m)
# ===== จบชิ้น =====

# ===== หยิบไปใช้: ป้ายจบเกม + Best แบบไม่หยุดเกม (จาก bentogame.py:1008-1019, 1043-1060 · shooter_full.py:270-277) =====
PANEL_BG, PANEL_EDGE = 0x101A38, 0x80F4FF    # สีกล่องเดียวกับ game.game_over (bentogame.py:1046-1047)

def make_over_panel():                        # กล่อง 360x140 น้ำเงินเข้ม ขอบฟ้า มุมมน
    x, y = game.WIDTH // 2 - 180, game.HEIGHT // 2 - 70
    p = {"box": game.Box(x, y, 360, 140, PANEL_BG, border=PANEL_EDGE, border_w=2, radius=10),
         "t": [game.Text("", x + 28, y + 22 + i * 36, c) for i, c in enumerate((game.CYAN, game.WHITE, game.GB_LIGHT))]}
    p["box"].hide()
    return p

def over_show(p, score, name, msg="GAME OVER", hint="A = เล่นใหม่"):
    best = game.save_best(score, name)        # บันทึกเข้าช่อง Best ก่อนแล้วค่อยโชว์ (game_over ทำแบบนี้)
    for t, text in zip(p["t"], (msg, "Score %d      Best %d" % (score, best), hint)):
        t.set(text)
    p["box"].show()

def over_hide(p):
    p["box"].hide()
    for t in p["t"]:                          # ป้ายข้อความซ่อนไม่ได้ จึงเขียนเป็นว่างแทน
        t.set("")
# ===== จบชิ้น =====

# ===== หยิบไปใช้: HUD คะแนน/Best/ชีวิต วาดใหม่เฉพาะตอนเลขเปลี่ยน (จาก full_games/shooter_full.py:77, 89-96) =====
def make_hud(x=10, y=8):
    return {"t": game.Text("", x, y, game.WHITE), "last": None, "draws": 0}

def hud_update(h, score, lives, name="game"):
    row = (score, max(game.best(name), score), lives)    # Best ขยับตามทันทีเมื่อแซง แบบเกม C
    if row != h["last"]:
        h["last"] = row
        h["t"].set("Score: %d    Best: %d    Lives: %d" % row)
        h["draws"] += 1
# ===== จบชิ้น =====


# ---- 4) หน้าจอ: สร้างครบทุกชิ้นครั้งเดียว แล้วโชว์ทีละชิ้น ----
def enter_bullets(d):
    bg.set_color(SPACE)
    d["on"] = [False] * len(d["on"])

def play_bullets(d, k, a):
    if a and not bullet_fire(d, gun.x + 29, gun.y - 8):  # ยิงจากหัวยาน (ยานกว้าง 62)
        game.sfx("deny")
    bullets_step(d)
    say("ในอากาศ %d/%d นัด   หมดพูล = ยิงไม่ออก" % (d["on"].count(True), len(d["on"])))

def enter_food(d):
    bg.set_color(0x4CA838)                    # สนามหญ้า (snake_sprite_full.py:25)
    d["n"] = 0
    play_food(d, None, True)

def play_food(d, k, a):
    if a:
        for f in d["foods"]:
            place_food(f, d["body"], (4, 19))  # เว้นแถวบนกับล่างให้ป้าย
        game.sfx("eat")
        d["n"] += 1
    say("วางใหม่ %d ครั้ง  ไม่เคยทับตัวงู" % d["n"])

def enter_menu(m):
    bg.set_color(SPACE)
    m["head"].set(m["heading"])
    menu_paint(m)

def play_menu(m, k, a):
    menu_step(m, game.pressed_once("up", k), game.pressed_once("down", k))
    if a:
        game.sfx("select")
        say("เลือก %s แล้ว   ในเกมเต็มคือ mode = picked" % m["opt"][m["sel"]])

def enter_over(d):
    bg.set_color(SPACE)
    d["score"], d["t"], d["over"], d["hud"]["last"] = 0, 0, False, None

def play_over(d, k, a):
    if a and d["over"]:                       # เล่นใหม่: ซ่อนป้าย เริ่มนับคะแนนใหม่
        over_hide(d["panel"])
        d["score"], d["over"] = 0, False
        game.sfx("start")
    elif a:                                   # จบตา: บันทึก Best + ขึ้นป้าย (เกมยังเดินต่อได้)
        over_show(d["panel"], d["score"], NAME)
        d["over"] = True
        game.sfx("gameover")
    d["t"] += 1
    if not d["over"] and d["t"] % 20 == 0:    # จำลองว่ากำลังเล่น: ได้แต้มทุก 20 เฟรม
        d["score"] += 1
    hud_update(d["hud"], d["score"], 3, NAME)
    say("ป้ายจบเกม" if d["over"] else "คะแนนวิ่งเอง  A = จบตา แล้วดู Best")

def enter_hud(h):
    bg.set_color(SPACE)
    h["score"], h["lives"], h["frames"], h["last"], h["draws"] = 0, 3, 0, None, 0

def play_hud(h, k, a):
    if a:
        h["score"] += 1
        game.sfx("hit")
    if game.pressed_once("b", k):
        h["lives"] -= 1
        game.sfx("lose_life")
        if h["lives"] == 0:                   # ชีวิตหมด: บันทึก Best แล้วเริ่มใหม่
            game.save_best(h["score"], NAME)
            game.sfx("gameover")
            h["score"], h["lives"] = 0, 3
    hud_update(h, h["score"], h["lives"], NAME)
    h["frames"] += 1
    if h["frames"] % FPS == 0:                # ป้ายนี้เองก็เขียนแค่วินาทีละครั้ง
        say("%d เฟรม  แต่วาด HUD ใหม่ %d ครั้ง" % (h["frames"], h["draws"]))

def say(msg):                                 # เขียนป้ายล่างเฉพาะตอนข้อความเปลี่ยน
    if msg != view["last"]:
        info.set(msg)
        view["last"] = msg

def hide(w):
    if hasattr(w, "hide"):
        w.hide()
    else:
        w.set("")                             # ป้ายข้อความ (Text) ไม่มี hide ใช้ข้อความว่างแทน

def open_piece(i):                            # ซ่อนชิ้นเก่าทั้งชุด โชว์ชิ้นใหม่ แล้วเริ่มสถานะใหม่
    for w in pieces[view["idx"]]["all"]:
        hide(w)
    view["idx"] = i
    say("")
    for w in pieces[i]["base"]:
        w.show()
    PIECES[i][1](pieces[i])
    caption.set("%d/%d  %s   %s" % (i + 1, len(PIECES), PIECES[i][0], PIECES[i][3]))

PIECES = (("BULLETS", enter_bullets, play_bullets, "A = ยิง"),
          ("FOOD", enter_food, play_food, "A = วางใหม่"),
          ("MENU", enter_menu, play_menu, "UP/DOWN + A"),
          ("GAME OVER + BEST", enter_over, play_over, "A = จบตา / เล่นใหม่"),
          ("HUD", enter_hud, play_hud, "A = ได้แต้ม  B = เสียชีวิต"))

game.title("KIT PROPS + HUD")
bg = game.background(SPACE)                   # พื้นหลังชิ้นเดียวใช้ทุกชิ้น เปลี่ยนแค่สี
gun = game.Sprite("ship", game.WIDTH // 2 - 31, 250)
bullets = make_bullets(4)
bullets["all"], bullets["base"] = [gun] + bullets["box"], [gun]
body = [[c, 10] for c in range(22, 25)]       # ตัวงูสมมุติ 3 ช่อง ไว้ให้ place_food หลบ
food = {"foods": [make_food(True), make_food(False)], "body": body,
        "segs": [game.Sprite("snake_body", c * CELL, r * CELL) for c, r in body]}
food["all"] = food["base"] = [f["w"] for f in food["foods"]] + food["segs"]
menu = make_menu(("EASY", "NORMAL", "HARD"))
menu["all"], menu["base"] = [menu["box"], menu["head"]] + menu["rows"], [menu["box"]]
over = {"hud": make_hud(), "panel": make_over_panel()}
over["all"], over["base"] = [over["hud"]["t"], over["panel"]["box"]] + over["panel"]["t"], []
hud = make_hud()
hud["all"], hud["base"] = [hud["t"]], []
pieces = [bullets, food, menu, over, hud]
caption = game.Text("", 12, game.HEIGHT - 74, game.YELLOW)   # ป้ายล่าง สร้างหลังสุดจะได้อยู่ชั้นบน
info = game.Text("", 12, game.HEIGHT - 50, game.CYAN)
view = {"idx": 0, "last": None}
for d in pieces[1:]:                          # เริ่มต้น: ซ่อนทุกชิ้นยกเว้นชิ้นแรก
    for w in d["all"]:
        hide(w)
open_piece(0)

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    go = int(game.pressed_once("right", k)) - int(game.pressed_once("left", k))
    if go:                                    # เปลี่ยนชิ้นเฉพาะเฟรมที่เพิ่งกด
        open_piece((view["idx"] + go) % len(PIECES))
        game.sfx("move")
        return True
    PIECES[view["idx"]][2](pieces[view["idx"]], k, game.pressed_once("a", k))
    return True

game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) make_bullets(4) -> make_bullets(2) : กด A รัวแล้วได้เสียง deny บ่อยขึ้น เพราะนัดว่างในพูลหมดไวขึ้น
# 2) ใน make_bullets เปลี่ยน radius=2 -> radius=6 และ border=game.WHITE -> game.PINK : กระสุนกลมมนขอบชมพู
# 3) ใน hud_update ย้ายทั้ง h["t"].set(...) และ h["draws"] += 1 ออกมานอก if : ตัวเลขเหมือนเดิม แต่ป้ายล่างจะบอกว่าวาดใหม่ทุกเฟรม

เอาภาพเดิมไปเล่าเรื่องใหม่

ภาพ ในเกมเดิม ในเกมของน้อง ในหน้าจอควบคุม
ship ยานผู้เล่น รถแข่ง · เรือ โดรนพ่นยา · รถ AGV
enemy enemy2 enemy3 ศัตรู 3 แบบ อุกกาบาต · ผี แมลงศัตรูพืช · ชิ้นงานเสีย 3 แบบ
boom1–boom6 ระเบิด เก็บของสำเร็จ ชิ้นงานถูกคัดทิ้ง
ball ลูกบอล อัญมณี ชิ้นงาน · พัสดุ
paddle ไม้ตี กำแพง ประตูคัดแยก · แขนกล
bird นกเงือก ตัวเอก มาสคอตหน้าเริ่มระบบ
snake_food อาหารงู ผลไม้โบนัส ผลผลิตในโรงเรือน

กฎ: ภาพเดิม ความหมายใหม่ — เขียนเรื่องที่ภาพเล่าก่อน แล้วค่อยเลือกภาพ

ลองเล่น · เอาภาพเดิมไปเล่าเรื่องใหม่

คัดลอกไฟล์ s13_13_reskin_farm.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอย 4 ทิศ = บินโดรน · A = พ่นยา (ละอองตกลงตรง ๆ)
  • แมลงคลานไปตามแถวผัก เจอผลไหนจะหยุดกัดราว 3.5 วินาที แล้วผลนั้นหายไป
  • พ่นโดนแมลง = ภาพระเบิดแวบหนึ่งแล้วหาย ตัวใหม่คลานเข้ามาแทน · กำจัดครบ = FARM SAFE!

ลองแก้ แล้วรันใหม่

  1. ENEMY = (...) -> ("bird", "bird", "bird") และ SIZE เป็น (44, 29) ทั้งสามช่อง : แมลงกลายเป็นนกมาจิกผลไม้ เรื่องใหม่ทันทีโดยไม่แตะตรรกะเลยสักบรรทัด
  2. BITE = 105 -> 30 : แมลงกินไวจนพ่นไม่ทัน ผลผลิตรอดน้อยลง (ในงานจริงค่าแบบนี้มาจากการวัดในแปลง)
  3. SPRAY_COOLDOWN = 6 -> 1 : กดค้างแล้วพ่นรัว แต่ละอองมีแค่ 3 ก้อน ครบ 3 ก้อนต้องรอก้อนแรกตกถึงดินก่อน

โค้ดเต็ม · s13_13_reskin_farm.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_13_reskin_farm.py — คาบ 13 (คาบพิเศษ): เอาภาพเดิมไปเล่าเรื่องใหม่ = โดรนพ่นยากันแมลงในแปลงผัก
#
# หลักการ   : ภาพเดิม ความหมายใหม่ (สไลด์ "เอาภาพเดิมไปเล่าเรื่องใหม่")
#             ship = โดรนพ่นยา · enemy / enemy2 / enemy3 = แมลงศัตรูพืช 3 แบบ · snake_food = ผลผลิต
#             boom1-boom6 = แมลงโดนยา · เสียง flap ของ Flappy = เสียงพ่นละออง
#             ตรรกะคือ Shooter เดิมทั้งหมด: ชุดหมุนเวียน (คาบ 11) · ชนแบบกล่อง (คาบ 4, 12)
#             คูลดาวน์ c = max(0, c - 1) (คาบ 11) · จำกัดขอบ x = max(lo, min(hi, x)) (คาบ 3)
# ลองเล่น   : กด Start · จอย 4 ทิศ = บินโดรน · A = พ่นยา (ละอองตกลงตรง ๆ)
#             แมลงคลานไปตามแถวผัก เจอผลไหนจะหยุดกัดราว 3.5 วินาที แล้วผลนั้นหายไป
#             พ่นโดนแมลง = ภาพระเบิดแวบหนึ่งแล้วหาย ตัวใหม่คลานเข้ามาแทน · กำจัดครบ = FARM SAFE!
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : Shooter (ยานยิงศัตรู · ระเบิด · ชุดหมุนเวียนของกระสุนและศัตรู)
# ในงานจริง : หน้าจอติดตามโดรนพ่นยาในแปลงเกษตร ตำแหน่งแมลงมาจากกล้องหรือกับดัก
#             ผลผลิตที่รอดคือตัวชี้วัดของรอบ · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : shooter_step3.py (คูลดาวน์การยิง) · shooter_full.py (pool + frame ของระเบิด)
# งบวิดเจ็ต : 23 ชิ้น (ทุ่ง 1 + ดิน 1 + ผัก 7 + แมลง 3 + ละออง 3 + โดรน 1 + ข้อความ HUD 3
#                    + ป้ายกลางจอ 1 + ข้อความในป้าย 2 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
CROPS = 7            # ผลผลิตในแถว
PESTS_TOTAL = 15     # แมลงทั้งรอบ
ON_SCREEN = 3        # แมลงบนจอพร้อมกันกี่ตัว (ขนาดชุดหมุนเวียน)
BOOM_FRAMES = 10     # ภาพระเบิดค้างกี่เฟรม
BITE = 105           # แมลงกัดผลหนึ่งผลนานกี่เฟรม (105 / 30 = 3.5 วินาที)
SPRAYS = 3           # ละอองในชุดหมุนเวียน
SPRAY_COOLDOWN = 6   # พ่นได้ทุก 6 เฟรม
SPRAY_SPEED = 7      # ละอองตกเฟรมละกี่ px
DRONE_SPEED = 6
ENEMY = ("enemy", "enemy2", "enemy3")
SIZE = ((16, 14), (21, 10), (18, 14))                       # ขนาดภาพแมลงแต่ละแบบ (px)
BOOM = (("boom1", "boom2"), ("boom3", "boom4"), ("boom5", "boom6"))
LANE_Y, CROP_Y, SOIL_Y = 290, 304, 300                      # แนวแมลงคลาน · ผัก · ดิน
CROP_X = [96 + i * 100 for i in range(CROPS)]               # มุมซ้ายของผักแต่ละต้น

# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def clamp(v, lo, hi):
    return max(lo, min(hi, v))

def crop_under(cx, alive):
    # ผักต้นไหนอยู่ใต้กลางตัวแมลงพอดี (ห่างไม่เกิน 1 px) และยังไม่ถูกกิน · ไม่มี = -1
    for i in range(len(alive)):
        if alive[i] and abs(cx - (CROP_X[i] + 8)) <= 1:
            return i
    return -1

def verdict(saved, pests_left):
    if saved == 0:      return "lose"
    if pests_left == 0: return "win"
    return ""

# ---- 3) ตรวจสมองก่อนเปิดจอ ----
def self_test():
    for got, want in ((clamp(800, 0, 730), 730), (crop_under(104, [True] * CROPS), 0),
                      (crop_under(104, [False] * CROPS), -1), (crop_under(150, [True] * CROPS), -1),
                      (verdict(0, 3), "lose"), (verdict(4, 0), "win"), (verdict(4, 2), "")):
        if got != want:
            print("ยังไม่ผ่าน: ได้", got, "ควรได้", want)
            return False
    print("ผ่าน! สมองของแปลงผักถูกทุกกรณี")
    return True

if not self_test():
    raise SystemExit

# ---- 4) หน้าจอ: สร้างครั้งเดียว (ทุ่งก่อน โดรนทีหลัง โดรนจึงบินทับทุกอย่าง) ----
game.title("FARM DRONE")
game.Box(0, 0, game.WIDTH, game.HEIGHT, 0x16301C)                   # ทุ่ง
game.Box(0, SOIL_Y, game.WIDTH, 34, 0x5A3A22)                       # แถวดิน
crops = [game.Sprite("snake_food", x, CROP_Y) for x in CROP_X]      # snake_food = ผลผลิต
pests = game.pool("enemy", ON_SCREEN)                                       # enemy = แมลง
sprays = [game.Box(-20, -20, 6, 12, game.CYAN, radius=3) for _ in range(SPRAYS)]
drone = game.Sprite("ship", 365, 120)                               # ship = โดรน
hud_saved = game.Text("", 12, 8, game.WHITE)
hud_pests = game.Text("", 640, 8, game.WHITE)
game.Text("ship = โดรน   enemy = แมลง   snake_food = ผลผลิต", 200, 40, 0x9FC9A0)
banner = game.Box(246, 150, 300, 84, 0x10122C, border=game.YELLOW, radius=8, border_w=3)
big = game.Text("", game.WIDTH // 2 - 50, 168, game.YELLOW)        # สองข้อความยาวเท่ากัน ใช้ที่เดียวได้
small = game.Text("", game.WIDTH // 2 - 70, 200, game.WHITE)

st, alive, shown = {}, [], {"saved": None, "pests": None}
n = ON_SCREEN
kind, state, timer, target, pdir, pspd = [0] * n, [""] * n, [0] * n, [0] * n, [1] * n, [1.0] * n

def spawn(i):
    # แมลงตัวใหม่คลานเข้ามาจากขอบซ้ายหรือขวา หน้าตาสุ่ม 1 ใน 3 แบบ (เปลี่ยนภาพด้วย .frame)
    kind[i], pdir[i] = random.randint(0, 2), random.choice((1, -1))
    pspd[i], state[i] = random.randint(6, 12) / 10, "walk"
    pests[i].frame(ENEMY[kind[i]])
    pests[i].w, pests[i].h = SIZE[kind[i]]
    pests[i].move_to(-20 if pdir[i] > 0 else game.WIDTH, LANE_Y)
    pests[i].show()

def show_banner(msg, sub):
    # ข่าวใหญ่กลางจอ: มีข้อความ = โชว์กรอบ · ข้อความว่าง = ซ่อน
    if msg: banner.show()
    else:   banner.hide()
    big.set(msg)
    small.set(sub)

def new_round():
    st.update({"saved": CROPS, "left": PESTS_TOTAL, "queue": PESTS_TOTAL - ON_SCREEN, "cool": 0, "tick": 0, "over": False})
    alive[:] = [True] * CROPS
    for c in crops: c.show()
    for i in range(ON_SCREEN): spawn(i)
    show_banner("", "")

def spray_step(s):
    # ละอองหนึ่งก้อน: ตกลง · โดนแมลง = แมลงเปลี่ยนภาพเป็นระเบิด · ใช้แล้วเก็บคืนชุด
    s.move_to(s.x, s.y + SPRAY_SPEED)
    used = s.y > SOIL_Y + 10
    for i in range(ON_SCREEN):
        if not used and state[i] in ("walk", "chew") and game.hit(s, pests[i]):
            state[i], timer[i] = "boom", BOOM_FRAMES
            pests[i].frame(BOOM[kind[i]][0])
            pests[i].move_to(pests[i].x - 6, LANE_Y - 6)            # ภาพระเบิด 29x24 ใหญ่กว่าแมลง จัดกลางใหม่
            st["left"] -= 1
            game.sfx("hit")
            used = True
    if used:
        s.move_to(-20, -20)
        s.hide()

def pest_step(i):
    p = pests[i]
    if state[i] == "walk":
        if (p.x < 0 and pdir[i] < 0) or (p.x > game.WIDTH - 20 and pdir[i] > 0):
            pdir[i] = -pdir[i]                                      # ถึงขอบแปลง กลับหลังหัน
        p.move_to(p.x + pdir[i] * pspd[i], LANE_Y + (st["tick"] // 6) % 2 * 2)   # คลานกระดึ๊บ
        c = crop_under(p.x + p.w / 2, alive)
        if c >= 0:
            state[i], timer[i], target[i] = "chew", BITE, c
    elif state[i] == "chew":
        timer[i] -= 1
        if timer[i] == 0:
            state[i] = "walk"
            if alive[target[i]]:
                alive[target[i]] = False
                crops[target[i]].hide()
                st["saved"] -= 1
                game.sfx("lose_life")
            p.move_to(p.x + pdir[i] * 3, LANE_Y)                    # ก้าวพ้นต้นที่กินแล้ว
    elif state[i] == "boom":
        timer[i] -= 1
        if timer[i] == BOOM_FRAMES // 2: p.frame(BOOM[kind[i]][1])                 # ครึ่งหลังเปลี่ยนเป็นเฟรมระเบิดวงใหญ่
        if timer[i] == 0:
            if st["queue"] > 0:                                     # ยังมีแมลงรอคิว = ตัวใหม่คลานเข้ามา
                st["queue"] -= 1
                spawn(i)
            else:                                                   # หมดคิว = ช่องนี้ว่างถาวร
                state[i] = "gone"
                p.hide()

new_round()

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    again = game.pressed_once("a", k)        # อ่านทุกเฟรม: กด A ค้างตอนจบรอบจะไม่นับเป็นการกดใหม่
    if st["over"]:
        if again: new_round()
        return True
    st["tick"] += 1
    dx, dy = (k.right - k.left) * DRONE_SPEED, (k.down - k.up) * DRONE_SPEED
    if dx or dy:                                                    # ขยับเฉพาะตอนกดจอย
        drone.move_to(clamp(drone.x + dx, 0, game.WIDTH - 62), clamp(drone.y + dy, 64, 220))
    st["cool"] = max(0, st["cool"] - 1)
    if k.a and st["cool"] == 0:
        for s in sprays:
            if s.y < 0:                                             # ก้อนที่ว่างอยู่ในชุด
                s.move_to(drone.x + 28, drone.y + 28)
                s.show()
                game.sfx("flap")
                st["cool"] = SPRAY_COOLDOWN
                break
    for s in sprays:
        if s.y >= 0: spray_step(s)
    for i in range(ON_SCREEN):
        pest_step(i)
    if st["saved"] != shown["saved"]:                               # HUD เขียนเฉพาะตอนค่าเปลี่ยน
        hud_saved.set("SAVED %d/%d" % (st["saved"], CROPS))
        shown["saved"] = st["saved"]
    if st["left"] != shown["pests"]:
        hud_pests.set("PESTS %d" % st["left"])
        shown["pests"] = st["left"]
    v = verdict(st["saved"], st["left"])
    if v:
        st["over"] = True
        show_banner("FARM SAFE!" if v == "win" else "CROPS LOST", "A = play again")
        game.sfx("win" if v == "win" else "gameover")
    return True

game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) ENEMY = (...) -> ("bird", "bird", "bird") และ SIZE เป็น (44, 29) ทั้งสามช่อง :
#    แมลงกลายเป็นนกมาจิกผลไม้ เรื่องใหม่ทันทีโดยไม่แตะตรรกะเลยสักบรรทัด
# 2) BITE = 105 -> 30 : แมลงกินไวจนพ่นไม่ทัน ผลผลิตรอดน้อยลง (ในงานจริงค่าแบบนี้มาจากการวัดในแปลง)
# 3) SPRAY_COOLDOWN = 6 -> 1 : กดค้างแล้วพ่นรัว แต่ละอองมีแค่ 3 ก้อน ครบ 3 ก้อนต้องรอก้อนแรกตกถึงดินก่อน

เสียง — ในเกมคือรางวัล ในโรงงานคือคำเตือน

ความหมาย เสียงสำเร็จรูป (game.sfx("…"))
ตอบรับการกด select move back deny start
ได้ eat point hit win pong_score
เสีย die fall lose lose_life gameover
การกระทำ fire turn flap wall paddle explode

f=440⋅2(n−69)/12(คาบ 8)game.tone(72, ms=150)  = โด สูงf = 440 \cdot 2^{(n - 69)/12} \quad \text{(คาบ 8)} \qquad \texttt{game.tone(72, ms=150)} \;\text{= โด สูง}

  • เกม: เสียงสั้น คม ทันทีที่เกิดเหตุ — ผู้เล่นรู้ผลโดยไม่ต้องมองจอ
  • โรงงาน: แยกระดับ — ด่วนมาก = ถี่ ซ้ำ จนมีคนกดรับทราบ · แจ้งให้ทราบ = ครั้งเดียว
  • ฟังครบทุกเสียง → Sound Explorer

กฎ: หนึ่งเสียง หนึ่งความหมาย และดัง เฉพาะตอนเกิดเหตุ

ลองเล่น · แผงเสียง 21 เสียง · ในเกมคือรางวัล ในโรงงานคือคำเตือน

คัดลอกไฟล์ s13_14_sound_board.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอย ขึ้น/ลง = เลือกกลุ่ม · ซ้าย/ขวา = เลือกเสียง (ชื่อในวงเล็บ) · A = เล่นเสียง
  • B = ทำนองสองโน้ต ดูเลข n กับความถี่ Hz ที่คำนวณจากสูตรขึ้นทีละโน้ต
  • X = จำลองสัญญาณเตือน สลับระดับทุกครั้งที่กด: ด่วน (ไฟแดงกะพริบ ดังถี่จนกด Y)
  • หรือ แจ้งทราบ (ไฟเหลือง ดังครั้งเดียว) · Y = รับทราบ

ลองแก้ แล้วรันใหม่

  1. JINGLE = (72, 79) -> (60, 64, 67, 72) : ทำนองยาวขึ้นเป็นสี่โน้ตเองโดยไม่ต้องแก้วงวน · ดูว่า n = 72 ได้ 523.3 Hz ซึ่งเป็นสองเท่าของ n = 60 (261.6 Hz) พอดี เพราะห่างกัน 12
  2. HIGH_EVERY = 6 -> 30 : เตือนด่วนดังแค่วินาทีละครั้ง ฟังแล้วยังรู้สึก "ด่วน" อยู่ไหม
  3. NOTE_GAP = 8 -> 2 : โน้ตถัดไปมาก่อนโน้ตแรกจบ เสียงแรกถูกตัด เพราะ game.tone() ไม่ต่อคิว

โค้ดเต็ม · s13_14_sound_board.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_14_sound_board.py — คาบ 13 (คาบพิเศษ): แผงเสียง 21 เสียง · ในเกมคือรางวัล ในโรงงานคือคำเตือน
#
# หลักการ   : หนึ่งเสียง หนึ่งความหมาย จัดเสียงเป็น 4 กลุ่ม ตอบรับ / ได้ / เสีย / กระทำ (สไลด์เรื่องเสียง)
#             ความถี่ของโน้ต MIDI  f = 440 * 2^((n - 69) / 12)   n = 69 คือ ลา 440 Hz   (คาบ 8)
#             ทำนองไม่ใช้ sleep: นับเฟรมแล้วเล่นโน้ตถัดไปเมื่อถึงจังหวะ  (เวลาคือการนับเฟรม คาบ 4, 11)
#             สัญญาณเตือนสองระดับ = เครื่องสถานะ ปกติ / ด่วน / แจ้งทราบ  (คาบ 2, 7)
# ลองเล่น   : กด Start · จอย ขึ้น/ลง = เลือกกลุ่ม · ซ้าย/ขวา = เลือกเสียง (ชื่อในวงเล็บ) · A = เล่นเสียง
#             B = ทำนองสองโน้ต ดูเลข n กับความถี่ Hz ที่คำนวณจากสูตรขึ้นทีละโน้ต
#             X = จำลองสัญญาณเตือน สลับระดับทุกครั้งที่กด: ด่วน (ไฟแดงกะพริบ ดังถี่จนกด Y)
#                 หรือ แจ้งทราบ (ไฟเหลือง ดังครั้งเดียว) · Y = รับทราบ
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : eat / point / win = รางวัล · die / lose_life / gameover = เสีย · fire / flap = การกระทำ
# ในงานจริง : เสียงเตือนหน้าเครื่องจักรแยกระดับ ด่วนมาก = ถี่ ซ้ำ จนมีคนกดรับทราบ · แจ้งให้ทราบ = ครั้งเดียว
#             ตัวเลขและสัญญาณเตือนทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s08_sound.py (sfx + tone) · s02_button_fsm.py (กดหนึ่งครั้ง = นับหนึ่งครั้ง)
# งบวิดเจ็ต : 16 ชิ้น (แถบเลือก 1 + ชื่อกลุ่ม 4 + แถวเสียง 4 + ข้อความ 5 + ไฟเตือน 1 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game
import math

# ---- 1) ตั้งค่า (แก้ได้) ----
GROUPS = [("ตอบรับ", ["select", "move", "back", "deny", "start"]),
          ("ได้",    ["eat", "point", "hit", "win", "pong_score"]),
          ("เสีย",   ["die", "fall", "lose", "lose_life", "gameover"]),
          ("กระทำ",  ["fire", "turn", "flap", "wall", "paddle", "explode"])]   # รวม 21 เสียง
JINGLE = (72, 79)     # โด สูง -> ซอล สูง (เลข MIDI)
NOTE_GAP = 8          # โน้ตถัดไปห่าง 8 เฟรม (ราว 0.27 วินาที)
HIGH_EVERY = 6        # เตือนด่วน: ดังทุก 6 เฟรม = 5 ครั้งต่อวินาที
HIGH_NOTE, LOW_NOTE = 88, 76
ROW_Y = [62, 100, 138, 176]
GRAY, DARK_RED = 0x556070, 0x5C1F1F

# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def midi_hz(n):
    return 440 * math.pow(2, (n - 69) / 12)     # ขึ้น 12 = ความถี่คูณ 2 (สูงขึ้นหนึ่งช่วงคู่แปด)

def row_text(names, sel):
    # ชื่อที่เลือกอยู่ในวงเล็บ ชื่ออื่นเว้นช่องเท่ากัน ตัวหนังสือจึงไม่ขยับไปมา
    return " ".join(("[%s]" if i == sel else " %s ") % n for i, n in enumerate(names))

def alarm_beep(level, t, every):
    # ด่วน = ดังทุก every เฟรม ไม่หยุดเอง · แจ้งทราบ = ดังเฉพาะเฟรมแรก · ปกติ = เงียบ
    if level == "HIGH": return t % every == 0
    if level == "LOW":  return t == 0
    return False

# ---- 3) ตรวจสมองก่อนเปิดจอ ----
def self_test():
    for got, want in ((round(midi_hz(69), 2), 440.0), (round(midi_hz(72), 2), 523.25), (round(midi_hz(81), 2), 880.0),
                      (row_text(["a", "b"], 1), " a  [b]"), (alarm_beep("HIGH", 12, 6), True),
                      (alarm_beep("HIGH", 13, 6), False), (alarm_beep("LOW", 0, 6), True), (alarm_beep("LOW", 6, 6), False)):
        if got != want:
            print("ยังไม่ผ่าน: ได้", got, "ควรได้", want)
            return False
    print("ผ่าน! สมองของแผงเสียงถูกทุกกรณี")
    return True

if not self_test():
    raise SystemExit

# ---- 4) หน้าจอ: สร้างครั้งเดียว (แถบเลือกก่อน ตัวหนังสือทีหลัง จะได้อยู่บนแถบ) ----
game.title("SOUND BOARD")
# ข้อความหนึ่งป้ายยาวได้ไม่เกิน 95 ไบต์ตอนสร้าง (ไทยตัวละ 3 ไบต์) ยาวกว่านั้นบอร์ดตัดท้ายทิ้ง
game.Text("A = เล่น   B = ทำนอง   X = เตือน   Y = รับทราบ", 12, 12, game.CYAN)
bar = game.Box(8, ROW_Y[0] - 6, 776, 34, 0x2A3340, radius=6)
for r in range(4):
    game.Text(GROUPS[r][0], 20, ROW_Y[r], 0x8899AA)
rows = [game.Text("", 110, ROW_Y[r], game.WHITE) for r in range(4)]
game.Text("f = 440 * 2^((n - 69) / 12)", 20, 230, game.YELLOW)
hud_note = game.Text("B = ทำนอง 2 โน้ต", 20, 262, game.WHITE)
lamp = game.Box(470, 226, 44, 44, GRAY, border=0x8899AA, radius=22, border_w=2)
hud_alarm = game.Text("NORMAL", 530, 230, game.WHITE)
hud_next = game.Text("", 530, 262, 0x8899AA)

st = {"r": 0, "c": 0, "flash": 0, "jingle": -1, "level": "", "t": 0, "next": "HIGH", "lit": False}

def paint_rows():
    for r in range(4):
        rows[r].set(row_text(GROUPS[r][1], st["c"] if r == st["r"] else -1))
    bar.move_to(8, ROW_Y[st["r"]] - 6)

def set_alarm(level):
    st["level"], st["t"] = level, 0
    lamp.set_color({"HIGH": game.RED, "LOW": game.YELLOW}.get(level, GRAY))
    hud_alarm.set({"HIGH": "HIGH ALARM - press Y", "LOW": "LOW: notify once"}.get(level, "NORMAL"))
    hud_next.set("X = raise %s" % st["next"])

paint_rows()
set_alarm("")

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    dr = game.pressed_once("down", k) - game.pressed_once("up", k)      # +1 / 0 / -1
    dc = game.pressed_once("right", k) - game.pressed_once("left", k)
    if dr or dc:                                              # วาดแถวใหม่เฉพาะตอนเลื่อน
        st["r"] = max(0, min(3, st["r"] + dr))                # ขึ้นลงมีขอบ
        n = len(GROUPS[st["r"]][1])
        st["c"] = (min(st["c"], n - 1) + dc) % n              # ซ้ายขวาวนรอบในแถว
        paint_rows()
    if game.pressed_once("a", k):
        game.sfx(GROUPS[st["r"]][1][st["c"]])
        bar.set_color(0x3E6A9A)                               # แถบสว่างแวบ 6 เฟรม = รู้ว่ากดติด
        st["flash"] = 6
    if st["flash"] > 0:
        st["flash"] -= 1
        if st["flash"] == 0: bar.set_color(0x2A3340)

    if game.pressed_once("b", k):
        st["jingle"] = 0                                      # เริ่มนับเฟรมของทำนอง
    if st["jingle"] >= 0:
        if st["jingle"] % NOTE_GAP == 0:                      # ถึงจังหวะโน้ตถัดไป
            i = st["jingle"] // NOTE_GAP
            game.tone(JINGLE[i], ms=220)
            hud_note.set("note %d/%d   n = %d  ->  f = %.1f Hz" % (i + 1, len(JINGLE), JINGLE[i], midi_hz(JINGLE[i])))
        st["jingle"] += 1
        if st["jingle"] >= len(JINGLE) * NOTE_GAP:
            st["jingle"] = -1                                 # ครบทุกโน้ต หยุดนับ

    if game.pressed_once("x", k):                             # ยกสัญญาณเตือน สลับระดับทุกครั้ง
        level = st["next"]
        st["next"] = "LOW" if level == "HIGH" else "HIGH"
        set_alarm(level)
    if game.pressed_once("y", k) and st["level"]:             # รับทราบ = เงียบ ไฟกลับเทา
        game.sfx("select")
        set_alarm("")
    if alarm_beep(st["level"], st["t"], HIGH_EVERY):
        game.tone(HIGH_NOTE if st["level"] == "HIGH" else LOW_NOTE, ms=90)
        if st["level"] == "HIGH":                             # ไฟแดงกะพริบตามจังหวะเสียง
            st["lit"] = not st["lit"]
            lamp.set_color(game.RED if st["lit"] else DARK_RED)
    st["t"] += 1
    return True

game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) JINGLE = (72, 79) -> (60, 64, 67, 72) : ทำนองยาวขึ้นเป็นสี่โน้ตเองโดยไม่ต้องแก้วงวน · ดูว่า n = 72 ได้ 523.3 Hz
#    ซึ่งเป็นสองเท่าของ n = 60 (261.6 Hz) พอดี เพราะห่างกัน 12
# 2) HIGH_EVERY = 6 -> 30 : เตือนด่วนดังแค่วินาทีละครั้ง ฟังแล้วยังรู้สึก "ด่วน" อยู่ไหม
# 3) NOTE_GAP = 8 -> 2 : โน้ตถัดไปมาก่อนโน้ตแรกจบ เสียงแรกถูกตัด เพราะ game.tone() ไม่ต่อคิว

ลองเล่น · สูตรเสียงประกอบจากเกมเต็มของจริง เหตุการณ์ไหนใช้เสียงไหน + ทำนองแบบไม่ใช้ sleep

คัดลอกไฟล์ s13_19_kit_sounds.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start (ฟังทำนองเปิดเกม melody=True) · ซ้าย/ขวา = เปลี่ยนเกม · UP/DOWN = เลือกแถว · A = เล่น
  • ป้ายล่างบอกว่ากำลังเล่นอะไร มาจากบรรทัดไหนของเกมเต็ม · หน้าสุดท้าย MELODY ดูไฟวิ่งตามระดับโน้ต

ลองแก้ แล้วรันใหม่

  1. COIN = ((83, 60, 1), (88, 160, 1)) -> ((83, 60, 6), (88, 160, 1)) : เว้นช่วงยาวขึ้น ฟังเป็นสองเสียงแยกกัน ไม่ใช่เหรียญ
  2. TEAM เปลี่ยนเป็น ((72, 110, 4), (67, 110, 4), (64, 110, 4), (60, 200, 4)) : ทำนองไล่ลง ฟังเหมือนเสียงแพ้แทนเสียงชนะ
  3. ใน on_frame ใส่ game.tone(60) ทุกเฟรม (นอก if) : เสียงอื่นโดนทับจนฟังไม่ออก — หนึ่งเสียงต้องดังเฉพาะตอนเกิดเหตุ

หยิบไปใช้ · ตารางเหตุการณ์ -> เสียง ของเกมเต็มทั้ง 4 เกม (อ่านจากโค้ดจริง เลขบรรทัดอยู่ท้ายแถว)

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก เกมตัวเต็ม · ไฟล์ตัวอย่าง s13_19_kit_sounds.py↗
# ===== หยิบไปใช้: ตารางเหตุการณ์ -> เสียง ของเกมเต็มทั้ง 4 เกม (อ่านจากโค้ดจริง เลขบรรทัดอยู่ท้ายแถว) =====
# (ชื่อเสียงสำหรับ game.sfx, เหตุการณ์ในเกม, ไฟล์:บรรทัด ที่เรียกเสียงนั้น)
SOUND_TABLES = (
    ("SHOOTER", (("fire", "ยิงกระสุน", "shooter_full.py:142"),
                 ("explode", "ระเบิด ทุกครั้งที่บึ้ม", "shooter_full.py:152"),
                 ("hit", "ยิงโดนศัตรู (ซ้อนเสียงระเบิด)", "shooter_full.py:242"),
                 ("lose_life", "ศัตรูชนยาน ยังรอด", "shooter_full.py:259"),
                 ("gameover", "ชีวิตหมด", "shooter_full.py:229, 256"))),
    ("FLAPPY", (("flap", "กระพือปีก", "flappy_full.py:177"),
                ("point", "บินผ่านท่อ ได้ 1 คะแนน", "flappy_full.py:213"),
                ("fall", "ตกพื้น หรือชนท่อ", "flappy_full.py:197, 221"))),
    ("PONG", (("wall", "ลูกเด้งขอบบน/ล่าง", "pong_full.py:101, 105"),
              ("paddle", "ตีโดนไม้", "pong_full.py:115, 122"),
              ("pong_score", "ได้แต้ม แมตช์ยังไม่จบ", "pong_full.py:134, 142"),
              ("win", "ผู้เล่นชนะแมตช์", "pong_full.py:140"),
              ("lose", "AI ชนะแมตช์", "pong_full.py:132"))),
    ("SNAKE", (("eat", "กินแอปเปิล", "snake_full.py:127"),
               ("COIN", "ทุกลูกที่ 10 เสียงเหรียญ 2 โน้ต", "snake_full.py:128-129"),
               ("point", "ทุกลูกที่ 10 (ฉบับสไปรต์)", "snake_sprite_full.py:132-133"),
               ("die", "ชนกำแพง", "snake_full.py:119"),
               ("lose", "กัดตัวเอง (คนละเสียงกับชนกำแพง)", "snake_full.py:121"))),
    ("SYSTEM", (("start", "กด Start หน้าเริ่ม / เล่นใหม่", "bentogame.py:915, 1072"),
                ("select", "พักเกม / ยืนยันเมนู", "bentogame.py:991, 1154"),
                ("move", "เลื่อนเมนูขึ้นลง", "bentogame.py:1147, 1151"),
                ("back", "กด Back ในเมนู", "bentogame.py:1142"))),
)
# ===== จบชิ้น =====

หยิบไปใช้ · เครื่องเล่นทำนองแบบนับเฟรม ไม่มี sleep (แทน game.jingle ที่รอด้วย sleep — bentogame.py:869-877)

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก เกมตัวเต็ม · ไฟล์ตัวอย่าง s13_19_kit_sounds.py↗
# ===== หยิบไปใช้: เครื่องเล่นทำนองแบบนับเฟรม ไม่มี sleep (แทน game.jingle ที่รอด้วย sleep — bentogame.py:869-877) =====
# ทำนอง = ลำดับขั้น (โน้ต MIDI, ยาวกี่ ms, อีกกี่เฟรมค่อยเล่นขั้นถัดไป) · โน้ต 0 = เว้นจังหวะ
def make_player():
    return {"steps": (), "i": 0, "wait": 0}

def play(pl, steps):                          # เริ่มทำนองใหม่ (ทับทำนองเดิมที่ยังเล่นไม่จบ)
    pl["steps"], pl["i"], pl["wait"] = steps, 0, 0

def player_step(pl):                          # เรียกทุกเฟรม คืนโน้ตที่เพิ่งเริ่ม (0 = เว้น) หรือ None ถ้ายังไม่ถึงคิว
    if pl["i"] >= len(pl["steps"]):
        return None
    if pl["wait"] > 0:                        # ยังไม่ถึงคิว: นับถอยหลังแล้วกลับไปทำงานอื่นก่อน
        pl["wait"] -= 1
        return None
    note, ms, frames = pl["steps"][pl["i"]]
    if note:
        game.tone(note, ms=ms)
    pl["i"] += 1
    pl["wait"] = frames - 1
    return note

def jingle_steps(notes, ms=90, frames=3):     # แปลงโน้ตแบบ game.jingle(...) เป็นขั้นของเครื่องเล่นนี้
    return tuple((n, ms, frames) for n in notes)
# ===== จบชิ้น =====

หยิบไปใช้ · สูตรทำนองจากเกมเต็ม

วางไว้บนสุดของไฟล์เกมของน้อง แล้วเรียกใช้ได้เลย · ตัดมาจาก full_games/snake_full.py:32-37 · bentogame.py:869-871, 907-908 · ไฟล์ตัวอย่าง s13_19_kit_sounds.py↗
# ===== หยิบไปใช้: สูตรทำนองจากเกมเต็ม (จาก full_games/snake_full.py:32-37 · bentogame.py:869-871, 907-908) =====
# ตัวเลขเฟรมคิดที่ 30 fps (1 เฟรม = 33 ms) ใช้คู่กับเครื่องเล่นทำนองกล่องก่อนหน้า
COIN = ((83, 60, 1), (88, 160, 1))            # เหรียญ cha-ching: B5 สั้น แล้ว E6 ยาว (เกมเต็มเว้น 45 ms ด้วย sleep)
TITLE = ((60, 90, 3), (64, 90, 3), (67, 90, 3), (72, 90, 3))   # โด มี ซอล โดสูง = game.title(..., melody=True)
TEAM = ((64, 110, 4), (0, 110, 4), (64, 110, 4), (67, 110, 4), (72, 110, 4))   # ตัวอย่างในคู่มือ game.jingle
# ===== จบชิ้น =====

โค้ดเต็ม · s13_19_kit_sounds.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_19_kit_sounds.py — คาบ 13 (คาบพิเศษ): สูตรเสียงประกอบจากเกมเต็มของจริง เหตุการณ์ไหนใช้เสียงไหน + ทำนองแบบไม่ใช้ sleep
#
# หลักการ   : หนึ่งเสียง หนึ่งความหมาย ดังเฉพาะตอนเกิดเหตุ — game.sfx("ชื่อ") ตรงบรรทัดที่เหตุเกิด    (คาบ 8)
#             โน้ตเอง game.tone(โน้ต MIDI, ms=...) · 60 = โดกลาง · f = 440 * 2^((n - 69)/12)          (คาบ 8)
#             tone() ไม่ต่อคิว และห้าม sleep ในวงวน — ทำนองจึงใช้ตัวนับเฟรม: ถึงคิวค่อยเล่นโน้ตถัดไป   (คาบ 4, 11)
# ลองเล่น   : กด Start (ฟังทำนองเปิดเกม melody=True) · ซ้าย/ขวา = เปลี่ยนเกม · UP/DOWN = เลือกแถว · A = เล่น
#             ป้ายล่างบอกว่ากำลังเล่นอะไร มาจากบรรทัดไหนของเกมเต็ม · หน้าสุดท้าย MELODY ดูไฟวิ่งตามระดับโน้ต
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : ตารางทั้งหมดอ่านจากโค้ดจริงของ shooter_full · flappy_full · pong_full · snake_full · bentogame
# ในงานจริง : เสียงเตือนของเครื่องจักรก็ใช้กฎเดียวกัน — ด่วนมาก = ถี่ ซ้ำ จนมีคนรับทราบ · แจ้งให้ทราบ = ครั้งเดียว
#             ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : s08_sound.py · full_games/*.py · bentogame.py (sfx, tone, jingle, title)
# งบวิดเจ็ต : 13 ชิ้น (พื้นหลัง 1 + แถบเลือก 1 + หัวข้อ 1 + แถว 6 + ป้ายกำลังเล่น 1 + ป้ายปุ่ม 1 + ไฟโน้ต 1 + game.run 1)
#
# วิธีหยิบไปใช้: ตารางเสียง = อ่านแล้วใส่ game.sfx(...) ในเกมของน้องตรงเหตุการณ์เดียวกัน
#   เครื่องเล่นทำนอง + สูตรทำนอง = คัดลอกทั้งสองกล่อง สร้าง make_player() ครั้งเดียว เรียก player_step() ทุกเฟรม
import bentogame as game

# ---- 1) ตั้งค่า (แก้ได้) ----
FPS = 30
ROWS_Y, ROW_H = 60, 34                        # แถวแรกอยู่ที่ y = 60 ห่างกันแถวละ 34 px

# ---- 2) สมอง + ชิ้นสำเร็จรูป (หยิบไปใช้ได้ทีละกล่อง) ----
# ===== หยิบไปใช้: ตารางเหตุการณ์ -> เสียง ของเกมเต็มทั้ง 4 เกม (อ่านจากโค้ดจริง เลขบรรทัดอยู่ท้ายแถว) =====
# (ชื่อเสียงสำหรับ game.sfx, เหตุการณ์ในเกม, ไฟล์:บรรทัด ที่เรียกเสียงนั้น)
SOUND_TABLES = (
    ("SHOOTER", (("fire", "ยิงกระสุน", "shooter_full.py:142"),
                 ("explode", "ระเบิด ทุกครั้งที่บึ้ม", "shooter_full.py:152"),
                 ("hit", "ยิงโดนศัตรู (ซ้อนเสียงระเบิด)", "shooter_full.py:242"),
                 ("lose_life", "ศัตรูชนยาน ยังรอด", "shooter_full.py:259"),
                 ("gameover", "ชีวิตหมด", "shooter_full.py:229, 256"))),
    ("FLAPPY", (("flap", "กระพือปีก", "flappy_full.py:177"),
                ("point", "บินผ่านท่อ ได้ 1 คะแนน", "flappy_full.py:213"),
                ("fall", "ตกพื้น หรือชนท่อ", "flappy_full.py:197, 221"))),
    ("PONG", (("wall", "ลูกเด้งขอบบน/ล่าง", "pong_full.py:101, 105"),
              ("paddle", "ตีโดนไม้", "pong_full.py:115, 122"),
              ("pong_score", "ได้แต้ม แมตช์ยังไม่จบ", "pong_full.py:134, 142"),
              ("win", "ผู้เล่นชนะแมตช์", "pong_full.py:140"),
              ("lose", "AI ชนะแมตช์", "pong_full.py:132"))),
    ("SNAKE", (("eat", "กินแอปเปิล", "snake_full.py:127"),
               ("COIN", "ทุกลูกที่ 10 เสียงเหรียญ 2 โน้ต", "snake_full.py:128-129"),
               ("point", "ทุกลูกที่ 10 (ฉบับสไปรต์)", "snake_sprite_full.py:132-133"),
               ("die", "ชนกำแพง", "snake_full.py:119"),
               ("lose", "กัดตัวเอง (คนละเสียงกับชนกำแพง)", "snake_full.py:121"))),
    ("SYSTEM", (("start", "กด Start หน้าเริ่ม / เล่นใหม่", "bentogame.py:915, 1072"),
                ("select", "พักเกม / ยืนยันเมนู", "bentogame.py:991, 1154"),
                ("move", "เลื่อนเมนูขึ้นลง", "bentogame.py:1147, 1151"),
                ("back", "กด Back ในเมนู", "bentogame.py:1142"))),
)
# ===== จบชิ้น =====

# ===== หยิบไปใช้: เครื่องเล่นทำนองแบบนับเฟรม ไม่มี sleep (แทน game.jingle ที่รอด้วย sleep — bentogame.py:869-877) =====
# ทำนอง = ลำดับขั้น (โน้ต MIDI, ยาวกี่ ms, อีกกี่เฟรมค่อยเล่นขั้นถัดไป) · โน้ต 0 = เว้นจังหวะ
def make_player():
    return {"steps": (), "i": 0, "wait": 0}

def play(pl, steps):                          # เริ่มทำนองใหม่ (ทับทำนองเดิมที่ยังเล่นไม่จบ)
    pl["steps"], pl["i"], pl["wait"] = steps, 0, 0

def player_step(pl):                          # เรียกทุกเฟรม คืนโน้ตที่เพิ่งเริ่ม (0 = เว้น) หรือ None ถ้ายังไม่ถึงคิว
    if pl["i"] >= len(pl["steps"]):
        return None
    if pl["wait"] > 0:                        # ยังไม่ถึงคิว: นับถอยหลังแล้วกลับไปทำงานอื่นก่อน
        pl["wait"] -= 1
        return None
    note, ms, frames = pl["steps"][pl["i"]]
    if note:
        game.tone(note, ms=ms)
    pl["i"] += 1
    pl["wait"] = frames - 1
    return note

def jingle_steps(notes, ms=90, frames=3):     # แปลงโน้ตแบบ game.jingle(...) เป็นขั้นของเครื่องเล่นนี้
    return tuple((n, ms, frames) for n in notes)
# ===== จบชิ้น =====

# ===== หยิบไปใช้: สูตรทำนองจากเกมเต็ม (จาก full_games/snake_full.py:32-37 · bentogame.py:869-871, 907-908) =====
# ตัวเลขเฟรมคิดที่ 30 fps (1 เฟรม = 33 ms) ใช้คู่กับเครื่องเล่นทำนองกล่องก่อนหน้า
COIN = ((83, 60, 1), (88, 160, 1))            # เหรียญ cha-ching: B5 สั้น แล้ว E6 ยาว (เกมเต็มเว้น 45 ms ด้วย sleep)
TITLE = ((60, 90, 3), (64, 90, 3), (67, 90, 3), (72, 90, 3))   # โด มี ซอล โดสูง = game.title(..., melody=True)
TEAM = ((64, 110, 4), (0, 110, 4), (64, 110, 4), (67, 110, 4), (72, 110, 4))   # ตัวอย่างในคู่มือ game.jingle
# ===== จบชิ้น =====


# ---- 4) หน้าจอ: สร้างครั้งเดียว เปลี่ยนหน้า = เขียนข้อความใหม่ลงแถวเดิม ----
PAGES = SOUND_TABLES + (("MELODY", (("TITLE", "ทำนองเปิดเกม (ทุกเกมเต็มใช้)", "bentogame.py:869, 907"),
                                    ("COIN", "เหรียญ 2 โน้ตของ snake_full", "snake_full.py:32-37"),
                                    ("TEAM", "ทำนองของทีม แต่งเองได้", "bentogame.py:871"))),)
RECIPES = {"TITLE": TITLE, "COIN": COIN, "TEAM": TEAM}

game.title("KIT SOUNDS", melody=True)         # ทำนองเปิดเกมใช้ sleep ได้ เพราะยังไม่เข้าวงวนเกม
game.background(0x10122C)
bar = game.Box(20, ROWS_Y - 5, 752, ROW_H - 4, 0x24406A, border=0x80F4FF, radius=6)   # แถบบอกแถวที่เลือก
header = game.Text("", 24, 16, game.YELLOW)
rows = [game.Text("", 36, ROWS_Y + i * ROW_H, game.WHITE) for i in range(6)]
now = game.Text("", 24, 290, game.CYAN)
game.Text("ซ้าย/ขวา = เกมอื่น   UP/DOWN = เลือก   A = เล่น", 24, 330, game.WHITE)
lamp = game.Box(700, 286, 24, 24, 0x334455, radius=12)   # ไฟโน้ต: ติดเมื่อมีเสียง วิ่งตามระดับโน้ต
player = make_player()
view = {"page": 0, "sel": 0, "last": None, "lamp": 0, "name": ""}

def show_page():
    name, table = PAGES[view["page"]]
    header.set("%d/%d  %s   เหตุการณ์ -> เสียง" % (view["page"] + 1, len(PAGES), name))
    for i, row in enumerate(rows):
        row.set("%-11s %s" % (table[i][0], table[i][1]) if i < len(table) else "")
    view["sel"] = min(view["sel"], len(table) - 1)
    bar.move_to(20, ROWS_Y - 5 + view["sel"] * ROW_H)

def say(msg):                                 # เขียนป้ายเฉพาะตอนข้อความเปลี่ยน
    if msg != view["last"]:
        now.set(msg)
        view["last"] = msg

def light(x):                                 # ไฟโน้ตติด 6 เฟรม แล้วดับเอง (นับเฟรม ไม่ใช้ sleep)
    if view["lamp"] == 0:
        lamp.set_color(game.YELLOW)
    lamp.move_to(x, 286)
    view["lamp"] = 6

show_page()
say("เลือกแถวแล้วกด A")

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    go = int(game.pressed_once("right", k)) - int(game.pressed_once("left", k))
    step = int(game.pressed_once("down", k)) - int(game.pressed_once("up", k))
    table = PAGES[view["page"]][1]
    if go:
        view["page"] = (view["page"] + go) % len(PAGES)
        show_page()
        game.sfx("move")
    elif step:
        view["sel"] = (view["sel"] + step) % len(table)
        bar.move_to(20, ROWS_Y - 5 + view["sel"] * ROW_H)
    if game.pressed_once("a", k):
        name, _, src = table[view["sel"]]
        view["name"] = name
        if name in RECIPES:                   # ทำนอง: ส่งให้เครื่องเล่น แล้วมันเล่นทีละโน้ตตามเฟรม
            play(player, RECIPES[name])
        else:                                 # เสียงสำเร็จรูป: เรียกครั้งเดียวจบ
            game.sfx(name)
            light(560)
            say("กำลังเล่น: %s   <- %s" % (name, src))
    note = player_step(player)
    if note is not None:                      # ทำนองเพิ่งขึ้นโน้ตใหม่: ไฟวิ่งตามระดับเสียง
        if note:
            light(420 + (note - 60) * 10)
        say("กำลังเล่น: %s  โน้ต %d/%d  (%d)" % (view["name"], player["i"], len(player["steps"]), note))
    if view["lamp"] > 0:
        view["lamp"] -= 1
        if view["lamp"] == 0:
            lamp.set_color(0x334455)
    return True

game.run(on_frame, fps=FPS)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) COIN = ((83, 60, 1), (88, 160, 1)) -> ((83, 60, 6), (88, 160, 1)) : เว้นช่วงยาวขึ้น ฟังเป็นสองเสียงแยกกัน ไม่ใช่เหรียญ
# 2) TEAM เปลี่ยนเป็น ((72, 110, 4), (67, 110, 4), (64, 110, 4), (60, 200, 4)) : ทำนองไล่ลง ฟังเหมือนเสียงแพ้แทนเสียงชนะ
# 3) ใน on_frame ใส่ game.tone(60) ทุกเฟรม (นอก if) : เสียงอื่นโดนทับจนฟังไม่ออก — หนึ่งเสียงต้องดังเฉพาะตอนเกิดเหตุ

ความสนุกที่มาจากคณิตศาสตร์ที่เรียนแล้ว

ลูกเล่น ทำยังไง สมการ (คาบ)
ยิ่งเก่งยิ่งยาก ความเร็วศัตรูเพิ่มตามคะแนน แต่มีเพดาน min⁡(BASE+score//5,MAX)\min(\text{BASE} + \text{score} // 5, \text{MAX}) (3)
โดนแล้วกะพริบ set_color(game.RED) แล้วนับถอยหลัง 6 เฟรมก่อนกลับสีเดิม c=max⁡(0,c−1)c = \max(0, c-1) (11)
ระเบิดเป็นภาพ ย้าย boom1 ไปจุดที่โดน แสดง 10 เฟรมแล้วซ่อน นับเฟรม (4)
รู้สึกมีน้ำหนัก เร่งแล้วไหลต่อ ไม่หยุดทันที v×=fv \mathrel{\times}= f (6)
ตีแล้วเบี่ยงมุม ตำแหน่งที่ลูกกระทบไม้กำหนดมุมเด้ง vy+=2⋅offsetv_y \mathrel{+}= 2 \cdot \text{offset} (7)
บอสมีแถบพลัง กล่องเล็ก 5 ก้อน ซ่อนทีละก้อนเมื่อโดน ตัวนับ (12)
  • อย่าย่อ-ขยายกล่องทุกเฟรม จอจะเป็นรอย ใช้ซ่อน-แสดงแทน
  • game.tone() ไม่ต่อคิว ถ้าใส่ sleep ในวงวน เกมจะค้างตาม

กฎ: ความสนุกไม่ได้มาจากของเยอะ แต่มาจาก ผลตอบกลับที่ทันที และชัด

ลองเล่น · ความสนุกที่มาจากคณิตศาสตร์ที่เรียนแล้ว (game juice)

คัดลอกไฟล์ s13_15_game_juice.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · จอยซ้าย/ขวา = ขับยาน · กด A ค้าง = ยิงต่อเนื่อง (มีคูลดาวน์)
  • ยิงศัตรูที่ตกลงมา +1 แต้ม · ยิงบอส (กล่องม่วงด้านบน) ดูบอสกะพริบขาวและแถบพลังขวาบนหายทีละก้อน
  • บอสพลังหมด +5 แต้ม · ดูบรรทัด SPEED กลางบน ตัวเลขขึ้นทุก 5 แต้ม แล้วหยุดที่เพดาน

ลองแก้ แล้วรันใหม่

  1. MAX_SPEED = 6 -> 99 : ไม่มีเพดาน ยิ่งเก่งยิ่งเร็วไม่หยุด จนศัตรูก้าวต่อเฟรมยาวกว่าตัวกระสุนกับตัวมันรวมกัน บางนัดจึงทะลุผ่านไปเฉย ๆ ทั้งที่เล็งตรง — เพดานคือสิ่งที่ทำให้เกมยังเล่นได้
  2. FLASH_FRAMES = 6 -> 1 : กะพริบแวบเดียวแทบไม่เห็น · -> 30 : ขาวค้างนานจนไม่รู้ว่าโดนครั้งไหน
  3. ลบบรรทัด boss.set_color(game.WHITE) ออก : ยิงโดนบอสแล้วรู้สึก "ไม่โดน" ทั้งที่แถบพลังลด ลองเทียบความรู้สึกดู

โค้ดเต็ม · s13_15_game_juice.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_15_game_juice.py — คาบ 13 (คาบพิเศษ): ความสนุกที่มาจากคณิตศาสตร์ที่เรียนแล้ว (game juice)
#
# หลักการ   : ลูกเล่นทุกอย่างในไฟล์นี้ใช้แค่สมการที่เรียนแล้ว ไม่มีของใหม่เลย
#             ยิ่งเก่งยิ่งยาก   speed = min(BASE + score // 5, MAX)   ทุก 5 แต้มเร็วขึ้น 1 แต่มีเพดาน  (คาบ 3)
#             โดนแล้วกะพริบ    set_color(ขาว) แล้วนับถอยหลัง c = max(0, c - 1) ครบ 6 เฟรมคืนสีเดิม  (คาบ 11)
#             ระเบิดเป็นภาพ    ย้าย boom ไปจุดที่โดน แสดง 10 เฟรม ครึ่งแรก boom1 ครึ่งหลัง boom2  (คาบ 4)
#             บอสมีแถบพลัง     กล่องเล็ก 5 ก้อน โดนหนึ่งครั้งซ่อนหนึ่งก้อน ไม่ย่อขยายกล่อง  (ตัวนับ คาบ 12)
# ลองเล่น   : กด Start · จอยซ้าย/ขวา = ขับยาน · กด A ค้าง = ยิงต่อเนื่อง (มีคูลดาวน์)
#             ยิงศัตรูที่ตกลงมา +1 แต้ม · ยิงบอส (กล่องม่วงด้านบน) ดูบอสกะพริบขาวและแถบพลังขวาบนหายทีละก้อน
#             บอสพลังหมด +5 แต้ม · ดูบรรทัด SPEED กลางบน ตัวเลขขึ้นทุก 5 แต้ม แล้วหยุดที่เพดาน
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : Shooter ตัวเต็ม (ระเบิด 2 เฟรม · คูลดาวน์การยิง · ความยากเพิ่มตามคะแนน)
# ในงานจริง : ผลตอบกลับที่ทันทีและชัดใช้กับหน้าจอควบคุมด้วย กดปุ่มแล้วปุ่มกะพริบตอบ · ค่าที่เพิ่มตามเวลาต้องมีเพดาน
#             แถบความจุถังหรือแบตเตอรี่แบบเป็นก้อน ซ่อนทีละก้อน · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และ BENTO Emulator
# ต่อยอดจาก : shooter_step3.py (คูลดาวน์) · shooter_full.py (ระเบิด 2 เฟรม) · s03_box_joystick.py (min กับเพดาน)
# งบวิดเจ็ต : 21 ชิ้น (อวกาศ 1 + ศัตรู 3 + บอส 2 + กระสุน 3 + ยาน 1 + ระเบิด 2 + แถบพลัง 5
#                    + ข้อความ 3 + game.run 1)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
BASE_SPEED, MAX_SPEED = 2, 6   # ความเร็วศัตรูตอนเริ่ม และเพดาน (px ต่อเฟรม)
FLASH_FRAMES = 6               # บอสกะพริบขาวกี่เฟรม
BOOM_FRAMES = 10               # ระเบิดค้างกี่เฟรม
BOSS_HP = 5                    # แถบพลังบอส 5 ก้อน
BOSS_RESPAWN = 60              # บอสตัวใหม่มาหลังตัวเก่าแตก 60 เฟรม (2 วินาที)
ENEMIES, BULLETS, BOOMS = 3, 3, 2   # ขนาดชุดหมุนเวียน: ของที่ขยับพร้อมกันน้อย = ลื่นบนบอร์ด
FIRE_COOLDOWN = 6
BULLET_SPEED = 10
SHIP_SPEED = 8
SHIP_Y, BOSS_Y = 330, 60
BOSS_W, BOSS_H = 110, 34
BOSS_COLOR = 0x7B4FD6
ENEMY = ("enemy", "enemy2", "enemy3")
SIZE = ((16, 14), (21, 10), (18, 14))

# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def ramp(score):
    return min(BASE_SPEED + score // 5, MAX_SPEED)    # // = หารปัดเศษทิ้ง 0-4 แต้ม = +0, 5-9 แต้ม = +1

def countdown(c):
    return max(0, c - 1)                              # นับถอยหลังแล้วหยุดที่ 0 ไม่ติดลบ

def boom_frame(t):
    return "boom1" if t > BOOM_FRAMES // 2 else "boom2"   # ครึ่งแรกลูกไฟเล็ก ครึ่งหลังวงใหญ่

# ---- 3) ตรวจสมองก่อนเปิดจอ ----
def self_test():
    for got, want in ((ramp(0), 2), (ramp(4), 2), (ramp(5), 3), (ramp(19), 5), (ramp(999), 6),
                      (countdown(3), 2), (countdown(0), 0), (boom_frame(10), "boom1"), (boom_frame(5), "boom2")):
        if got != want:
            print("ยังไม่ผ่าน: ได้", got, "ควรได้", want)
            return False
    print("ผ่าน! สมองของลูกเล่นถูกทุกกรณี")
    return True

if not self_test():
    raise SystemExit

# ---- 4) หน้าจอ: สร้างครั้งเดียว (พื้นหลังก่อน ระเบิดกับ HUD ทีหลัง จะได้อยู่บนสุด) ----
game.title("GAME JUICE")
game.Box(0, 0, game.WIDTH, game.HEIGHT, 0x10122C)
enemies = game.pool("enemy", ENEMIES)
boss = game.Box(340, BOSS_Y, BOSS_W, BOSS_H, BOSS_COLOR, border=game.WHITE, radius=8, border_w=2)
boss_face = game.Sprite("enemy3", 0, 0)
bullets = [game.Box(-20, -50, 5, 12, 0x80F4FF, border=game.WHITE, radius=2) for _ in range(BULLETS)]
ship = game.Sprite("ship", 365, SHIP_Y)
booms = game.pool("boom1", BOOMS)
hud_score = game.Text("", 12, 8, game.WHITE)
hud_ramp = game.Text("", 230, 8, game.YELLOW)
game.Text("BOSS", 560, 8, game.WHITE)
hp_boxes = [game.Box(620 + i * 32, 10, 26, 16, game.RED, border=game.WHITE, radius=3) for i in range(BOSS_HP)]

st = {"score": 0, "cool": 0, "flash": 0, "hp": BOSS_HP, "away": 0, "bvx": 3, "shown": None}
boom_t = [0] * BOOMS

def drop(i):
    kind = random.randint(0, 2)
    enemies[i].frame(ENEMY[kind])
    enemies[i].w, enemies[i].h = SIZE[kind]
    enemies[i].move_to(random.randint(20, game.WIDTH - 40), -random.randint(20, 220))
    enemies[i].show()

def boom_at(cx, cy):
    b = boom_t.index(min(boom_t))                   # ใช้ลูกที่ว่าง หรือใกล้หมดเวลาที่สุด
    boom_t[b] = BOOM_FRAMES
    booms[b].frame("boom1")
    booms[b].move_to(cx - 14, cy - 12)              # ภาพระเบิด 29x24 จัดกลางที่จุดโดน
    booms[b].show()

def hit_boss(x):
    boom_at(x, BOSS_Y + BOSS_H)                     # ระเบิดเล็กตรงจุดที่กระสุนกระทบใต้ท้องบอส
    st["hp"] -= 1
    hp_boxes[st["hp"]].hide()                       # แถบพลังหายทีละก้อน ไม่ resize
    boss.set_color(game.WHITE)                      # กะพริบ: เปลี่ยนสีทันที แล้วนับถอยหลัง
    st["flash"] = FLASH_FRAMES
    game.sfx("hit")
    if st["hp"] == 0:                               # บอสแตก
        boss.hide()
        boss_face.hide()
        boom_at(boss.x + BOSS_W // 2, BOSS_Y + BOSS_H // 2)
        st["score"] += 5
        st["away"] = BOSS_RESPAWN
        game.sfx("explode")

for i in range(ENEMIES): drop(i)

# ---- 5) วงวนหลัก ----
def on_frame():
    k = game.keys()
    if k.left or k.right:
        ship.move_to(max(0, min(game.WIDTH - 62, ship.x + (k.right - k.left) * SHIP_SPEED)), SHIP_Y)
    st["cool"] = countdown(st["cool"])
    if k.a and st["cool"] == 0:
        for b in bullets:
            if b.y < 0:
                b.move_to(ship.x + 28, SHIP_Y - 12)
                st["cool"] = FIRE_COOLDOWN
                game.sfx("fire")
                break

    if st["away"] == 0:                             # บอสวิ่งซ้ายขวา ชนขอบแล้วสะท้อน
        if boss.x < 20 or boss.x > game.WIDTH - 20 - BOSS_W:
            st["bvx"] = -st["bvx"]
        boss.move_to(boss.x + st["bvx"], BOSS_Y)
        boss_face.move_to(boss.x + BOSS_W // 2 - 9, BOSS_Y + 10)
    else:
        st["away"] = countdown(st["away"])
        if st["away"] == 0:                         # บอสตัวใหม่ พลังเต็ม
            st["hp"] = BOSS_HP
            for h in hp_boxes: h.show()
            boss.show()
            boss_face.show()

    speed = ramp(st["score"])                       # ศัตรูตกเร็วตามคะแนน (มีเพดาน)
    for i in range(ENEMIES):
        enemies[i].move_to(enemies[i].x, enemies[i].y + speed)
        if enemies[i].y > game.HEIGHT: drop(i)       # หลุดล่างจอ = วนกลับขึ้นบน (ชุดหมุนเวียน)

    for b in bullets:
        if b.y < 0: continue
        b.move_to(b.x, b.y - BULLET_SPEED)
        used = b.y < 30
        for i in range(ENEMIES):
            if not used and game.hit(b, enemies[i]):
                boom_at(enemies[i].x + enemies[i].w // 2, enemies[i].y + enemies[i].h // 2)
                st["score"] += 1
                game.sfx("hit")
                drop(i)
                used = True
        if not used and st["away"] == 0 and game.hit(b, boss):
            hit_boss(b.x + 2)
            used = True
        if used: b.move_to(-20, -50)

    if st["flash"] > 0:                             # นับถอยหลังกะพริบ ครบแล้วคืนสีเดิม
        st["flash"] = countdown(st["flash"])
        if st["flash"] == 0: boss.set_color(BOSS_COLOR)
    for j in range(BOOMS):                          # ระเบิด: ครึ่งแรก boom1 ครึ่งหลัง boom2 แล้วซ่อน
        if boom_t[j] > 0:
            boom_t[j] -= 1
            if boom_t[j] == BOOM_FRAMES // 2: booms[j].frame(boom_frame(boom_t[j]))
            if boom_t[j] == 0: booms[j].hide()

    if st["score"] != st["shown"]:                  # HUD เขียนเฉพาะตอนคะแนนเปลี่ยน
        hud_score.set("SCORE %d" % st["score"])
        hud_ramp.set("SPEED = min(%d + %d // 5, %d) = %d" % (BASE_SPEED, st["score"], MAX_SPEED, ramp(st["score"])))
        st["shown"] = st["score"]
    return True

game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) MAX_SPEED = 6 -> 99 : ไม่มีเพดาน ยิ่งเก่งยิ่งเร็วไม่หยุด จนศัตรูก้าวต่อเฟรมยาวกว่าตัวกระสุนกับตัวมันรวมกัน
#    บางนัดจึงทะลุผ่านไปเฉย ๆ ทั้งที่เล็งตรง — เพดานคือสิ่งที่ทำให้เกมยังเล่นได้
# 2) FLASH_FRAMES = 6 -> 1 : กะพริบแวบเดียวแทบไม่เห็น · -> 30 : ขาวค้างนานจนไม่รู้ว่าโดนครั้งไหน
# 3) ลบบรรทัด boss.set_color(game.WHITE) ออก : ยิงโดนบอสแล้วรู้สึก "ไม่โดน" ทั้งที่แถบพลังลด ลองเทียบความรู้สึกดู

ส่วนที่ 7 — ตัวอย่าง: Shooter กลายเป็นสายพานคัดแยก

ใน shooter_step4.py↗ ในสายพานคัดแยก เปลี่ยนอะไรในโค้ด
ศัตรู 6 ตัวตกลงมา ตำแหน่ง x สุ่ม กล่องพัสดุ 4 ใบไหลลงมากลางสายพาน มี 2 สี x คงที่กลางสายพาน · สุ่มสีจาก 2 สี แล้ว จำสีไว้ในลิสต์ box_color[index] (กล่องไม่จำสีตัวเอง)
ยานเลื่อนซ้าย-ขวาด้วยความเร่ง ประตูคัดแยก 2 ตำแหน่ง: ช่อง A หรือ B · สายพานเร่งได้ ความเร็วกล่องทุกใบ = speed ตัวเดียว (กด A ค้าง = เร่ง · ปล่อย = speed *= FRICTION) · ซ้าย = ช่อง A · ขวา = ช่อง B
กระสุน 6 นัด ไม่ใช้ ลบออก ได้ของบนจอคืน 6 ชิ้น
ยิงโดน → Score +1 กล่องถึงประตู ช่องตรงสี → ถูก +1 · ไม่ตรง → ผิด game.hit(กล่อง, ประตู) แล้วเทียบ box_color[index] กับช่อง
หลุดพื้น → Lives −1 คัดผิด → โควตาของเสีย −1 ตัวแปรเดิม เปลี่ยนชื่อเป็น rejects_left
GAME OVER หยุดสายพาน: คัดผิดครบ 3 ครั้ง ข้อความเดิม เปลี่ยนคำ

โค้ดเดิมส่วนใหญ่ใช้ต่อได้ สิ่งที่เปลี่ยนคือ ความหมาย ของแต่ละชิ้น

ลองเล่น · สายพานคัดแยกพัสดุ

คัดลอกไฟล์ s13_conveyor_sort.py↗ ไปวางใน BENTO Emulator แล้วกด Run

เล่นยังไง

  • กด Start · กด A ค้าง = สายพานเร่ง (แถบบนสายพานวิ่งตาม) · ปล่อย = ค่อย ๆ หยุด
  • จอยซ้าย = ประตูชี้ช่อง A (แดง) · จอยขวา = ประตูชี้ช่อง B (น้ำเงิน) ช่องที่ชี้อยู่จะสว่าง
  • กล่องป้ายแดงต้องลงช่อง A กล่องป้ายน้ำเงินต้องลงช่อง B · คัดผิด = ระเบิดที่ช่องนั้น
  • คัดผิดครบ 3 ครั้ง = สายพานหยุด (STOPPED) · ผิดได้อีกครั้งเดียว = มีบรรทัดเตือนสีส้ม

ลองแก้ แล้วรันใหม่

  1. FRICTION = 0.96 -> 0.80 : ปล่อย A แล้วสายพานหยุดเร็วขึ้น — แบบไหนคุมง่ายกว่า
  2. GAP = 78 -> 50 : กล่องมาถี่ขึ้น คัดทันไหม (ดูแถบสายพานวิ่งเท่าเดิม แต่กล่องแน่นขึ้น)
  3. เพิ่มป้ายสีที่สาม (game.GREEN) และช่อง C — ต้องแก้ judge() กับ GATE_X / CHUTE_X ตรงไหนบ้าง

โค้ดเต็ม · s13_conveyor_sort.py

แล้ววางใน BENTO Emulator · หรือเปิด บน GitHub↗
# s13_conveyor_sort.py — คาบ 13 (คาบพิเศษ): สายพานคัดแยกพัสดุ = Shooter คาบ 12 ที่เปลี่ยนความหมาย
#
# หลักการ   : โค้ดเดิมจาก shooter_step4.py ใช้ต่อได้เกือบทั้งหมด สิ่งที่เปลี่ยนคือ "ความหมาย"
#             ศัตรูที่ตก -> กล่องพัสดุบนสายพาน (ชุดหมุนเวียน คาบ 11 วนกลับขึ้นบน)
#             ยานมีน้ำหนัก -> สายพานเร่งแล้วค่อย ๆ หยุด (เร่ง + แรงเสียดทาน คาบ 6, 10)
#             ยิงโดน -> กล่องถึงประตูคัดแยก (ชนแบบกล่อง คาบ 4, 12) · ระเบิด -> กล่องลงช่องผิด
#             ชีวิต + GAME OVER -> โควตาของเสีย + หยุดสายพาน (เครื่องสถานะ คาบ 2, 7)
# ลองเล่น   : กด Start · กด A ค้าง = สายพานเร่ง (แถบบนสายพานวิ่งตาม) · ปล่อย = ค่อย ๆ หยุด
#             จอยซ้าย = ประตูชี้ช่อง A (แดง) · จอยขวา = ประตูชี้ช่อง B (น้ำเงิน) ช่องที่ชี้อยู่จะสว่าง
#             กล่องป้ายแดงต้องลงช่อง A กล่องป้ายน้ำเงินต้องลงช่อง B · คัดผิด = ระเบิดที่ช่องนั้น
#             คัดผิดครบ 3 ครั้ง = สายพานหยุด (STOPPED) · ผิดได้อีกครั้งเดียว = มีบรรทัดเตือนสีส้ม
# ของบนบอร์ด: จอ · จอย · ลำโพง
# ในเกม     : Shooter คาบ 12 (ศัตรูตก · ชน · ระเบิด · คะแนน · ชีวิต · GAME OVER)
# ในงานจริง : หน้าจอสายพานคัดแยกในศูนย์กระจายสินค้า · แถบสถานะบอก RUNNING / IDLE / STOPPED
#             บรรทัดเตือนขึ้นก่อนหยุดสายพาน · ตัวเลขทั้งหมดเป็นค่าจำลองเพื่อการเรียน
# บอร์ด     : Eva Kit (เฟิร์มแวร์ Game Console) และเครื่องจำลอง
# ต่อยอดจาก : shooter_step4.py (ชุดหมุนเวียน · ชน · ชีวิต) · pong_step2.py (เร่ง + แรงเสียดทาน)
# งบวิดเจ็ต : 26 ชิ้น (แถบสถานะ 4 + สายพาน 3 + กล่อง 4 + ประตู 1 + ช่อง 4 + ระเบิด 1
#             + การ์ดค่า 8 + game.run 1) · ต่อเฟรมส่งคำสั่งราว 6-8 ครั้ง (เฟรมที่กล่องถึงประตูราว 10-14)
#
# โครงไฟล์ (เหมือนกันทุกไฟล์ของคาบ 13):
#   1) ตั้งค่า (แก้ได้)   2) สมอง: คิดล้วน ๆ ไม่แตะจอ   3) ตรวจสมองก่อนเปิดจอ
#   4) หน้าจอ: สร้างครั้งเดียว อัปเดตเฉพาะที่เปลี่ยน   5) วงวนหลัก
import bentogame as game
import random

# ---- 1) ตั้งค่า (แก้ได้) ----
BOXES = 4            # กล่องบนสายพาน (ชุดหมุนเวียน สร้างครั้งเดียว)
GAP = 78             # ระยะห่างระหว่างกล่อง (px)
ACCEL = 0.20         # กด A ค้าง 1 เฟรม สายพานเร็วขึ้นเท่านี้
FRICTION = 0.96      # ปล่อย A แล้วความเร็วคูณเท่านี้ทุกเฟรม (ค่อย ๆ หยุด)
MAX_SPEED = 6.0      # เพดานความเร็วสายพาน (px ต่อเฟรม)
REJECTS = 3          # คัดผิดได้กี่ครั้งก่อนหยุดสายพาน
COLORS = [game.RED, game.BLUE]      # ป้ายสีที่ 0 = ช่อง A · ป้ายสีที่ 1 = ช่อง B

BELT_X, BELT_W, BELT_END = 336, 120, 300    # สายพานแนวตั้งกลางจอ (บนสุด y = 40)
BOX_W, BOX_H = 54, 42
LANE_X = BELT_X + (BELT_W - BOX_W) // 2
GATE_Y = 304
GATE_W = 160
GATE_X = {0: BELT_X - 100, 1: BELT_X + 60}  # ประตูเลื่อนไปทางช่อง A (ซ้าย) หรือช่อง B (ขวา)
CHUTE_X, CHUTE_W = {0: 146, 1: 416}, 230
DIM = {0: 0x2A1416, 1: 0x14202E}            # ช่องที่ประตูไม่ได้ชี้ (มืด)
LIT = {0: 0x6A2226, 1: 0x1E4A78}            # ช่องที่ประตูชี้อยู่ (สว่าง)
PANEL, EDGE = 0x1B2633, 0x3D5266
BOOM = ["boom1", "boom2", "boom3", "boom4", "boom5", "boom6"]



# ---- 2) สมอง: คิดล้วน ๆ ไม่แตะจอ (ทดสอบได้โดยไม่ต้องมีบอร์ด) ----
def belt_step(speed, pressed, accel, friction, vmax):
    # การ์ด 3: กดอยู่ = เร่ง (มีเพดาน) · ปล่อย = คูณแรงเสียดทาน · ช้ามากจนเกือบศูนย์ = หยุดสนิท
    if pressed:
        return min(speed + accel, vmax)
    speed = speed * friction
    if speed < 0.05:
        return 0.0
    return speed


def judge(color_index, gate_side):
    # ช่องตรงสี = คัดถูก (สี 0 ต้องลงช่อง 0 = A · สี 1 ต้องลงช่อง 1 = B)
    return color_index == gate_side


def next_y_above(ys, gap):
    # วนกล่องกลับขึ้นไปต่อท้ายแถว: เหนือกล่องที่อยู่สูงสุดอีกหนึ่งช่วง gap
    return min(ys) - gap


# ---- 3) ตรวจสมองก่อนเปิดจอ ----
def self_test():
    cases = [
        ("belt_step", belt_step(0.0, True, 0.5, 0.9, 6.0), 0.5),
        ("belt_step", belt_step(5.8, True, 0.5, 0.9, 6.0), 6.0),   # ชนเพดาน
        ("belt_step", belt_step(4.0, False, 0.5, 0.9, 6.0), 3.6),  # ปล่อย = คูณแรงเสียดทาน
        ("belt_step", belt_step(0.04, False, 0.5, 0.9, 6.0), 0.0), # ช้ามาก = หยุดสนิท
        ("judge", judge(0, 0), True),
        ("judge", judge(1, 1), True),
        ("judge", judge(1, 0), False),
        ("next_y_above", next_y_above([100, -20, 300], 90), -110),
    ]
    for name, got, want in cases:
        if abs(got - want) > 1e-6:
            print("ยังไม่ผ่าน:", name, "ได้", got, "ควรได้", want)
            return False
    print("ผ่าน! สมองของสายพานถูกทั้ง", len(cases), "กรณี")
    return True


if not self_test():
    raise SystemExit

# ---- 4) หน้าจอ: สร้างครั้งเดียว (สร้างก่อน = อยู่ล่าง สร้างทีหลัง = ทับข้างบน) ----
game.title("CONVEYOR SORT")
game.Box(BELT_X, 40, BELT_W, BELT_END - 40, 0x202833, border=0x8899AA, border_w=4)   # ผืนสายพาน + ราง
slats = [game.Box(BELT_X + 4, 60 + i * 130, BELT_W - 8, 8, 0x46546A) for i in range(2)]
box_color = [random.randint(0, 1) for _ in range(BOXES)]  # กล่องไม่จำสีตัวเอง จึงจำไว้ในลิสต์
boxes = [game.Box(LANE_X, BELT_END - 70 - i * GAP, BOX_W, BOX_H, COLORS[box_color[i]],
                  border=0xC08A4E, radius=3, border_w=8) for i in range(BOXES)]   # ขอบน้ำตาล = กล่อง · กลาง = ป้ายสี

bar = game.Box(-4, -4, 800, 44, PANEL, border=EDGE, radius=4)    # แถบสถานะชิดขอบจอ (บังกล่องที่โผล่จากบนจอ)
game.Text("สายพานคัดแยกพัสดุ   SORTER", 14, 9, game.WHITE)
hud_state = game.Text("IDLE  หยุดรอ", 600, 9, game.WHITE)
warn = game.Text(" ", 18, 240, game.ORANGE)

gate = game.Box(GATE_X[0], GATE_Y, GATE_W, 12, COLORS[0], border=game.WHITE, radius=4, border_w=2)
chutes, chute_txt = [], []
for side in (0, 1):
    chutes.append(game.Box(CHUTE_X[side], 322, CHUTE_W, 46, LIT[side] if side == 0 else DIM[side],
                           border=COLORS[side], radius=6, border_w=3))
    chute_txt.append(game.Text("ช่อง %s   0" % "AB"[side], CHUTE_X[side] + 24, 334, game.WHITE))
boom = game.Sprite("boom1", 0, 0)
boom.hide()


def card(x, y, accent, title):
    # การ์ดค่า: แผงมืดขอบสี + ป้ายสองบรรทัด (บรรทัดบน = ชื่อ · บรรทัดล่าง = ค่า)
    game.Box(x, y, 230, 78, PANEL, border=accent, radius=8, border_w=2)
    return game.Text(title, x + 16, y + 15, game.WHITE)


cards = {"ok": card(14, 52, game.GREEN, "คัดถูก  OK"), "wrong": card(14, 142, game.RED, "คัดผิด  WRONG"),
         "left": card(548, 52, game.ORANGE, "ผิดได้อีก  LEFT"), "belt": card(548, 142, game.CYAN, "สายพาน  BELT")}

speed, gate_side = 0.0, 0
sorted_ok, wrong, rejects_left = 0, 0, REJECTS
count = [0, 0]                     # นับกล่องที่ลงแต่ละช่อง
boom_t, flash_t = 0, 0             # ตัวนับเฟรมของภาพระเบิด / ช่องกะพริบ (ไม่ใช้ sleep)
shown = {}


def show(key, widget, text):
    # อัปเดตป้ายเฉพาะตอนข้อความเปลี่ยน
    if shown.get(key) != text:
        widget.set(text)
        shown[key] = text


# ---- 5) วงวนหลัก ----
def on_frame():
    global speed, gate_side, sorted_ok, wrong, rejects_left, boom_t, flash_t
    keys = game.keys()
    stopped = rejects_left <= 0

    # รับปุ่ม (วัด)
    speed = 0.0 if stopped else belt_step(speed, keys.a, ACCEL, FRICTION, MAX_SPEED)
    side = 0 if keys.left else (1 if keys.right else gate_side)
    if side != gate_side and not stopped:
        gate_side = side
        gate.move_to(GATE_X[side], GATE_Y); gate.set_color(COLORS[side])
        chutes[side].set_color(LIT[side]); chutes[1 - side].set_color(DIM[1 - side])
        game.sfx("move")

    # คิด (ตัดสิน): กล่องทุกใบและแถบสายพานไหลด้วยความเร็วสายพานตัวเดียว แล้วเช็คว่าถึงประตูหรือยัง
    if speed > 0:
        for s in slats:
            y = s.y + speed
            s.move_to(s.x, y - 260 if y > BELT_END - 8 else y)
        for i, box in enumerate(boxes):
            box.y += speed                     # ขยับในตัวแปรก่อน แล้วส่งไปจอครั้งเดียวท้ายรอบ
            if game.hit(box, gate):
                count[gate_side] += 1
                if judge(box_color[i], gate_side):
                    sorted_ok += 1
                    chutes[gate_side].set_color(COLORS[gate_side])     # ช่องกะพริบสีตัวเอง = รับกล่องถูก
                    game.sfx("point")
                else:
                    wrong += 1
                    rejects_left -= 1
                    chutes[gate_side].set_color(game.ORANGE)           # ช่องกะพริบส้ม + ระเบิด = ลงผิด
                    boom.move_to(CHUTE_X[gate_side] + CHUTE_W - 50, 333)
                    boom.show()
                    boom_t = 3 * len(BOOM)
                    game.sfx("explode")
                flash_t = 6
                box_color[i] = random.randint(0, 1)
                box.set_color(COLORS[box_color[i]])
                box.y = next_y_above([b.y for b in boxes], GAP)
            box.move_to(box.x, box.y)

    # ภาพเคลื่อนไหวนับเฟรม: ระเบิดเปลี่ยนภาพทุก 3 เฟรม · ช่องกะพริบ 6 เฟรมแล้วกลับสีเดิม
    if boom_t > 0:
        boom_t -= 1
        if boom_t == 0:
            boom.hide()
            boom.frame(BOOM[0])                 # เตรียมภาพแรกไว้สำหรับรอบหน้า
        elif boom_t % 3 == 0:
            boom.frame(BOOM[len(BOOM) - boom_t // 3])
    if flash_t > 0:
        flash_t -= 1
        if flash_t == 0:
            chutes[gate_side].set_color(LIT[gate_side])

    # ออก (ทำ + โชว์)
    show("ok", cards["ok"], "คัดถูก  OK\n%d กล่อง" % sorted_ok)
    show("wrong", cards["wrong"], "คัดผิด  WRONG\n%d กล่อง" % wrong)
    show("left", cards["left"], "ผิดได้อีก  LEFT\n%d ครั้ง" % max(rejects_left, 0))
    show("belt", cards["belt"], "สายพาน  BELT\n%d.%d px ต่อเฟรม" % (int(speed), int(speed * 10) % 10))
    for side in (0, 1):
        show("c%d" % side, chute_txt[side], "ช่อง %s   %d" % ("AB"[side], count[side]))
    state = "STOPPED" if rejects_left <= 0 else ("RUNNING" if speed > 0 else "IDLE")
    if state != shown.get("state"):                     # เครื่องสถานะ: สีแถบบนบอกสถานะเครื่อง
        hud_state.set({"STOPPED": "STOPPED  หยุดเครื่อง", "RUNNING": "RUNNING  กำลังเดิน",
                       "IDLE": "IDLE  หยุดรอ"}[state])
        bar.set_color({"STOPPED": 0xB02020, "RUNNING": 0x1C5A34, "IDLE": PANEL}[state])
        shown["state"] = state
        if state == "STOPPED":
            game.sfx("gameover")
    if rejects_left <= 0:
        show("warn", warn, "หยุดสายพาน!\nคัดผิดครบ %d ครั้ง" % REJECTS)
    else:
        show("warn", warn, "ระวัง! ผิดอีก 1 ครั้ง\nสายพานจะหยุด" if rejects_left == 1 else " ")
    if stopped and boom_t == 0:                        # รอระเบิดเล่นจบก่อน แล้วจบโปรแกรม
        return False
    return True


game.run(on_frame, fps=30)

# ---- ลองแก้ แล้วรันใหม่ ----
# 1) FRICTION = 0.96 -> 0.80 : ปล่อย A แล้วสายพานหยุดเร็วขึ้น — แบบไหนคุมง่ายกว่า
# 2) GAP = 78 -> 50 : กล่องมาถี่ขึ้น คัดทันไหม (ดูแถบสายพานวิ่งเท่าเดิม แต่กล่องแน่นขึ้น)
# 3) เพิ่มป้ายสีที่สาม (game.GREEN) และช่อง C — ต้องแก้ judge() กับ GATE_X / CHUTE_X ตรงไหนบ้าง

หน้าจอที่เคลื่อนไหวในงานจริง

งาน สิ่งที่เคลื่อนไหวบนจอ เทคนิคจากเกม
โรงงาน กล่องบนสายพาน · ประตูคัดแยก · ตัวนับ ชุดหมุนเวียน · ชน · HUD
คลังสินค้า รถยกในโซน · ไฟเตือนระดับ ชน · เครื่องสถานะ · เสียงเตือน
เกษตร ระดับน้ำในถังโรงเรือน · วาล์ว สะสมค่า · จำกัดขอบ · สามระดับ + ช่องว่าง
จราจร รถในสี่แยก · ไฟสัญญาณ เครื่องสถานะ · ชุดหมุนเวียน · ชน
ลานจอดรถ รถเข้า-ออก · ป้ายช่องว่าง ชุดหมุนเวียน · HUD · LED
ศูนย์ฝึก เกมจำลองฝึกขับรถยกก่อนลงงานจริง ทุกอย่างรวมกัน

ทุกหน้าจอในตารางนี้ทำได้ด้วยเทคนิคจาก 12 คาบ — ของที่น้อง ๆ ทำวันนี้คือ ต้นแบบเพื่อการเรียน ตัวเลขเป็นค่าจำลอง

โปรเจกต์เริ่มวันนี้ — ประเมินรายคนเสมอ

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

วัน สิ่งที่ต้องเสร็จ
7 ต.ค. (วันนี้) ฟังอาจารย์ประกาศ · เริ่มทำใบงานคาบ 13 (ลองแก้ทุกตัวอย่าง + ไอเดียของตัวเอง 2 เรื่อง)
14 ต.ค. เสนอหัวข้อ: pitch deck + โปสเตอร์ร่างหนึ่งหน้า
21 ต.ค. ความก้าวหน้า 1/2: รุ่นแรกที่เล่นได้ รันบนบอร์ด
28 ต.ค. ความก้าวหน้า 2/2 และผลงานสุดท้าย + สอบรายคน

ไม่ว่าทำรายคนหรือเป็นคู่ อาจารย์ประเมินรายคนเสมอ

  1. อาจารย์ถามรายคนเรื่องโค้ดที่ น้อง เขียนเอง
  2. สอบรายคนด้วย ไฟล์ตั้งต้นเดียวกันทุกคน — วัดสิ่งที่น้องทำได้เอง

แนวทาง pitch deck และโปสเตอร์อยู่ในสไลด์คาบ 14 และเอกสารโจทย์ใน Google Classroom · เตรียมตัวสอบรายคนที่ดีที่สุด = ลองแก้ทุกตัวอย่างของคาบนี้จนเข้าใจ

fit-css

core

core

core

gen:index

core

core

/gen:index

core

gen:s13

core

core

/gen:s13

core

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

core

gen:s13

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

core

core

core

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

gen:s13

core

core

/gen:s13

core

core

← Roadmap (TOC)