[TH] เกม Tic-Tac-Toe

บทความนี้เป็นเก็บตกตัวอย่างเกมโอเอ็กซ์หรือ Tic-Tac-Toe ที่ทางทีมเราใช้ในการสอนวิชาพัฒนาเกมด้วยภาษาต่าง ๆ ตามความเหมาะสมกับกลุ่มผู้เรียน แต่ส่วนใหญ่จะใช้ภาษาไพธอนในการนำสอนเนื่องจากอธิบายและเขียนไปด้วยได้สะดวกกว่าภาษาอื่น ประกอบกับอยากให้มองเห็นแนวทางการนำไปใช้กับแพลตฟอร์มอื่น ๆ บ้าง ทางพวกเราจึงนำตัวอย่างมาใช้กับ MicroPython ของบอร์ดที่เราตั้งชื่อกันว่า ml4m ซึ่งมีที่มาจากบอร์ดนี้ติดตั้ง TensorFlow Lite บน ESP32 แบบ ROM 4MB โดยบอร์ดมีหน้าตาดังภาพที่ 1

ภาพที่ 1 บอร์ด ml4m บอร์ดทดสอบเกม Tic-Tac-Toe

จากภาพที่ 1 จะพบวา บอร์ด ml4m ประกอบด้วยส่วนนำเข้าข้อมูลทั้งที่เป็น JoyStick สวิตช์แบบสัมผัส สวิตช์แบบกด (เพาะสวิตช์ของจอยสติกเสีย ……. เหอะ ๆ) มีเซ็นเซอร์ BME280 (ที่ส่วนของความชื้นเสีย) และ MPU6050 สำหรับใช้อ่านค่าการเคลื่อนที่ของแกนต่าง ๆ พร้อมทั้งมีส่วนนำออก 2 ชิ้น คือ ลำโพง และ OLED ซึ่งการเชื่อมต่อกับ OLED ใช้ SCL ของ OLED ต่อเข้ากับขา GPIO22 และ SDA ของ OLED เข้ากับขา GPIO21 ดังภาพที่ 2

ภาพที่ 2 oled ที่เชื่อมต่อกับ GPIO22 และ GPIO21

การเชื่อมต่อของสวิตช์ทั้ง 4 คือ swA, swB, swC และ swD ต่อเข้ากับ GPIO32, GPIO33, GPIO34 และ GPIO35 ตามลำดับ ตามภาพที่ 3

ภาพที่ 3 swD, swA และ swB

เกม Tic-Tac-Toe

เกม Tic-Tac-Toe เป็นเกมกระดานสำหรับผู้เล่น 2 คน โดยลักษณะของเกม เป็นดังนี้
1. ตาราง ขนาด 3×3

   |   |
---+---+---
   |   |
---+---+---
   |   |
  1. เล่น 2 คน
    ผู้เล่น O
    อีกฝั่ง X
  2. การแพ้ชนะ ชนะ
    1. เรียงกันเป็นแถวแนวนอน
    1. เรียงกันเป็นแนวตั้ง
    2. เรียงกันในแนวทแยง

การเก็บข้อมูล

เกม OX ตาราง 3×3 ดังนั้น ข้อมูลต้องเป็นตาราง 3×3  โดยการเก็บตาราง ในภาษาไพธอนนั้นสามารถใช้ตัวแแปรประเภท list หรือ tuple ในการจัดเก็บ ตัวอย่างเป็นดังนี้ ซึ่งจะพบว่าตัวแปร table เป็นลิสต์ของ 3 สมาชิก โดยแต่ละสมาชิกมี 3 สมาชิกอยู่ภายใน แทนคอลัมน์แต่ละคอลัมน์ของแต่ละแถว พร้อมทั้งมีฟังก์ชัน showTable() เป็นการสั่งแสดงผลข้อมูลในตัวแปร table

# 1. ข้อมูล
table = [ # สร้างตัวแปรประเภท list
    [1,2,3], # ลิสต์แถวที่ 0
    [4,5,6], # ลิสต์แถวที่ 1
    [7,8,9]  # ลิสต์แถวที่ 2
# แต่ละแถวมี 3 คอลัมน์ : 0, 1, 2
]
# แสดงตาราง
def showTable():
    print("{}|{}|{}".format(table[0][0],table[0][1],table[0][2]));
    print("{}|{}|{}".format(table[1][0],table[1][1],table[1][2]));
    print("{}|{}|{}".format(table[2][0],table[2][1],table[2][2]));
    
# โปรแกรมหลัก
showTable()

การับข้อมูลการเดินจากผู้เล่น

การให้ผู้เล่นเลือกจุดวาง O ในตารางใช้คำสั่งนำเข้า input() ซึ่งผู้อ่านต้องทดลองการทำงานโดยพิมพ์ข้อมูลในหน่างคอนโซล โดยโค้ดโปรแกรมเป็นดังนี้

