[TH] เกม 15-Puzzle

บทความนี้เป็นตัวอย่างเกม 15-Puzzle โดยใช้บอร์ด ml4m ที่มีผลลัพธ์ของหน้าจอดังภาพที่ 1 ซึ่งเป็นเกมที่ทำให้ผู้เล่นได้ฝึกทักษะการคิดแบบมีกลยุทธ์มีการมองเกมล่วงหน้าเพื่อวางแผนการเลื่อนตัวเลข นอกจากนี้เกม 15-puzzle นอกจากอยู่ในรูปแบบของตัวเลขแล้วยังสามารถเปลี่ยนแปลงจากตัวเลขให้เป็นภาพ คือ เปลี่ยนเป็นภาพ 1 ภาพและแบ่งออกเป็น 16 ส่วน แล้วให้ผู้เล่นทำการเลื่อนภาพเพื่อต่อให้เหมือนกับต้นฉบับ นอกจากนี้ในตัวอย่างมีการใช้บัซเซอร์ในการสร้างเสียงบี๊บโดยใช้ DAC ขนาด 8 บิตของไมโครคอนโทรลเลอร์ esp32 พร้อมทั้งการเขียนโปรแกรมเลือกใช้ภาษาไพธอนบน MicroPython เช่นเคย

ภาพที่ 1 ตัวอย่างการสุ่มค่าในตาราง 4×4

15 Puzzle

เกม 15 puzzle เป็นเกมแบบตารางขนาด 4×4 ช่องดังที่ได้แสดงเป็นตัวอย่างนภาพที่ 1 ซึ่งแต่ละช่องประกอบไปด้วยตัวเลข 1 ถึง 15 และช่องว่าง 1 ช่อง ซึ่งในบทความนี้ใช้เป็นตัวเลข 1 ถึง 9 และตัวอักขระ A,B,C,D,E และ F แทนตัวเลข 10,11,12,13,14 และ 15 เนื่องจากต้องการประหยัดพื้นที่แสดงผลเนื่องจากจอแสดงผลของบอร์ด ml4m มีขนาด 128×64 จุด

ดังนั้น พวกเราจึงเลือกใช้ตัวแปรแบบลิสต์เพื่อเก็บรายการข้อมูลภายในตารางชื่อ randPuzzle และสร้างตัวแปร posPuzzle สำหรับเก็บตำแหน่งค่า (แถว,คอลัมน์) ของข้อมูลแต่ละตำแหน่งดังนี้

## ตัวแปรเก็บค่าสุ่มในตาราง 4x4
randPuzzle = [] 
## ตัวแปรเก็บค่า x,y ของลำดับใน randPuzzle เช่น ลำดับที่ 3 ใน randPuzzle คือ posPuzzle[3]
## วึ่งเป็น (3,0) หมายถึง คอลัมน์ 3 แถวแรก ใช้สำหรับตรวจสอบคุณสมบัติของการสลับตำแหน่ง
posPuzzle = (
    (0,0),(1,0),(2,0),(3,0),
    (0,1),(1,1),(2,1),(3,1),
    (0,2),(1,2),(2,2),(3,2),
    (0,3),(1,3),(2,3),(3,3))

ข้อกำหนด

ข้อกำหนดของการเล่นเกมกำหนดดังนี้

  • คันโยกใช้สำหรับเลื่อนซ้าย ขวา บท หรือล่าง
  • swA สำหรับเริ่มเกมใหม่ พร้อมทำการสุ่มค่าตารางใหม่
  • swB สำหรับออกจากเกม
  • การชนะเกมจะต้องเรียงให้เป็นดังนี้
1234
5678
9ABC
DEF

การสุ่มตัวเลข

การสุ่มตัวเลขใช้การสุ่มค่าจากรายการในลิสต์ซึ่งเก็บในตัวแปร tmp ซึ่งขั้นตอนวิธีเป็นดังนี้

  1. สร้างรายการ 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E และ F
  2. ล้างค่าตัวแปร randPuzzle ให้เป็นค่าว่าง
  3. ทำการสุ่ม 15 ครั้ง
    1. สุ่มเลือกข้อมูลจาก tmp
    2. นำข้อมูลที่สุ่มเลือกได้จัดเก็บลง randPuzzle
    3. ลบข้อมูลที่สุ่มได้ออกจาก tmp
  4. นำค่าตัวสุดท้ายที่เก็บใน tmp ไปเก็บใน randPuzzle
  5. นำค่าสุดท้ายออกจาก tmp

