Multiban (was Re: Sokoban)

Seo Sanghyeon unendliche at hanmail.net
Tue Aug 13 07:53:41 EDT 2002


Thanks for feedbacks.

Terry Reedy (tjreedy at udel.edu) wrote:
> Without a sample map or any knowledge of how to construct one,
> I could not get very far 8-(.

It uses XSokoban text format, de facto standard for Sokoban levels.
http://www.clickfest88.freeserve.co.uk/xsbformat.html

2315 levels in XSokoban text format.
http://mujweb.cz/Zabava/sokoban/downloada.htm

But it fails if it encounter comments. For solution, see below.

Terry Reedy (tjreedy at udel.edu) wrote:
> With two players, it randomly selects one to move with each keystroke.
> That could make for interesting challenges.

Oh, it was not my intention, but see below.

Michael B?ker (aida.kensuke at gmx.no-spam-accepted.net) wrote:
> What could I do to avoid these errors?
> [snip nested scope errors]

fron __future__ import nested_scope, or see below.

Lee Harr (missive at frontiernet.net) wrote:
> Very nice, but...
> No Undo?!

See below...

----

So, here is Multiban,
Sokoban with multiple players,
which read XSokoban format with comments,
which runs on Python 1.5.2
which has undo,
which prints your solution,
etc.

== MANUAL ==
Up, Left, Down, Right -> Move a man
Tab -> Select a man
BackSpace -> Undo

Enjoy!

----

# Sokoban in Python
# Tested on Python 2.2.1 AND Python 1.5.2

# XSokoban format for levels
# Lurd format for bookmarks

import Tkinter  # to do graphics
import re       # to parse xsb format

import string
upper = {'u': 'U', 'l': 'L', 'd': 'D', 'r': 'R'}
lower = {'U': 'u', 'L': 'l', 'D': 'd', 'R': 'r'}

# == start configuration =====
WALL     = 'black'
BOX      = 'blue'
EMPTY    = 'yellow'
FULL     = 'green'
INACTIVE = 'brown'
ACTIVE   = 'red'

size = 20
margin = 5
maxline = 60
# == end configuration =======

class Struct:
    def load(self, keys):
        if type(keys) == type(()):
            res = []
            for key in keys:
                res.append(self.__dict__[key])
            return res
        else:
            res = self.__dict__[keys]
            return res
    def save(self, dic):
        for key in dic.keys():
            self.__dict__[key] = dic[key]

# ============================

def loadlevel(filename):
    xsbline = re.compile('[ #$.*@+]+$')
    lines = open(filename).readlines()
    lines = map(lambda x: x[:-1], lines)
    lines = filter(xsbline.match, lines)
    xs = max(map(len, lines))
    ys = len(lines)
    wall, depot, box, player = {}, {}, {}, {}
    switch = {' ': (),
              '#': (wall,),
              '$': (box,),
              '.': (depot,),
              '*': (depot, box),
              '@': (player,),
              '+': (depot, player)}
    for y in range(len(lines)):
        for x in range(len(lines[y])):
            for dic in switch[lines[y][x]]:
                dic[x, y] = 1
    assert len(depot.keys()) == len(box.keys())
    boxcnt = len(box.keys())
    level = Struct()
    level.save({'xs': xs,
                'ys': ys,
                'wall': wall,
                'depot': depot,
                'box': box,
                'player': player,
                'boxcnt': boxcnt})
    return level

# ============================

def create_canvas(parent, level):
    xs, ys = level.load(('xs', 'ys'))
    width = xs*size+margin*2
    height = ys*size+margin*2
    canvas = Tkinter.Canvas(
        parent, width=width, height=height)
    return canvas

def create_square(canvas, pos, fill):
    x, y = pos
    id = canvas.create_rectangle(
        (x+0)*size+margin, (y+0)*size+margin,
        (x+1)*size+margin, (y+1)*size+margin,
        width=0, fill=fill)
    return id

def create_circle(canvas, pos, fill):
    x, y = pos
    id = canvas.create_oval(
        (x+0.2)*size+margin, (y+0.2)*size+margin,
        (x+0.8)*size+margin, (y+0.8)*size+margin,
        width=0, fill=fill)
    return id

# ============================

def drawlevel(canvas, level):
    depotid, boxid, playerid = {}, {}, {}
    xs, ys, wall, depot, box, player = level.load(
        ('xs', 'ys', 'wall', 'depot', 'box', 'player'))
    for x in range(xs):
        for y in range(ys):
            pos = x, y
            if pos in wall.keys():
                create_square(canvas, pos, WALL)
            if pos in depot.keys():
                depotid[pos] = create_square(canvas, pos, EMPTY)
    # circles should be drawn after squares are drawn
    for x in range(xs):
        for y in range(ys):
            pos = x, y
            if pos in box.keys():
                boxid[pos] = create_circle(canvas, pos, BOX)
            if pos in player.keys():
                playerid[pos] = create_circle(canvas, pos, INACTIVE)
    idtable = Struct()
    idtable.save({'depot': depotid,
                  'box': boxid,
                  'player': playerid})
    return idtable

# ============================

def initlevel(canvas, level, idtable):
    depot, box = level.load(('depot', 'box'))
    depotid = idtable.load('depot')
    boxcnt = level.load('boxcnt')
    for pos in depot.keys():
        if pos in box.keys():
            fill(canvas, level, idtable, 'depot', pos, FULL)
            boxcnt = boxcnt - 1
    level.save({'boxcnt': boxcnt})
    players = level.load('player').keys()
    pidtable = {}
    newpid = 0
    for pos in players:
        pidtable[newpid] = pos
        newpid = newpid + 1
    level.save({'pidtable': pidtable,
                'pid': 0})
    pos = curpos(level)
    fill(canvas, level, idtable, 'player', pos, ACTIVE)
    level.save({'history': []})

# ============================

def move(canvas, level, idtable, code, pos, dd):
    x, y = pos
    dx, dy = dd
    dpos = x+dx, y+dy
    dic = level.load(code)
    idic = idtable.load(code)
    dic[dpos] = dic[pos]
    idic[dpos] = idic[pos]
    canvas.move(idic[pos], dx*size, dy*size)
    del dic[pos]
    del idic[pos]

def fill(canvas, level, idtable, code, pos, fill):
    idic = idtable.load(code)
    canvas.itemconfigure(idic[pos], fill=fill)

def curpos(level):
    pidtable, pid = level.load(('pidtable', 'pid'))
    pos = pidtable[pid]
    return pos

def checkfin(level):
    boxcnt = level.load('boxcnt')
    fin = (boxcnt == 0)
    return fin

def delta(event):
    key = event.keysym
    switch = {'Up': (0, -1),
              'Left': (-1, 0),
              'Down': (0, 1),
              'Right': (1, 0)}
    dd = switch[key]
    return dd

def convert(dd):
    switch_1 = {(0, -1): 'u',
                (-1, 0): 'l',
                (0, 1): 'd',
                (1, 0): 'r'}
    switch_2 = {'u': (0, -1),
                'l': (-1, 0),
                'd': (0, 1),
                'r': (1, 0)}
    if type(dd) == type(()):
        cc = switch_1[dd]
    else:
        cc = switch_2[dd]
    return cc

# ============================

def mover(event, canvas, level, idtable):
    dd = delta(event)
    dir = convert(dd)
    pos = curpos(level)
    x, y = pos
    dx, dy = dd
    dpos = x+dx, y+dy
    ddpos = x+dx+dx, y+dy+dy
    wall, box, depot = level.load(('wall', 'box', 'depot'))
    boxcnt = level.load('boxcnt')
    history = level.load('history')
    pidtable, pid = level.load(('pidtable', 'pid'))
    if dpos not in wall.keys():
        if dpos not in box.keys():
            move(canvas, level, idtable, 'player', pos, dd)
            pidtable[pid] = dpos
            history.append(dir)
        elif (ddpos not in wall.keys() and
              ddpos not in box.keys()):
            move(canvas, level, idtable, 'player', pos, dd)
            pidtable[pid] = dpos
            move(canvas, level, idtable, 'box', dpos, dd)
            if dpos in depot.keys():
                fill(canvas, level, idtable, 'depot', dpos, EMPTY)
                boxcnt = boxcnt + 1
            if ddpos in depot.keys():
                fill(canvas, level, idtable, 'depot', ddpos, FULL)
                boxcnt = boxcnt - 1
            level.save({'boxcnt': boxcnt})
            history.append(upper[dir])

# ============================

def selector(event, canvas, level, idtable):
    pidtable, pid = level.load(('pidtable', 'pid'))
    pidlimit = len(pidtable.keys())
    pos = curpos(level)
    fill(canvas, level, idtable, 'player', pos, INACTIVE)
    pid = pid + 1
    if pid == pidlimit:
        pid = 0
    level.save({'pid': pid})
    pos = curpos(level)
    fill(canvas, level, idtable, 'player', pos, ACTIVE)
    history = level.load('history')
    history.append('+')

# ============================

def rewinder(event, canvas, level, idtable):
    history = level.load('history')
    if not history:
        return
    lastmove = history.pop()
    pidtable, pid = level.load(('pidtable', 'pid'))
    if lastmove == '+':
        pos = curpos(level)
        fill(canvas, level, idtable, 'player', pos, INACTIVE)
        pid = pid - 1
        if pid == -1:
            pid = 0
        level.save({'pid': pid})
        pos = curpos(level)
        fill(canvas, level, idtable, 'player', pos, ACTIVE)
        return
    dir = lastmove
    push = 0
    if dir in upper.values():
        push = 1
        dir = lower[dir]
    dd = convert(dir)
    pos = curpos(level)
    x, y = pos
    dx, dy = dd
    idd = -dx, -dy
    dpos = x+dx, y+dy
    ipos = x-dx, y-dy
    depot = level.load('depot')
    boxcnt = level.load('boxcnt')
    if not push:
        move(canvas, level, idtable, 'player', pos, idd)
        pidtable[pid] = ipos
    else:
        move(canvas, level, idtable, 'player', pos, idd)
        pidtable[pid] = ipos
        move(canvas, level, idtable, 'box', dpos, idd)
        if dpos in depot.keys():
            fill(canvas, level, idtable, 'depot', dpos, EMPTY)
            boxcnt = boxcnt + 1
        if pos in depot.keys():
            fill(canvas, level, idtable, 'depot', pos, FULL)
            boxcnt = boxcnt - 1
    level.save({'boxcnt': boxcnt})

# ============================

def splitline(string, maxline):
    res = ''
    cnt = 0
    while 1:
        line = string[cnt*maxline:(cnt+1)*maxline]
        if not line: break
        res = res + line + '\n'
        cnt = cnt + 1
    return res

# ============================

def finish(parent, level):
    solution = string.join(level.load('history'), '')
    solution = splitline(solution, maxline)
    print solution
    button = Tkinter.Button(
        parent, text='Close', command=parent.destroy)
    button.pack()

# ============================

def makegame(parent, filename):
    level = loadlevel(filename)
    canvas = create_canvas(parent, level)
    idtable = drawlevel(canvas, level)
    initlevel(canvas, level, idtable)
    def move_callback(event, parent=parent,
                      canvas=canvas, level=level, idtable=idtable):
        mover(event, canvas, level, idtable)
        if checkfin(level):
            finish(parent, level)
    def rewind_callback(event, parent=parent,
                        canvas=canvas, level=level, idtable=idtable):
        rewinder(event, canvas, level, idtable)
        if checkfin(level):
            finish(parent, level)
    def select_callback(event, parent=parent,
                        canvas=canvas, level=level, idtable=idtable):
        selector(event, canvas, level, idtable)
        if checkfin(level):
            finish(parent, level)
    parent.bind('<Up>', move_callback)
    parent.bind('<Left>', move_callback)
    parent.bind('<Down>', move_callback)
    parent.bind('<Right>', move_callback)
    parent.bind('<BackSpace>', rewind_callback)
    parent.bind('<Tab>', select_callback)
    canvas.pack(side=Tkinter.TOP,
                padx=margin, pady=margin)
    if checkfin(level):
        finish(parent, level)

# ============================

def main():
    filename = raw_input('> ')
    root = Tkinter.Tk()
    makegame(root, filename)
    root.mainloop()

if __name__ == '__main__':
    main()



More information about the Python-list mailing list