บทความนี้เป็นการทดลองสร้างเกม Simple MineSweeper ดังภาพที่ 1 ซึ่งใช้บอร์ดไมโครคอนโทรลเลอร์ ESP32 กับจอแสดงผล st7735 แบบ REDTAB ขนาด 1.8″ ความละเอียดของการแสดงผลเป็น 128×160 อันเป็นฮาร์ดแวร์เดียวกับเกม Simple Tetris [ตอนที่ 1, ตอนที่ 2 และตอนที่ 3] ที่ได้กล่าวไปก่อนหน้านี้ โดยยังคงใช้ MicroPython เป็นหลักเช่นเดิม และการอธิบายจะเริ่มเป็นขั้นตอน ๆ ไป จากสร้างหน้าจอ สุ่มค่า การนับค่า การควบคุมการเคลื่อนที่ การเลื่อนกรอบตัวเลือก การปิดไม่ให้เห็นข้อมูล การสร้างความสัมพันธ์ระหว่างการระบุว่าตำแหน่งใดน่าจะเป็นระเบิด การเลือกเปิด และการนับคะแนนเมื่อจบเกม
ตัวเกม Simple MineSweeper เป็นเกมแรก ๆ ที่พวกเราทำเลียนแบบเพื่อศึกษาวิธีคิดและพัฒนาเทคนิคการเขียนโปรแกรมมาตั้งแต่ยุคระบบปฏิบัติการ DOS และ Windows ที่เป็น GUI ของ DOS ซึ่งตอนนั้นเขียนและทำงานบนระบบปฏิบัติการ DOS พร้อมทั้งต้องเปลี่ยนโหมดเป็นกราฟิกส์โหมด ติดต่อกับเมาส์ และสั่งวาดพิกเซลเอง (จะว่าไปแล้วก็เหมือนกันกับการเขียนบนบอร์ดไมโครคอนโทรลเลอร์ ESP32 แหละครับ แต่ไม่มีระบบปฏิบัติการให้ใช้) … ว่าแล้วมาทดลองสร้างกันดีกว่าครับ ดูจะรำลึกอดีตกันเนิ่นนานเลยทีเดียว
เริ่มสร้าง
เนื้อหาที่เขียนมี 2 ส่วนหลัก คือ ส่วนแรกเป็นการพัฒนาตัวระบบหลังบ้านของเกม อันได้แก่ โครงสร้างข้อมูลสำหรับเก็บตำแหน่งของระเบิด การนับว่ารอบตัวมีระเบิดเท่าไร การวาดหน้าจอเพื่อดูผลลัพธ์ของการทำงานในส่วนของการสุ่มตำแหน่งและการนับจำนวนระเบิดรอบตัว และจบที่การเคลื่อนที่ด้วยการกดสวิตช์เลื่อนซ้าย ขวา บน และล่างเพื่อใช้สำหรับเลือกตำแหน่งของช่อง
โครงสร้างข้อมูล
การออกแบบเกมที่อยู่บนจอแสดงผล 128×160 กับตัวอักษรที่เลือกใช้มีขนาด 16×16 ทำให้ทางทีมงานต้องเลือกใช้ตารางขนาด 8×6 ช่องเป็นตารางของเกม และสร้างตัวแปรสำหรับเก็บข้อมูลตารางชื่อ table ดังนี้
table = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
ข้อมูลที่เก็บใน table ประกอบด้วยตัวเลขที่มีความหมายดังนี้
- 0 หมายถึงเป็นช่องว่าง หรือ รอบตัวไม่มีระเบิด
- 1-8 หมายถึง รอบตัวมีระเบิดจำนวน 1, 2, 3, 4, 5, 6, 7 หรือ 8 ลูก ตามค่าที่ระบุ
- 9 หมายถึงตำแหน่งของระเบิด
การสุ่มระเบิด
จากตาราง table สำหรับเก็บข้อมูลตำแหน่งระเบิด โดยกำหนดให้ถ้าสุ่มแล้วระเบิดอยู่ตำแหน่งใด ให้ช่องนั้นเก็บค่า 9 และเพื่อความสะดวกในการทดสอบการแสดงผล พวกเรากำหนดให้แสดงระเบิดเป็นช่องสีแดง และใส่เครื่องหมาย * ประกอบดังภาพที่ 2 และ 3
ขั้นตอนวิธีของการสุ่มเป็นดังนี้
- กำหนดให้จำนวนระเบิดที่สุ่มสำเร็จไปแล้วเป็น 0
- สุ่มค่า x และ y ถ้ายังไม่พบระเบิดใน table ที่แถว y คอลัมน์ x ให้ช่องนั้นเก็บค่า 9 และเพิ่มค่าจำนวนระเบิดที่สุ่มสำเร็จ
- กลับไป 1 จนกว่าจะได้จำนวนระเบิดตามที่กำหนด
โค้ดการสุ่มเป็นดังนี้
table = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
counter = 0
while (counter < 10):
x = random.randint(1,maxCol)-1
y = random.randint(1,maxRow)-1
if (table[y][x] == 0):
# print("Bomb no.{} at ({},{}).".format(counter+1,x,y))
table[y][x] = 9
counter+=1
การนับระเบิดที่อยู่รอบตัวเอง
เมื่อได้จำนวนระเบิดในตารางครบตามที่กำหนด ขั้นตอนถัดไปคือ ทำการเข้าไปในแต่ละช่องของตารางที่ไม่ใช่ตำแหน่งของระเบิด และนับว่ารอบข้างของตำแหน่งนั้นมีระเบิดเป็นจำนวนกี่ลูก ดังตัวอย่างผลลัพธ์การนับในภาพที่ 4 (แสดงผลตำแหน่งของระเบิดเป็นสี่เหลี่ยมทึบสีแดง)
จากภาพที่ 4 ถ้ากำหนดให้อยู่ที่แถว 0 คอลัมน์ 0 จะพบว่ารอบตัวเองไม่มีระเบิดจึงรายลงานเป็นช่องว่าง (จำนวนระเบิดเป็น 0) แต่ในแถวที่ 5 คอลัมน์ที่ 0 พบจำนวนระเบิด 2 ลูก คือ จากแถวดัานบน 1 ลูก และด้านล่าง 1 ลูก ส่วนตัวอย่างของแถวที่ 5 คอลัมน์ที่ 5 พบระเบิดจำนวน 3 ลูกจากด้านซ้าย ด้านบน และด้านล่าง เป็นต้น
ขั้นตอนวิธีการนับระเบิดกระทำโดยแบ่งกรณีของการนับเป็น ดังนี้
- เป็นแถวแรก
- เป็นคอลัมน์แรกในแถวแรก ให้ดูจากด้านขวา ด้านล่าง และขวาล่าง
- เป็นคอลัมน์สุดท้ายในแถวแรก ให้ดูจากด้านซ้าย ด้านซ้ายล่าง และด้านล่าง
- เป็นคอลัมน์อื่น ๆ ในแถวแรก ให้ดูจากด้านซ้าย ด้านขวา ด้านซ้ายล่าง ด้านล่าง และด้านขวาล่าง
- เป็นแถวสุดท้าย
- เป็นคอลัมน์แรกในแถวสุดท้าย ให้ดูจากด้านบน ด้านขวาบน และด้านขวา
- เป็นคอลัมน์สุดท้ายในแถวสุดท้าย ให้ดูจากด้านซ้าย ด้านซ้ายบน และด้านบน
- เป็นคอลัมน์อื่น ๆ ในแถวสุดท้าย ให้ดูจากด้านซ้าย ด้านซ้ายบน ด้านบน ด้านขวาบน และด้านขวา
- เป็นแถวอื่น ๆ
- เป็นคอลัมน์แรก ให้ดูจากด้านบน ด้านขวาบน ด้านขวา ด้านขวาล่าง และด้านล่าง
- เป็นคอลัมน์สุดท้าย ให้ดูจากด้านซ้าย ด้ายซ้ายบน ด้านบน ด้านล่าง และด้านซ้ายล่าง
- เป็นคอลัมน์อื่น ๆ ให้ดูจากด้านซ้าย ด้านซ้ายบน ด้านบน ด้านขวาบน ด้านขวา ด้านล่างขวา ด้านล่าง และด้านล่างซ้าย
ตัวอย่างโค้ดของการนับ คือ
## เติมตัวเลข
for row in range(maxRow):
for col in range(maxCol):
sum = 0
if (table[row][col] != 9):
if (row == 0): # แถวแรก
if col == 0: # คอลัมน์แรก
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif (row == maxRow-1): # แถวสุดท้าย
if col == 0: # คอลัมน์แรก
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
else:
if col == 0: # คอลัมน์แรก
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
table[row][col] = sum
การวาดหน้าจอเพื่อทดสอบ
การวาดข้อมูลจาก table บนจอแสดงผลดังตัวอย่างในภาพที่ 5 เป็นดังนี้
w = 20
h = 20
for row in range(6):
y = row*h+4+2
for col in range(8):
x = col*w+2
if table[row][col]:
if (table[row][col]== 9):
scr.fill_rect(x,y,w-4,h-4, bgColor)
scr.text(font,"*",x,y,tft.YELLOW,bgColor)
else:
scr.fill_rect(x,y,w-4,h-4, tft.BLUE)
scr.text(font,"{}".format(table[row][col]),x,y,tft.WHITE,tft.BLUE)
else:
scr.fill_rect(x,y,w-4,h-4, tft.BLUE)
การเคลื่อนที่
การเคลื่อนที่กระทำโดยการกดสวิตช์ซ้าย ขวา บน หรือล่าง เพื่อย้ายตำแหน่งของกรอบสี่เหลี่ยมดังในภาพที่ 6 โดยหลักการของการทำงานจะอาศัยการจำตำแหน่งล่าสุดของช่องปัจจุบันด้วยตัวแปร posX และ posY พร้อมทั้งเขียนฟังก์ชันสำหรับวาด/ลบกรอบสี่เหลี่ยมเอาไว้ดังนี้
def cursor(marked=True):
w = 20
h = 20
if marked:
scr.rect(posX*w,posY*h+4,w,h,tft.WHITE)
else:
scr.rect(posX*w,posY*h+4,w,h,tft.BLACK)
ส่วนโค้ดของการตอบสนองการกดสวิตช์ทิศทางทั้ง 4 ทิศเขียนไว้ดังนี้
#######################################################
##### Input : เลื่อนไปทางซ้าย
#######################################################
if keL.value() == 0:
#updated = True
if (posX > 0):
cursor(False)
posX -= 1
cursor()
#######################################################
##### Input : เลื่อนไปทางขวา
#######################################################
if keR.value() == 0:
#updated = True
if (posX < maxCol-1):
cursor(False)
posX += 1
cursor()
#######################################################
##### Input : เลื่อนไปด้านบน
#######################################################
if keU.value() == 0:
#updated = True
if (posY > 0):
cursor(False)
posY -= 1
cursor()
#######################################################
##### Input : เลื่อนไปด้านล่าง
#######################################################
if keD.value() == 0:
#updated = True
if (posY < maxRow-1):
cursor(False)
posY += 1
cursor()
ตัวอย่างโปรแกรมในส่วนที่ 1
โค้ดโปรแกรมสำหรับส่วนแรกเป็นดังนี้
#################################################################
# mineSweep Part 1
# JarutEx 2021-11-12
# 128x160
#################################################################
import gc
import os
import sys
import time
import machine as mc
from machine import Pin,SPI, Timer
import math
import st7735 as tft
import vga1_16x16 as font
import random
#################################################################
###### setting ##################################################
#################################################################
gc.enable()
gc.collect()
mc.freq(240000000)
spi = SPI(2, baudrate=26000000,
sck=Pin(14), mosi=Pin(12),
polarity=0, phase=0)
#### Key
keL = Pin(39, Pin.IN, Pin.PULL_UP)
keU = Pin(34, Pin.IN, Pin.PULL_UP)
keD = Pin(35, Pin.IN, Pin.PULL_UP)
keR = Pin(32, Pin.IN, Pin.PULL_UP)
swM1 = Pin(33, Pin.IN, Pin.PULL_UP)
swM2 = Pin(25, Pin.IN, Pin.PULL_UP)
swA = Pin(26, Pin.IN, Pin.PULL_UP)
swB = Pin(27, Pin.IN, Pin.PULL_UP)
spk = Pin(19,Pin.OUT)
spk.on()
time.sleep(0.1)
spk.off()
# dc, rst, cs
scr = tft.ST7735(spi, 128, 160, dc=Pin(15, Pin.OUT), reset=Pin(13,Pin.OUT), cs=Pin(2, Pin.OUT), rotation=3)
scr.initr()
Tmr = Timer(0)
Render = Timer(1)
bgColor = tft.color565(0x96,0x24,0x24)
updated = False
gameOver = False
maxRow = const(6)
maxCol = const(8)
table = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
posX = 0
posY = 0
#################################################################
###### sub modules ##############################################
#################################################################
def splash():
scr.fill(bgColor)
scr.text(font,"mine", 20, 42, tft.YELLOW, bgColor)
for i in range(200):
color = tft.color565(55+i,55+i,55+i)
scr.text(font,"Sweep", 56, 60, color, bgColor)
time.sleep_ms(10)
time.sleep_ms(2000)
def randomTable():
global table
table = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
counter = 0
while (counter < 10):
x = random.randint(1,maxCol)-1
y = random.randint(1,maxRow)-1
if (table[y][x] == 0):
# print("Bomb no.{} at ({},{}).".format(counter+1,x,y))
table[y][x] = 9
counter+=1
## เติมตัวเลข
for row in range(maxRow):
for col in range(maxCol):
sum = 0
if (table[row][col] != 9):
if (row == 0): # แถวแรก
if col == 0: # คอลัมน์แรก
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif (row == maxRow-1): # แถวสุดท้าย
if col == 0: # คอลัมน์แรก
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
else:
if col == 0: # คอลัมน์แรก
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
table[row][col] = sum
def cbInput(x):
global updated, posX, posY
#######################################################
##### Input
#######################################################
if (swM2.value() == 0):
randomTable()
updated = True
#######################################################
##### Input : เลื่อนไปทางซ้าย
#######################################################
if keL.value() == 0:
#updated = True
if (posX > 0):
cursor(False)
posX -= 1
cursor()
#######################################################
##### Input : เลื่อนไปทางขวา
#######################################################
if keR.value() == 0:
#updated = True
if (posX < maxCol-1):
cursor(False)
posX += 1
cursor()
#######################################################
##### Input : เลื่อนไปด้านบน
#######################################################
if keU.value() == 0:
#updated = True
if (posY > 0):
cursor(False)
posY -= 1
cursor()
#######################################################
##### Input : เลื่อนไปด้านล่าง
#######################################################
if keD.value() == 0:
#updated = True
if (posY < maxRow-1):
cursor(False)
posY += 1
cursor()
def cbRender(x):
global updated
#######################################################
##### อัพเดตหน้าจอ
#######################################################
if (updated):
draw()
updated = False
def draw():
w = 20
h = 20
for row in range(6):
y = row*h+4+2
for col in range(8):
x = col*w+2
if table[row][col]:
if (table[row][col]== 9):
scr.fill_rect(x,y,w-4,h-4, bgColor)
scr.text(font,"*",x,y,tft.YELLOW,bgColor)
else:
scr.fill_rect(x,y,w-4,h-4, tft.BLUE)
scr.text(font,"{}".format(table[row][col]),x,y,tft.WHITE,tft.BLUE)
else:
scr.fill_rect(x,y,w-4,h-4, tft.BLUE)
def cursor(marked=True):
w = 20
h = 20
if marked:
scr.rect(posX*w,posY*h+4,w,h,tft.WHITE)
else:
scr.rect(posX*w,posY*h+4,w,h,tft.BLACK)
#################################################################
###### main program #############################################
#################################################################
#splash()
randomTable()
scr.fill(tft.BLACK)
draw()
cursor()
Tmr.init( period=200, mode=Timer.PERIODIC, callback=cbInput)
Render.init( period=100, mode=Timer.PERIODIC, callback=cbRender)
while swM1.value():
pass
scr.fill(0)
Tmr.deinit()
Render.deinit()
spi.deinit()
ปรับปรุง
จากโค้ดโปรแกรมในส่วนแรกที่เรียกว่าเป็นการทำงานของหลังบ้าน ทำให้ผู้พัฒนาโปรแกรมมองเห็นว่าตำแหน่งใดเป็นข้อมูลใด แต่ถ้าผู้เล่นมองเห็นข้อมูลเหล่านี้ก็ไม่เกิดความท้าทาย จึงต้องมีหน้าจอสำหรับปิดไม่ให้ผู้เล่นมองเห็นดังภาพที่ 6
โครงสร้างข้อมูล 2
เพื่อเป็นการสร้างความท้ายทายและให้ผู้เล่นได้คิดเพื่อคาดเดาตำแหน่งของระเบิดว่าอยู่ ณ ช่องใดบ้าง จึงสร้างตัวแปรสำหรับเป็นการเก็บสถานะของแต่ละช่องเอาไว้ในชื่อตัวแปร plate ซึ่งมีค่าที่กำหนดไว้ดังนี้
- 0 หมายถึงยังไม่ถูกเปิด
- 1 หมายถึงถูกเปิดไปแล้ว
- 2 หมายถึงตำแหน่งที่ผู้เล่นกำหนดไว้ว่าน่าจะเป็นตำแหน่งของระเบิด
โครงสร้างข้อมูลของ plate เป็นดังนี้ โดบในการสุ่มค่าตำแหน่งของระเบิดจะกำหนดให้ค่า plate เป็น 0 ในทุกช่อง
plate = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
การมาร์กระเบิด
ตัวอย่างเกมที่ออกแบบนี้กำหนดไว้ว่าถ้าตำแหน่งใดเป็นจุดที่ผู้เล่นคิดว่าเป็นระเบิด ให้กด swB เพื่อเปลี่ยนสถานะของช่องนั้นเป็นตำแหน่งที่คาดว่าเป็นระเบิด พร้อมทั้งแสดงเครื่องหมาย ! ดังภาพที่ 7 ประกอบเอาไว้ให้ทราบ
จากภาพที่ 7 ได้เพิ่มเติมการนับจำนวนการมาร์กระเบิดเก็บไว้ในตัวแปร markCounter และถ้าผู้เล่นกดซ้ำที่ตำแหน่งเดิมจะหมายถึงยกเลิกการมาณืกตำแหน่ง ดังโค้ดโปรแกรมดังนี้
#######################################################
##### Input : คิดว่าเป็นระเบิด
#######################################################
if swB.value() == 0:
if (plate[posY][posX] == 0):
if (markCounter < maxBomb): # ต้องมีไม่มากกว่าจำนวนระเบิด
plate[posY][posX] = 2
markCounter += 1
updated = True
elif (plate[posY][posX] == 2):
plate[posY][posX] = 0
markCounter -= 1
updated = True
การเปิด
การเปิดเป็นการเปลี่ยนสถานะของ plate ให้เป็น 1 และไปตรวจสอบค่าของตัวแปร table ว่า ณ ตำแหน่งนั้นมีค่าเป็นอะไร และถ้าเป็น 9 หมายถึงระเบิดจะเป็นการจบเกม แต่ถ้าไม่ใช่จะแสดงค่าของจำนวนระเบิดตามตัวอย่างของภาพที่ 1 และ 8
จากภาพที่ 8 สามารถเขียนโค้ดสำหรับการตอบสนองการกด swA ได้ดังนี้
#######################################################
##### Input : เลือก
#######################################################
if swA.value() == 0:
if (plate[posY][posX] == 0):
if (table[posY][posX] == 9):
gameOver = True
plate[posY][posX] = 1
updated = True
ส่วนการแสดงผลได้ถูกเปลี่ยนใหม่ คือ ถ้าช่องใด plate เป็น 0 จะแสดงช่องทึบ ถ้าเป็น 1 จะนำค่าจาก table มาแสดง และถ้าเป็น 2 จะแสดง ! ดังภาพที่ 7 ซึ่งโค้ดโปรแกรมเป็นดังนี้
w = 20
h = 20
for row in range(6):
y = row*h+4+2
for col in range(8):
x = col*w+2
if plate[row][col]:
if (plate[row][col] == 1):
if (table[row][col]== 9):
scr.fill_rect(x,y,w-4,h-4, bgColor)
scr.text(font,"*",x,y,tft.YELLOW,bgColor)
else:
scr.fill_rect(x,y,w-4,h-4, tft.BLUE)
if (table[row][col] > 0):
scr.text(font,"{}".format(table[row][col]),x,y,tft.WHITE,tft.BLUE)
else:
scr.fill_rect(x,y,w-4,h-4, tft.GRAY)
scr.text(font,"!",x,y,tft.BLACK,tft.GRAY)
else:
scr.fill_rect(x,y,w-4,h-4, tft.GRAY)
การนับคะแนน
การนับคะแนนเป็นการนับจากจำนวนการคาดเดาระเบิดของผู้เล่น และนำมาแสดงผลเพื่อจบเกมดังตัวอย่างในภาพที่ 9 ซ฿่งหลักการคิดเป็นดังโค้ดโปรแกรมต่อไปนี้ นั่นคือ ถ้าช่องใดของ plate ที่มีค่าเป็น 2 และไปตรงกับ table ที่เก็บค่า 9 อันเป็นค่าของตำแหน่งที่มาร์กว่าเป็นระเบิดกับตำแหน่งของระเบิด จะทำการนับคะแนนเพิ่มขึ้น 1 คะแนน
score = 0
for i in range(maxRow):
for j in range(maxCol):
if ((plate[i][j] == 2) and (table[i][j] == 9)):
score += 1
ตัวอย่างโปรแกรม
จากเนื้อหาทั้งหมดสามารถนำมาต่อกันเป็นโค้ดโปรแกรมได้ดังนี้
#################################################################
# mineSweep Part2
# JarutEx 2021-11-12
# 128x160
#################################################################
import gc
import os
import sys
import time
import machine as mc
from machine import Pin,SPI, Timer
import math
import st7735 as tft
import vga1_16x16 as font
import random
#################################################################
###### setting ##################################################
#################################################################
gc.enable()
gc.collect()
mc.freq(240000000)
spi = SPI(2, baudrate=26000000,
sck=Pin(14), mosi=Pin(12),
polarity=0, phase=0)
#### Key
keL = Pin(39, Pin.IN, Pin.PULL_UP)
keU = Pin(34, Pin.IN, Pin.PULL_UP)
keD = Pin(35, Pin.IN, Pin.PULL_UP)
keR = Pin(32, Pin.IN, Pin.PULL_UP)
swM1 = Pin(33, Pin.IN, Pin.PULL_UP)
swM2 = Pin(25, Pin.IN, Pin.PULL_UP)
swA = Pin(26, Pin.IN, Pin.PULL_UP)
swB = Pin(27, Pin.IN, Pin.PULL_UP)
spk = Pin(19,Pin.OUT)
spk.on()
time.sleep(0.1)
spk.off()
# dc, rst, cs
scr = tft.ST7735(spi, 128, 160, dc=Pin(15, Pin.OUT), reset=Pin(13,Pin.OUT), cs=Pin(2, Pin.OUT), rotation=3)
scr.initr()
Tmr = Timer(0)
Render = Timer(1)
bgColor = tft.color565(0x96,0x24,0x24)
updated = False
gameOver = False
maxRow = const(6)
maxCol = const(8)
maxBomb = const(7)
table = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
plate = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
posX = 0
posY = 0
markCounter = 0
#################################################################
###### sub modules ##############################################
#################################################################
def splash():
scr.fill(bgColor)
scr.text(font,"mine", 20, 42, tft.YELLOW, bgColor)
for i in range(200):
color = tft.color565(55+i,55+i,55+i)
scr.text(font,"Sweep", 56, 60, color, bgColor)
time.sleep_ms(10)
time.sleep_ms(2000)
def randomTable():
global table,plate,markCounter
table = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
plate = [
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]
]
counter = 0
markCounter=0
while (counter < maxBomb):
x = random.randint(1,maxCol)-1
y = random.randint(1,maxRow)-1
if (table[y][x] == 0):
# print("Bomb no.{} at ({},{}).".format(counter+1,x,y))
table[y][x] = 9
counter+=1
## เติมตัวเลข
for row in range(maxRow):
for col in range(maxCol):
sum = 0
if (table[row][col] != 9):
if (row == 0): # แถวแรก
if col == 0: # คอลัมน์แรก
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif (row == maxRow-1): # แถวสุดท้าย
if col == 0: # คอลัมน์แรก
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
else:
if col == 0: # คอลัมน์แรก
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
elif col == maxCol-1: # คอลัมน์สุดท้าย
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
else: # คอลัมน์อื่น ๆ
if (table[row-1][col-1]==9): # มุมซ้ายบน
sum += 1
if (table[row-1][col]==9): # ด้านบน
sum += 1
if (table[row-1][col+1]==9): # มุมขวาบน
sum += 1
if (table[row][col-1]==9): # ซ้าย
sum += 1
if (table[row][col+1]==9): # ขวา
sum += 1
if (table[row+1][col-1]==9): # มุมซ้ายล่าง
sum += 1
if (table[row+1][col]==9): # ด้านล่าง
sum += 1
if (table[row+1][col+1]==9): # มุมขวาล่าง
sum += 1
table[row][col] = sum
def cbInput(x):
global updated, posX, posY, table, plate, gameOver, markCounter
#######################################################
##### Input
#######################################################
if (swM2.value() == 0):
randomTable()
updated = True
#######################################################
##### Input : เลื่อนไปทางซ้าย
#######################################################
if keL.value() == 0:
#updated = True
if (posX > 0):
cursor(False)
posX -= 1
cursor()
#######################################################
##### Input : เลื่อนไปทางขวา
#######################################################
if keR.value() == 0:
#updated = True
if (posX < maxCol-1):
cursor(False)
posX += 1
cursor()
#######################################################
##### Input : เลื่อนไปด้านบน
#######################################################
if keU.value() == 0:
#updated = True
if (posY > 0):
cursor(False)
posY -= 1
cursor()
#######################################################
##### Input : เลื่อนไปด้านล่าง
#######################################################
if keD.value() == 0:
#updated = True
if (posY < maxRow-1):
cursor(False)
posY += 1
cursor()
#######################################################
##### Input : เลือก
#######################################################
if swA.value() == 0:
if (plate[posY][posX] == 0):
if (table[posY][posX] == 9):
gameOver = True
plate[posY][posX] = 1
updated = True
#######################################################
##### Input : คิดว่าเป็นระเบิด
#######################################################
if swB.value() == 0:
if (plate[posY][posX] == 0):
if (markCounter < maxBomb): # ต้องมีไม่มากกว่าจำนวนระเบิด
plate[posY][posX] = 2
markCounter += 1
updated = True
elif (plate[posY][posX] == 2):
plate[posY][posX] = 0
markCounter -= 1
updated = True
def cbRender(x):
global updated
#######################################################
##### อัพเดตหน้าจอ
#######################################################
if (updated):
draw()
updated = False
def draw():
w = 20
h = 20
for row in range(6):
y = row*h+4+2
for col in range(8):
x = col*w+2
if plate[row][col]:
if (plate[row][col] == 1):
if (table[row][col]== 9):
scr.fill_rect(x,y,w-4,h-4, bgColor)
scr.text(font,"*",x,y,tft.YELLOW,bgColor)
else:
scr.fill_rect(x,y,w-4,h-4, tft.BLUE)
if (table[row][col] > 0):
scr.text(font,"{}".format(table[row][col]),x,y,tft.WHITE,tft.BLUE)
else:
scr.fill_rect(x,y,w-4,h-4, tft.GRAY)
scr.text(font,"!",x,y,tft.BLACK,tft.GRAY)
else:
scr.fill_rect(x,y,w-4,h-4, tft.GRAY)
def cursor(marked=True):
w = 20
h = 20
if marked:
scr.rect(posX*w,posY*h+4,w,h,tft.WHITE)
else:
scr.rect(posX*w,posY*h+4,w,h,tft.BLACK)
#################################################################
###### main program #############################################
#################################################################
#splash()
randomTable()
scr.fill(tft.BLACK)
draw()
cursor()
Tmr.init( period=150, mode=Timer.PERIODIC, callback=cbInput)
Render.init( period=33, mode=Timer.PERIODIC, callback=cbRender)
while (swM1.value() and (gameOver==False)):
pass
Tmr.deinit()
Render.deinit()
if (gameOver):
draw()
score = 0
for i in range(maxRow):
for j in range(maxCol):
if ((plate[i][j] == 2) and (table[i][j] == 9)):
score += 1
scr.fill(tft.BLACK)
scr.text(font,"Score={}".format(score),8,8,tft.WHITE,tft.BLACK)
spi.deinit()
สรุป
จากบทความนี้จะพบว่าเกม Simple MineSweeper ยังต้องมีการปรับปรุงการทำงานที่สำคัญงานหนึ่งคือ กรณีที่ผู้เล่นกดเลือกแล้วโดนบริเวณพื้นที่ว่าง หรือเก็บค่า 0 เอาไว้ ตัวเกมจะต้องเปิดพื้นที่ที่เป็นที่ว่างที่ติดกันให้เห็นออกทั้งหมด และการเก็บสถิติในการเล่น ส่วนการทำงานหลักของตัวอย่างในครั้งนี้คงทำให้ผู้ที่นำไปศึกษาต่อทำการปรับปรุงได้ไม่ยากนัก สุดท้ายนี้หวังว่าตัวอย่างของบทความนี้คงมีประโยชน์และสร้างแนวคิดให้กับผู้ที่สนใจพัฒนาเกมแบบทำเองง่าย ๆ และสามารถทำได้ด้วยตนเอง ขอให้สนุกกับการเขียนโปรแกรมครับ
(C) 2020-2022, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-11-12, 2022-01-18