sokoban.py

Created by arthurjacquin

Created on July 10, 2021

5.18 KB


# Sokoban - June 2020
# Arthur Jacquin (arthur@jacquin.xyz)
# https://github.com/arthur-jacquin/numworks-games

# Modules
from ion import keydown
from kandinsky import *
from time import sleep

# Levels
LEVELS = ('4-5#10-|4-#3-#10-|4-#$--#10-|--3#--$##9-|--#--$-$-#9-|3#-#-##-#3-6#|#3-#-##-5#--..#|#-$--$10-..#|5#-3#-#@##--..#|4-#5-9#|4-7#8-',
    '12#--|#..--#5-3#|#..--#-$--$--#|#..--#$4#--#|#..4-@-##--#|#..--#-#--$-##|6#-##$-$-#|--#-$--$-$-$-#|--#4-#5-#|--12#',
    '8-8#-|8-#5-@#-|8-#-$#$-##-|8-#-$--$#--|8-##$-$-#--|9#-$-#-3#|#4.--##-$--$--#|##3.4-$--$3-#|#4.--10#|8#9-',
    '11-8#|11-#--4.#|12#--4.#|#4-#--$-$3-4.#|#-3$#$--$-#--4.#|#--$5-$-#--4.#|#-$$-#$-$-$8#|#--$-#5-#7-|##-9#7-|#4-#4-##7-|#5-$3-##7-|#--$$#$$--@#7-|#4-#4-##7-|11#8-',
    '8-5#4-|8-#3-5#|8-#-#$##--#|8-#5-$-#|9#-3#3-#|#4.--##-$--$3#|#4.4-$-$$-##-|#4.--##$--$-@#-|9#--$--##-|8-#-$-$--#-|8-3#-##-#-|10-#4-#-|10-6#-')

# Colors
BACK, TEXT = (120,)*3, (42,)*3 
COL = {
    '#': TEXT,           # Wall
    '@': (0, 153, 255),  # Player
    '+': (0, 122, 204),  # Player + Goal
    '$': (172, 115, 57), # Box
    '*': (134, 89, 45),  # Box + Goal
    '.': (70, 185, 70),  # Goal
    '-': BACK,           # Floor
    }

def wait(buttons = range(53)): # Wait for keypress 
    while True:
        for i in buttons:
            if keydown(i):
                sleep(0.1)
                return i

def decompress(rne): # Unpack the level
    grid = ['']
    while rne:
        if rne[0] == '|': grid.append('')
        elif 47 < ord(rne[0]) < 58:
            number = ''
            while 47 < ord(rne[0]) < 58: number += rne[0]; rne = rne[1:]
            grid[-1] += int(number)*rne[0]
        else: grid[-1] += rne[0]
        rne = rne[1:]
    return grid

def sett(x, y, test, valid, default):
    value = valid if grid[y][x] in test else default
    grid[y] = grid[y][:x] + value + grid[y][x+1:]
    fill_rect(X+d*x, Y+d*y, d, d, COL[value])

def win(): # Check for a win
    for y in grid:
        for char in y:
            if char=='$': return False
    return True

def menu(): # Rules, commands
    def display(*t):
        for i in list(range(len(t)))[::3]: draw_string(t[i], t[i+1], t[i+2], TEXT, BACK)
    fill_rect(0, 0, 320, 222, BACK)
    draw_string('SOKOBAN', 125, 6, TEXT, BACK)
    draw_string('Press a button to continue.', 25, 190, TEXT, BACK)
    display("You are a pusher employee in", 20, 40,
        "a store room.", 95, 60,
        "Push the boxes to their goal", 20, 90,
        "while minimizing moves.", 45, 110,
        "You can't push two boxes at", 20, 140,
        "once or pull them.", 70, 160)
    wait()
    fill_rect(0, 40, 320, 140, BACK)
    display("   COMMANDS       VISUALS", 25, 35,
        " Move : ARROWS    Wall :", 25, 60,
        " Undo : DEL     Player :", 25, 80,
        "Reset : EXE     + Goal :", 25, 100,
        " Help : ANS        Box :", 25, 120,
        "Level : +/-     + Goal :", 25, 140,
        " Quit : BACK      Goal :", 25, 160)
    for j in range(6): fill_rect(275, 60 + 20*j + 4, 10, 10, [COL['#'], COL['@'], COL['+'], COL['$'], COL['*'], COL['.']][j])
    wait()

menu()
count, i = len(LEVELS), 0
while True:
    # Initialisation
    grid, moves, his = decompress(LEVELS[i]), 0, ''
    
    # Interface drawing
    fill_rect(0, 0, 320, 222, BACK)
    draw_string('SOKOBAN', 125, 6, TEXT, BACK)
    lev = str(i+1) + '/' + str(count)
    draw_string(lev, 314-10*len(lev), 6, TEXT, BACK)
    draw_string('0', 6, 6, TEXT, BACK)
    X, Y, WIDTH, HEIGTH = 10, 33, 300, 180
    d = max(6, min(WIDTH//len(grid[0]), HEIGTH//len(grid), 12))
    X, Y = X + int((WIDTH - len(grid[0])*d)/2), Y + int((HEIGTH - len(grid)*d)/2)
    for y in range(len(grid)):
        for x in range(len(grid[y])):
            fill_rect(X+d*x, Y+d*y, d, d, COL[grid[y][x]])
            if grid[y][x] in '+@': XP, YP = x, y

    # Main loop
    while True:
        key = wait((0, 1, 2, 3, 4, 17, 45, 46, 51))
        if key in (0, 1, 2, 3, 17):
            if key == 17:
                if not(his): continue
                XN, YN = XP + (his[-1] in ('l', 'L')) - (his[-1] in ('r', 'R')), YP + (his[-1] in ('u', 'U')) - (his[-1] in ('d', 'D'))
                XN2, YN2 = 2*XP - XN, 2*YP - YN
            else:
                XN, YN = XP + (key==3) - (key==0), YP + (key==2) - (key==1)
                XN2, YN2 = 2*XN - XP, 2*YN - YP
                if (grid[YN][XN] == '#') or ((grid[YN][XN] in ('$', '*')) and (grid[YN2][XN2] in ('#', '$', '*'))): continue
                move = 'l'*(key==0) + 'u'*(key==1) + 'd'*(key==2) + 'r'*(key==3)
                if grid[YN][XN] in ('$', '*'): move = move.upper()
            
            if key != 17 or his[-1].islower(): sett(XP, YP, '+', '.', '-')
            else: sett(XP, YP, '+', '*', '$'); sett(XN2, YN2, '*', '.', '-')
            XP, YP = XN, YN
            if key != 17 and grid[YP][XP] in ('*', '$'): sett(XN2, YN2, '.', '*', '$')
            sett(XP, YP, '.*', '+', '@')
            
            moves = moves + 1 - 2*(key==17)
            his = his[:-1] if key==17 else (his + move)[-100:]
            draw_string(str(moves)+' ', 6, 6, TEXT, BACK)
            if win(): draw_string('YOU WON !', 115, 6, TEXT, BACK)
            continue
        elif key == 51: menu()
        else: i = (i + (key==45) - (key==46))%count
        break