# 1. ข้อมูล
table = [ # สร้างตัวแปรประเภท list
    [1,2,3], # ลิสต์แถวที่ 0
    [4,5,6], # ลิสต์แถวที่ 1
    [7,8,9]  # ลิสต์แถวที่ 2
# แต่ละแถวมี 3 คอลัมน์ : 0, 1, 2
]
# แสดงตาราง
def showTable():
    print("{}|{}|{}".format(table[0][0],table[0][1],table[0][2]))
    print("{}|{}|{}".format(table[1][0],table[1][1],table[1][2]))
    print("{}|{}|{}".format(table[2][0],table[2][1],table[2][2]))


# รับค่าเพื่อเลือกตำแหน่งของ o, 1..9 เลือกตำแหน่ง .. 20 นาที
def getInput(who):
    while True: # by Mr. Kanokpon Sawasdee1
        showTable()
        try:
            position = int(input("choose(1...9)?"))
            if((position in table[0])or(position in table[1])or
               (position in table[2])):
                break
            else:
                print("1...9only")
                continue
        except:
            print("1...9only")
    r = 0
    if (position in table[0]):
        r = 0
    elif (position in table[1]):
        r = 1
    else:
        r = 2
    c = table[r].index(position)
    table[r][c] = who
    
# โปรแกรมหลัก
for i in range(4):
    getInput('o')
    print("------------------")
    getInput('x')
    print("------------------")
getInput('o')
showTable()

จากโค๊ดจะพบว่ามีการตรวจสอบประเภทของข้อมูลนำเข้าว่าโดยใช้ try และ except เป็นตัวดกความผิดพลาด เพื่อให้ได้เลข 1 ถึง 9 อันเป้นตัวแทนของช่องหมายเลข 1 ถึง 9 ในตารางเกม หลังจากนั้นตรวจสอบอีกครั้งว่าตำแหน่งที่เลือกยังไม่เคยถูกเลือกมาก่อน ถ้ามีการเลือกมาก่อนหน้านี้จะรายงานความผิดพลาดและให้เลือกใหม่ แต่ถ้าไม่เคยถูกเลือกจะเปลี่ยนแปลงค่าเป็น “O”

ตรวจสอบการแพ้ชนะ

การตรวจสอบการแพ้ชนะของเกมมีด้วยกัน 8 กรณี คือ

  1. แถวที่ 1 เป็น O หรือ X ทั้งหมด
  2. แถวที่ 2 เป็น O หรือ X ทั้งหมด
  3. แถวที่ 3 เป็น O หรือ X ทั้งหมด
  4. คอลัมน์ที่ 1 เป็น O หรือ X ทั้งหมด
  5. คอลัมน์ที่ 2 เป็น O หรือ X ทั้งหมด
  6. คอลัมน์ที่ 3 เป็น O หรือ X ทั้งหมด
  7. ตำแหน่ง 1,5 และ 9 เป็น O หรือ X ทั้งหมด
  8. ตำแหน่ง 3, 5 และ 7 เป้น O หรือ X ทั้งหมด

ตัวอย่างโค้ดการตรวจสอบของกรณี แถวแรกเป็น O ทั้งหมด เป็นดังนี้

# ตรวจสอบการชนะของ O
def checkWinO():
    # กรณี 1 [0] --> 'o','o','o'
    if ((table[0][0] == 'o') and
        (table[0][1] == 'o') and
        (table[0][2] == 'o')):
        print("O Win!")
        return True
    return False        

ตาเดินของคอมพิวเตอร์

เพื่อให้การเล่นคนเดียวสะดวกขึ้นเราจึงเขียนโค้ดให้คอมพิวเตอร์เป็นผู้เล่นที่ 2 โดยหลักการทำงานของโปรแกรมฝั่งคอมพิวเตอร์นั้นเป็นดังนี้

  1. สุ่มเลข 1 ถึง 9 หรือ 0 ถึง 8
  2. ตรวจสอบว่ามีข้อมูลการเลือกมาก่อนหรือไม่
    1. ถ้ามีให้ทำ 1 อีกครั้ง
    2. ถ้าไม่มีให้เปลี่ยนค่าตำแหน่งนั้นเป็น “X”

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

ตัวอย่างโปรแกรมต่อไปนี้ใช้การแสดงผลไปยังจอ OLED ดังภาพที่ 4 และเมื่อมีการเลือกตำแหน่งสลับกันไปกับคอมพิวเตอร์ดังภาพที่ 5 จะมีผลลัพธ์ดังตัวอย่างภาพที่ 6

#######################################################
### Tic-Tac-Toe
#######################################################
from machine import Pin,I2C
import math
import machine
import gc
import ssd1306
import random
import time

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

#######################################################
## OLED
sclPin = Pin(22)
sdaPin = Pin(21)
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 = 128
screenHeight = 64

table = [1,2,3,4,5,6,7,8,9]