โค้ดสำหรับการสุ่มตัวเลขเพื่อเก็บในตารางเป็นดังนี้

def randomTable():
    global randPuzzle
    tmp = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
    randPuzzle = []
    for i in range(15):
        data = random.choice(tmp) # สุ่มเลือก
        randPuzzle.append(data) # นำไปเก็บในตาราง
        tmp.remove(data) # ดึงอันที่สุ่มออกไป
    randPuzzle.append(tmp[0]) # ย้ายอันสุดท้ายที่เหลืออยู่มาเก็บในตาราง
    tmp.pop() # นำออก
    tmp=[] # ล้างค่าของตัวแปรลิสต์
    gc.collect()

การเลื่อน

การเลื่อนอาศัยค่าจากการเลื่อนคันโยกของโมดูลจอยสติก โดยพิจารณาการเลื่อนจากตำแหน่งของช่องว่างดังเงื่อนไขต่อไปนี้

  1. ถ้าโยกคันโยกไปทางขวา และทางขวาของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง
  2. ถ้าโยกคันโยกไปทางซ้าย และทางซ้ายของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง
  3. ถ้าโยกคันโยกไปด้านบนและด้านบนของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง
  4. ถ้าโยกคันโลกลงด้านล่างและด้านล่างของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง

โค้ดโปรแกรมส่วนของการเลื่อนเป็นดังนี้

def moveControl():
    global randPuzzle
    while True:
        # หาตำแหน่งของ "0"
        (xp,yp,p) = find0() 
        ## วาด
        drawTable()
        ## ตรวจสอบการชนะ
        if (isWin()):
            youWin()
            break
        ## รับค่า
        (a,d,w,s,n1,n2) = getInput()
        if (n1):
            randomTable()
            (xp,yp,p) = find0()
            youLost()
        if (n2):
            break
        if (a and (not d) and (not w) and (not s)):
            #สลับกับค่าทางขวา 
            if (xp < 3):
                pos = yp*numTiles+(xp+1)
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0"
                beep()
        if ((not a) and (d) and (not w) and (not s)):
            # สลับกับค่าทางซ้าย 
            if (xp > 0):
                pos = yp*numTiles+(xp-1)
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0"
                beep()
        if ((not a) and (not d) and (w) and (not s)):
            # สลับกับค่าด้านล่าง 
            if (yp < 3):
                pos = (yp+1)*numTiles+xp
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0" 
                beep()
        if ((not a) and (not d) and (not w) and (s)):
            # สลับกับค่าด้านบน
            if (yp >0):
                pos = (yp-1)*numTiles+xp
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0" 
                beep()
        time.sleep_ms(100)

การแพ้ชนะ

การแพ้เกิดจากผู้ใช้กดที่ swA เพื่อทำการสุ่มค่าตำแหน่งใหม่ และหน้าจอจะเลื่อนไปทางขวา หลังจากนั้นแสดงข้อความu Lost” ดังภาพที่ 2

def youLost():
    for i in range(32):
        oled.scroll(1,0)
        oled.show()
    oled.text("You",10,20)
    oled.text("Lost",9,30)
    oled.text("Lost",10,30)
    oled.text("Lost",11,30)
    oled.show()
    for i in range(10):
        oled.invert(False)
        time.sleep_ms(50)
        oled.invert(True)
        time.sleep_ms(50)
    oled.invert(False)
    time.sleep_ms(2000)
ภาพที่ 2 ตัวอย่างหน้าจอการแพ้หรือกดที่ swA

การชนะเกิดจากการตรวจสอบว่าค่าที่เก็บในตัวแปร randPuzzle นั้นตรงกับการจัดเรียงตามตัวแปร winPuzzle หรือไม่ ถ้าใช่แสดงว่าผู้เล่นชนะเกมถ้ายังไม่ใช่จะคืนนค่า False ดังตัวอย่างโค้ดต่อไปนี้

def isWin():
    win = False
    winPuzzle = ['1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','0']
    if (randPuzzle == winPuzzle):
        win = True
    return win    

โค้ดโปรแกรมส่วนของการแสดงผล “You Win” เป็นดังนี้

def youWin():
    for i in range(32):
        oled.scroll(-1,0)
        oled.show()
    oled.text("You",80,20)
    oled.text("Win",79,30)
    oled.text("Win",80,30)
    oled.text("Win",81,30)
    oled.show()
    for i in range(10):
        oled.invert(False)
        time.sleep_ms(50)
        oled.invert(True)
        time.sleep_ms(50)
    oled.invert(False)
    time.sleep_ms(2000)        

