[EN] 15-Puzzle Game

This article is an example of a 15-Puzzle game using the ml4m board with the results shown in Figure 1. It is a game that allows players to practice their strategic thinking skills, looking ahead to plan their shifting numbers. In addition to being in the form of numbers, it can also be changed from numbers to images, that is, transformed into an image and divided into 16 parts, and then allow the player to move the image to make it the same as the original. Also in the example, a buzzer is used to generate a beep sound using an 8-bit DAC of an esp32 microcontroller, as well as programming in Python on MicroPython.

(Figure. 1 4×4 random)

15 Puzzle

The 15 puzzle game is a 4×4 grid game as shown in the first Figure, where each square contains the numbers 1 to 15 and one space. In this article, the numbers 1 to 9 and the letter A, B, C, D, E and F represent the numbers 10,11,12,13,14 and 15 because we want to save the display space because the display of the ml4m board is 128×64 dots.

ตำแหน่งค่า (แถว,คอลัมน์) ของข้อมูลแต่ละตำแหน่งดังนี้
Therefore, we use a list variable to store data items within a table called randPuzzle and create a posPuzzle variable to store values (rows, columns) of each data location as follows.

## store 4x4 random value
randPuzzle = [] 
## store value in x,y of randPuzzle ie. position 3 in randPuzzle is posPuzzle[3]
##  (3,0) mean first 3 column for position checking
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))

Rules

The rules of the game play are defined as follows.

  • The lever is used to move left, right, up or down.
  • swA to start a new game with randomize a new table
  • swB to exit the game
  • to win the game, the numbers must be arranged as follows.
1234
5678
9ABC
DEF

Random numbers

Random numbers are drawn from a list of values stored in a tmp variable. The algorithm is as follows.

  1. Create a list of 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E and F.
  2. Empty the randPuzzle variable.
  3. Do random 15 times.
    1. Randomly select data from tmp.
    2. Bring the randomly selected data to the randPuzzle.
    3. Remove random data from tmp.
  4. Take the last value stored in tmp and store it in randPuzzle.
  5. Remove the last value from tmp.

The code for randomly placing numbers in the table is as follows.

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) # random
        randPuzzle.append(data) # stored in table
        tmp.remove(data) # take chosen value
    randPuzzle.append(tmp[0]) # move the last to table
    tmp.pop() # remove
    tmp=[] # clear list
    gc.collect()

movement

Movement is based on moving the joystick module’s lever. By considering the shift from the position of the space as the following conditions.

  1. If the lever is moved to the right and the right of the space has data, it will swap that data with the space.
  2. If the lever is moved to the left and the left of the space has data, it will swap that data with the space.
  3. If the lever is moved to the top and the top of the space has data, it will swap that data with the space.
  4. If the lever is moved to the bottom and the bottom of the space has data, it will swap that data with the space.

The program code of the move section is as follows.

def moveControl():
    global randPuzzle
    while True:
        # find the position of "0"
        (xp,yp,p) = find0() 
        ## draw
        drawTable()
        ## check winner
        if (isWin()):
            youWin()
            break
        ## input
        (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)):
            #swap value
            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)):
            # swap value
            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)):
            # swap value
            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)):
            # swap value
            if (yp >0):
                pos = (yp-1)*numTiles+xp
                randPuzzle[p] = randPuzzle[pos]
                randPuzzle[pos] = "0" 
                beep()
        time.sleep_ms(100)

Win and lost

Losing is caused by the user pressing the swA to randomize the position and the screen will move to the right. After that, the message “u Lost” is displayed as shown in the Figure 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)
(Figure. 2 Example of losing screen or pressing SWA)

Winning is done by checking if the value stored in the randPuzzle variable matches the sort by the winPuzzle variable. If yes, then the player wins the game, otherwise, it returns 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    

The program code for the “You Win” display is as follows:

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)        

Code

From the part of the program mentioned above, it can be compiled into the program code as follows.

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

## store value in 4x4 table 
randPuzzle = [] 
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()
## display table
#######################################################
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()
## random new value
#######################################################
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) # choose
        randPuzzle.append(data) # stored in table
        tmp.remove(data) # remove
    randPuzzle.append(tmp[0]) # move the last to  table
    tmp.pop() # remove
    tmp=[] # clear list
    gc.collect()

    
#######################################################
## getInput()
## input from Joystick and  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:
        # find the position of "0"
        (xp,yp,p) = find0() 
        ## draw
        drawTable()
        ## winner checking
        if (isWin()):
            youWin()
            break
        ## input
        (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)):
            #swap
            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)):
            # swap
            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)):
            # swap
            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)):
            # swap
            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()

An example of playing is as follows:

Conclusion

In this example, game programming is a type of user interaction method. How the game works is based on the idea of ​​the programmer, not necessarily writing the same way as us. Plus, with the benefit of writing in Python, the code can be easily adapted for use on other platforms. More So we like to write in Python to create code to test ideas. After that, we can improve the performance or convert the code to a language that is faster or suitable for the platform you are using. So let’s continue to have fun with programming.

If you want to talk with us, feel free to leave commnets beloW!!

(C) 2020-2021, BY Jarut Busarathid and Danai Jedsadathitiukl

Updated 2021-12-02