def drawTable():
    oled.fill(0)
    left = (screenWidth - (blockWidth*3))//2
    top = (screenHeight - (blockHeight*3))//2
    for i in range(4):
        oled.hline(left-(blockWidth//4),(top-(blockHeight//4))+blockHeight*i, blockWidth*3,1)
        oled.vline((left-(blockWidth//4))+(i*blockWidth),top-(blockHeight//4), blockWidth*3,1)
    i = 0
    for r in range(3):
        for c in range(3):
            oled.text(str(table[i]),left+blockWidth*c,top+blockHeight*r)
            i += 1
    oled.show()
    
def isWin(p):
    win = False
    # case 1 : แถวแรกเป็น p
    if ((table[0]==p)and(table[1]==p)and(table[2]==p)):
        win = True
    # case 2 : แถวที่ 2 เป็น p
    if ((table[3]==p)and(table[4]==p)and(table[5]==p)):
        win = True
    # case 3 : แถวที่ 3 เป็น p
    if ((table[6]==p)and(table[7]==p)and(table[8]==p)):
        win = True
    # case 4 : คอลัมน์ 0 เป็น p
    if ((table[0]==p)and(table[3]==p)and(table[6]==p)):
        win = True
    # case 5 : คอลัมน์ 1 เป็น p
    if ((table[1]==p)and(table[4]==p)and(table[7]==p)):
        win = True
    # case 6 : คอลัมน์ 2 เป็น p
    if ((table[2]==p)and(table[5]==p)and(table[8]==p)):
        win = True
    # case 7 : แนวทแยงมุมซ้ายบนลงงล่างขวา
    if ((table[0]==p)and(table[4]==p)and(table[8]==p)):
        win = True
    # case 8 : แนวทแยงมุมจากซ้ายล่างขึ้นบนขวา
    if ((table[2]==p)and(table[4]==p)and(table[6]==p)):
        win = True
    return win    
#######################################################
### Main program
counter = 0
while True:
    # วาดตาราง
    drawTable()
    # รับค่า
    print("Human")
    while True:
        try:
            pos = int(input("ตำแหน่งที่ต้องการเลือก(1-9)?"))
        except:
            print("กรุณากรอก 1..9")
        if ((pos < 1) or (pos > 9)):
             print("กรุ่ณากรอก 1..9")
        else:
             if (table[pos-1] != pos):
                 print("กรุณาเลือกช่องอื่น")
             else:
                 table[pos-1] = "O"
                 break
    drawTable()
    # ตรวจสอบว่าคนชนะ ?
    if (isWin("O")):
        print("ขอแสดงความยินด้วยที่คุณชนะ")
        break
    # คอมพิวเตอร์เล่น
    print("Computer:")
    while True:
        pos = random.getrandbits(8)%9 # สุ่ม 0..8
        if (table[pos] == (pos+1)): # ถ้ายังว่าง
            table[pos] = "X"
            break
    # ตรวจสอบว่าคอมพิวเตอร์ชนะไหม?
    if (isWin("X")):
        print("ขอแสดงความเสียใจกับความพ่ายแพ้ของคุณ!")
        break
    # นับรอบการเล่น
    counter += 1
    if (counter == 4):
        break
drawTable()
time.sleep_ms(2000)
oled.poweroff()

ภาพที่ 4 ตารางที่แสดงบนจอ OLED
ภาพที่ 5 ตัวอย่างลำดับการเลือกตำแหน่งสลับกับคอมพิวเตอร์
ภาพที่ 6 ตัวอย่างผลลัพธ์ของการทำงานตามกลำดับการเลือกในนภาพที่ 5

สรุป

จากบทความนี้จะพบว่า เมื่อเรากำหนดเงื่อนไขการเลื่อน เงื่อนไขการคิด เงื่อนไขของการทำงาน หลังจากนั้นนำมาเขียนโค้ดจะทำให้การเขียนโค้ดนั้นทำงานได้หลากหลายระบบ ทั้งนี้เนื่องจากขั้นตอนการทำงานยังคงมีความคล้ายเดิม แต่เมื่อเปลี่ยนอุปกรณ์หรือแพลตฟอร์มสิ่งที่ต้องเพิ่มเข้ามาคือ ส่วนของการเขียนโค้ดเพื่อให้ใช้ได้กับแพลตฟอร์มนั้น อันได้แก่ วิธีการรำเข้าข้อมูลที่แตกต่างกัน และส่วนของการแสดงผล เช่นเดียวกับตัวอย่างเกม tic-tac-toe ที่ยกมาเป็นบทความในครั้งนี้ จะพบว่าหลักการเขียนเป็นภาษาไพธอน แต่เมื่อต้องการย้ายการแสดงผลไปที่ OLED จะต้องเขียนโค้ดเรื่องของการเชื่อมต่อกับ OLED พร้อมทั้งหาวิธีการเขียนโค้ดเพื่อแสดงผลให้เหมาะสมกับอุปกรณ์ เป็นต้น สุดท้ายนี้จะพบว่า การเล่นนั้นยังไม่สนุกเท่าไรนักเนื่องจากคอมพิวเตอร์สุ่มโดยไม่รู้จักป้องกัน และไม่เลือกจุดที่ดีที่สุด ดังนั้น ถ้าอยากให้เกมมีความสนุกเพิ่มขึ้น ท้าทายมากขึ้น ผู้อ่านต้องเพิ่มเติมวิธีคิดของคอมพิวเตอร์เข้าไปให้มากขึ้นกว่าการสุ่ม …. ขอให้สนุกกับการเขียนโปรแกรมครับ

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

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