โค้ดโปรแกรม

จากส่วนของโปรแกรมที่กล่าวไปข้างต้นสามารถรวมเป็นโค้ดโปรแกรมได้ดังนี้

#######################################################
### 15-Puzzle
### board: ML4M
### (C) 2021, JarutEx
### https://www.jarutex.com
#######################################################
from machine import Pin,I2C,ADC, DAC
import math
import machine
import gc
import ssd1306
import random
import time
import sys

#######################################################
## System setting
gc.enable()
gc.collect()
machine.freq(240000000)

## ตัวแปรเก็บค่าสุ่มในตาราง 4x4
randPuzzle = [] 
## ตัวแปรเก็บค่า x,y ของลำดับใน randPuzzle เช่น ลำดับที่ 3 ใน randPuzzle คือ posPuzzle[3]
## วึ่งเป็น (3,0) หมายถึง คอลัมน์ 3 แถวแรก ใช้สำหรับตรวจสอบคุณสมบัติของการสลับตำแหน่ง
posPuzzle = (
    (0,0),(1,0),(2,0),(3,0),
    (0,1),(1,1),(2,1),(3,1),
    (0,2),(1,2),(2,2),(3,2),
    (0,3),(1,3),(2,3),(3,3))

#######################################################
## OLED
sclPin = Pin(22)
sdaPin = Pin(21)
spkPin = DAC(Pin(25, Pin.OUT))
i2c = I2C(0,scl=sclPin, sda=sdaPin, freq=400000)
oled = ssd1306.SSD1306_I2C(128,64,i2c)
oled.poweron()
oled.contrast(255)
oled.init_display()
oled.fill(0)
oled.show()

## constant
blockHeight = const(16)
blockWidth = const(16)
screenWidth = const(128)
screenHeight = const(64)
numTiles = const(4)

## Game pad
swA = Pin(32, Pin.IN)
swB = Pin(33, Pin.IN)
swC = Pin(35, Pin.IN)
swD = Pin(34, Pin.IN)
swF = Pin(26, Pin.IN) # select
swE = Pin(27, Pin.IN) # start
jst = (ADC(Pin(39)), ADC(Pin(36))) # X,Y
jst[0].width( ADC.WIDTH_12BIT ) # 12bit
jst[0].atten( ADC.ATTN_11DB ) # 3.3V
jst[1].width( ADC.WIDTH_12BIT ) # 12bit
jst[1].atten( ADC.ATTN_11DB ) # 3.3V

#######################################################
## drawTable()
## แสดงตารางของเกม
#######################################################
def drawTable():
    tileSize = screenHeight//(numTiles+1)
    tableWidth = tileSize*numTiles
    tableHeight = tileSize*numTiles
    left = (screenWidth-tableWidth)//2
    top = (screenHeight-tableHeight)//2
    oled.fill(0)
    for r in range(5):
        oled.hline(left,top+r*tileSize,tableWidth,1)
    for c in range(5):
        oled.vline(left+c*tileSize,top,tableHeight,1)
    i = 0
    for r in range(4):
        for c in range(4):
            if (randPuzzle[i] != "0"):
                oled.text(randPuzzle[i],left+c*tileSize+2,top+r*tileSize+2,1)
            i += 1
    oled.show()

#######################################################
## randomTable()
#######################################################
def beep():
    spkPin.write(255)
    time.sleep_ms(20)
    spkPin.write(0)
    
#######################################################
## randomTable()
## สุ่มค่าในตารางใหม่
#######################################################
def randomTable():
    global randPuzzle
    tmp = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
    randPuzzle = []
    for i in range(15):
        data = random.choice(tmp) # สุ่มเลือก
        randPuzzle.append(data) # นำไปเก็บในตาราง
        tmp.remove(data) # ดึงอันที่สุ่มออกไป
    randPuzzle.append(tmp[0]) # ย้ายอันสุดท้ายที่เหลืออยู่มาเก็บในตาราง
    tmp.pop() # นำออก
    tmp=[] # ล้างค่าของตัวแปรลิสต์
    gc.collect()

    
#######################################################
## getInput()
## รับค่าจาก Joystick และ button switch
#######################################################
def getInput():
    left = False
    right = False
    up = False
    down = False
    button1 = False
    button2 = False
    #### Joystick
    jx = 4095-jst[0].read()
    jy = jst[1].read()
    
    if (jx < 1200):
        left = True
    elif (jx > 3000):
        right = True
    if (jy < 1200):
        up = True
    elif (jy > 3000):
        down = True
    # switch
    a = swA.value()
    b = swB.value()
    if (a):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        a2 = swA.value()
        if (a == a2):
            button1 = True
    if (b):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        b2 = swB.value()
        if (b == b2):
            button2 = True
    return (left,right,up,down,button1, button2)

