บทความนี้เป็นตัวอย่างเกม 15-Puzzle โดยใช้บอร์ด ml4m ที่มีผลลัพธ์ของหน้าจอดังภาพที่ 1 ซึ่งเป็นเกมที่ทำให้ผู้เล่นได้ฝึกทักษะการคิดแบบมีกลยุทธ์มีการมองเกมล่วงหน้าเพื่อวางแผนการเลื่อนตัวเลข นอกจากนี้เกม 15-puzzle นอกจากอยู่ในรูปแบบของตัวเลขแล้วยังสามารถเปลี่ยนแปลงจากตัวเลขให้เป็นภาพ คือ เปลี่ยนเป็นภาพ 1 ภาพและแบ่งออกเป็น 16 ส่วน แล้วให้ผู้เล่นทำการเลื่อนภาพเพื่อต่อให้เหมือนกับต้นฉบับ นอกจากนี้ในตัวอย่างมีการใช้บัซเซอร์ในการสร้างเสียงบี๊บโดยใช้ DAC ขนาด 8 บิตของไมโครคอนโทรลเลอร์ esp32 พร้อมทั้งการเขียนโปรแกรมเลือกใช้ภาษาไพธอนบน MicroPython เช่นเคย
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 สำหรับออกจากเกม
- การชนะเกมจะต้องเรียงให้เป็นดังนี้
1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | A | B | C |
D | E | F |
การสุ่มตัวเลข
การสุ่มตัวเลขใช้การสุ่มค่าจากรายการในลิสต์ซึ่งเก็บในตัวแปร tmp ซึ่งขั้นตอนวิธีเป็นดังนี้
- สร้างรายการ 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E และ F
- ล้างค่าตัวแปร randPuzzle ให้เป็นค่าว่าง
- ทำการสุ่ม 15 ครั้ง
- สุ่มเลือกข้อมูลจาก tmp
- นำข้อมูลที่สุ่มเลือกได้จัดเก็บลง randPuzzle
- ลบข้อมูลที่สุ่มได้ออกจาก tmp
- นำค่าตัวสุดท้ายที่เก็บใน tmp ไปเก็บใน randPuzzle
- นำค่าสุดท้ายออกจาก 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()
การเลื่อน
การเลื่อนอาศัยค่าจากการเลื่อนคันโยกของโมดูลจอยสติก โดยพิจารณาการเลื่อนจากตำแหน่งของช่องว่างดังเงื่อนไขต่อไปนี้
- ถ้าโยกคันโยกไปทางขวา และทางขวาของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง
- ถ้าโยกคันโยกไปทางซ้าย และทางซ้ายของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง
- ถ้าโยกคันโยกไปด้านบนและด้านบนของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง
- ถ้าโยกคันโลกลงด้านล่างและด้านล่างของช่องว่างมีข้อมูลจะทำการสลับข้อมูลนั้นกับช่องว่าง
โค้ดโปรแกรมส่วนของการเลื่อนเป็นดังนี้
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)
การชนะเกิดจากการตรวจสอบว่าค่าที่เก็บในตัวแปร 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