[TH] Simple MineSweeper

บทความนี้เป็นการทดลองสร้างเกม Simple MineSweeper ดังภาพที่ 1 ซึ่งใช้บอร์ดไมโครคอนโทรลเลอร์ ESP32 กับจอแสดงผล st7735 แบบ REDTAB ขนาด 1.8″ ความละเอียดของการแสดงผลเป็น 128×160 อันเป็นฮาร์ดแวร์เดียวกับเกม Simple Tetris [ตอนที่ 1, ตอนที่ 2 และตอนที่ 3] ที่ได้กล่าวไปก่อนหน้านี้ โดยยังคงใช้ MicroPython เป็นหลักเช่นเดิม และการอธิบายจะเริ่มเป็นขั้นตอน ๆ ไป จากสร้างหน้าจอ สุ่มค่า การนับค่า การควบคุมการเคลื่อนที่ การเลื่อนกรอบตัวเลือก การปิดไม่ให้เห็นข้อมูล การสร้างความสัมพันธ์ระหว่างการระบุว่าตำแหน่งใดน่าจะเป็นระเบิด การเลือกเปิด และการนับคะแนนเมื่อจบเกม

ตัวเกม Simple MineSweeper เป็นเกมแรก ๆ ที่พวกเราทำเลียนแบบเพื่อศึกษาวิธีคิดและพัฒนาเทคนิคการเขียนโปรแกรมมาตั้งแต่ยุคระบบปฏิบัติการ DOS และ Windows ที่เป็น GUI ของ DOS ซึ่งตอนนั้นเขียนและทำงานบนระบบปฏิบัติการ DOS พร้อมทั้งต้องเปลี่ยนโหมดเป็นกราฟิกส์โหมด ติดต่อกับเมาส์ และสั่งวาดพิกเซลเอง (จะว่าไปแล้วก็เหมือนกันกับการเขียนบนบอร์ดไมโครคอนโทรลเลอร์ ESP32 แหละครับ แต่ไม่มีระบบปฏิบัติการให้ใช้) … ว่าแล้วมาทดลองสร้างกันดีกว่าครับ ดูจะรำลึกอดีตกันเนิ่นนานเลยทีเดียว

ภาพที่ 1 เกม simple mineSweeper

เริ่มสร้าง

เนื้อหาที่เขียนมี 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

ภาพที่ 2 ตัวอย่างการสุ่มตำแหน่งระเบิด
ภาพที่ 3 ตัวอย่างการสุ่มตำแหน่งระเบิด #2

ขั้นตอนวิธีของการสุ่มเป็นดังนี้

  1. กำหนดให้จำนวนระเบิดที่สุ่มสำเร็จไปแล้วเป็น 0
  2. สุ่มค่า x และ y ถ้ายังไม่พบระเบิดใน table ที่แถว y คอลัมน์ x ให้ช่องนั้นเก็บค่า 9 และเพิ่มค่าจำนวนระเบิดที่สุ่มสำเร็จ
  3. กลับไป 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 ตัวอย่างการแสดงการนับจำนวนระเบิดรอบตัว

จากภาพที่ 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)
ภาพที่ 5 ตัวอย่างการแสดงตำแหน่งระเบิดและค่าการนับจำนวนระเบิด

การเคลื่อนที่

การเคลื่อนที่กระทำโดยการกดสวิตช์ซ้าย ขวา บน หรือล่าง เพื่อย้ายตำแหน่งของกรอบสี่เหลี่ยมดังในภาพที่ 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

ภาพที่ 6 ตัวอย่างหน้าจอการแสดงข้อมูลจาก plate

เพื่อเป็นการสร้างความท้ายทายและให้ผู้เล่นได้คิดเพื่อคาดเดาตำแหน่งของระเบิดว่าอยู่ ณ ช่องใดบ้าง จึงสร้างตัวแปรสำหรับเป็นการเก็บสถานะของแต่ละช่องเอาไว้ในชื่อตัวแปร 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 ตัวอย่างช่องที่ถูกมารฺกว่าน่าจะเป็นระเบิด

จากภาพที่ 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 เพื่อเปิดช่องของ plate

จากภาพที่ 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
ภาพที่ 9 ตัวอย่างหน้าจอแสดงคะแนน

ตัวอย่างโปรแกรม

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

#################################################################
# 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