#######################################################
## isWin(p)
## ตรวจสอบการชนะเกม
#######################################################
def isWin():
    win = False
    winPuzzle = ['1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','0']
    if (randPuzzle == winPuzzle):
        win = True
    return win    

#######################################################
## find0()
#######################################################
def find0():
    pos = randPuzzle.index("0")
    return (posPuzzle[pos][0],posPuzzle[pos][1],pos)


#######################################################
## youWin()
#######################################################
def youWin():
    for i in range(32):
        oled.scroll(-1,0)
        oled.show()
    oled.text("You",80,20)
    oled.text("Win",79,30)
    oled.text("Win",80,30)
    oled.text("Win",81,30)
    oled.show()
    for i in range(10):
        oled.invert(False)
        time.sleep_ms(50)
        oled.invert(True)
        time.sleep_ms(50)
    oled.invert(False)
    time.sleep_ms(2000)        

#######################################################
## youLost()
#######################################################
def youLost():
    for i in range(32):
        oled.scroll(1,0)
        oled.show()
    oled.text("You",10,20)
    oled.text("Lost",9,30)
    oled.text("Lost",10,30)
    oled.text("Lost",11,30)
    oled.show()
    for i in range(10):
        oled.invert(False)
        time.sleep_ms(50)
        oled.invert(True)
        time.sleep_ms(50)
    oled.invert(False)
    time.sleep_ms(2000)

#######################################################
## moveControl()
#######################################################
def moveControl():
    global randPuzzle
    while True:
        # หาตำแหน่งของ "0"
        (xp,yp,p) = find0() 
        ## วาด
        drawTable()
        ## ตรวจสอบการชนะ
        if (isWin()):
            youWin()
            break
        ## รับค่า
        (a,d,w,s,n1,n2) = getInput()
        if (n1):
            randomTable()
            (xp,yp,p) = find0()
            youLost()
        if (n2):
            break
        if (a and (not d) and (not w) and (not s)):
            #สลับกับค่าทางขวา 
            if (xp < 3):
                pos = yp*numTiles+(xp+1)
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0"
                beep()
        if ((not a) and (d) and (not w) and (not s)):
            # สลับกับค่าทางซ้าย 
            if (xp > 0):
                pos = yp*numTiles+(xp-1)
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0"
                beep()
        if ((not a) and (not d) and (w) and (not s)):
            # สลับกับค่าด้านล่าง 
            if (yp < 3):
                pos = (yp+1)*numTiles+xp
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0" 
                beep()
        if ((not a) and (not d) and (not w) and (s)):
            # สลับกับค่าด้านบน
            if (yp >0):
                pos = (yp-1)*numTiles+xp
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0" 
                beep()
        time.sleep_ms(100)


#######################################################
### Main program
#######################################################
beep()
randomTable()
moveControl()
oled.poweroff()
beep()

ตัวอย่างการเล่นเป็นดังคลิปต่อไปนี้ครับ

สรุป

จากตัวอย่างในครั้งนี้จะพบว่าการเขียนโปรแกรมเกมเป็นการอาศัยวิธีการสร้างปฏิสัมพันธ์กับผู้ใช้ประเภทหนึ่ง และการทำงานภายในเกมนั้นขึ้นอยู่กับแนวคิดของผู้เขียนโปรแกรมโดยไม่จำเป็นต้องเขียนด้วยวิธีเดียวกันเสมอไป นอกจากนี้ ด้วยข้อดีของการเขียนด้วยภาษาไพธอนทำให้นำโค้ดไปดัดแปลงเพื่อใช้งานกับแพลตฟอร์มอื่น ๆ ได้ง่ายขึ้น พวกเราเลยชอบเขียนด้วยไพธอนเพื่อสร้างโค้ดสำหรับทดสอบแนวคิด หลังจากนั้นจึงค่อยปรับปรุงประสิทธิภาพหรือแปลงโค้ดไปเป็นภาษาที่ทำงานได้รวดเร็วกว่าหรือเหมาะสมกับแพลฌตฟอร์มที่ใช้งาน ดังนั้น มาสนุกกับการเขียนโปรแกรมกันต่อไปนะครับ

ท่านใดต้องการพูดคุยสามารถคอมเมนท์ไว้ได้เลยครับ

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ

ปรับปรุงเมื่อ 2021-09-11, 2021-11-28