Sokoban: Difference between revisions

68,400 bytes added ,  3 months ago
m
m (→‎{{header|Wren}}: Minor tidy)
 
(43 intermediate revisions by 17 users not shown)
Line 10:
* + is the player on a goal
* * is a box on a goal
<syntaxhighlight lang="text">#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######</syntaxhighlight>
 
Sokoban solutions are usually stored in the LURD format, where lowercase l, u, r and d represent a move in that ('''l'''eft, '''u'''p, '''r'''ight, '''d'''own) direction and capital LURD represents a push.
Line 16 ⟶ 24:
 
For more information, see [http://www.sokobano.de/wiki/index.php?title=Main_Page the Sokoban wiki].
 
 
=={{header|11l}}==
{{trans|Python}}
 
<syntaxhighlight lang="11l">[String] data
V nrows = 0
V px = 0
V py = 0
V sdata = ‘’
V ddata = ‘’
 
F init(board)
:data = board.split("\n")
:nrows = max(:data.map(r -> r.len))
 
V maps = [‘ ’ = ‘ ’, ‘.’ = ‘.’, ‘@’ = ‘ ’, ‘#’ = ‘#’, ‘$’ = ‘ ’]
V mapd = [‘ ’ = ‘ ’, ‘.’ = ‘ ’, ‘@’ = ‘@’, ‘#’ = ‘ ’, ‘$’ = ‘*’]
 
L(row) :data
V r = L.index
L(ch) row
V c = L.index
:sdata ‘’= maps[ch]
:ddata ‘’= mapd[ch]
I ch == ‘@’
:px = c
:py = r
 
F push(x, y, dx, dy, &data)
I :sdata[(y + 2 * dy) * :nrows + x + 2 * dx] == ‘#’
| data[(y + 2 * dy) * :nrows + x + 2 * dx] != ‘ ’
data = ‘’
R
 
data[y * :nrows + x] = ‘ ’
data[(y + dy) * :nrows + x + dx] = ‘@’
data[(y + 2 * dy) * :nrows + x + 2 * dx] = ‘*’
 
F is_solved(data)
L(i) 0 .< data.len
I (:sdata[i] == ‘.’) != (data[i] == ‘*’)
R 0B
R 1B
 
F solve()
V open = Deque([(:ddata, ‘’, :px, :py)])
V visited = Set([:ddata])
V dirs = ((0, -1, ‘u’, ‘U’), ( 1, 0, ‘r’, ‘R’),
(0, 1, ‘d’, ‘D’), (-1, 0, ‘l’, ‘L’))
 
L !open.empty
V (cur, csol, x, y) = open.pop_left()
 
L(di) dirs
V temp = copy(cur)
V (dx, dy) = (di[0], di[1])
 
I temp[(y + dy) * :nrows + x + dx] == ‘*’
push(x, y, dx, dy, &temp)
I temp != ‘’ & temp !C visited
I is_solved(temp)
R csol‘’di[3]
open.append((temp, csol‘’di[3], x + dx, y + dy))
visited.add(temp)
E
I :sdata[(y + dy) * :nrows + x + dx] == ‘#’ | temp[(y + dy) * :nrows + x + dx] != ‘ ’
L.continue
 
temp[y * :nrows + x] = ‘ ’
temp[(y + dy) * :nrows + x + dx] = ‘@’
 
I temp !C visited
I is_solved(temp)
R csol‘’di[2]
open.append((temp, csol‘’di[2], x + dx, y + dy))
visited.add(temp)
 
R ‘No solution’
 
V level =
|‘#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######’
 
init(level)
print(level"\n\n"solve())</syntaxhighlight>
 
{{out}}
<pre>
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
ulULLulDDurrrddlULrruLLrrUruLLLulD
</pre>
 
=={{header|Befunge}}==
El código no es mío, sólo lo reproduzco.
<syntaxhighlight lang="befunge">
589*+0g"0"-25**689*+0g"0"-+50p v # Sokoban - (c) Matthew Westcott 2000 # 03
83*>10p99*2->:00p10gg"x"-#v_v p01g<> ## # # # # # # # # # # # # # # # # # # #
<< ^ -1g01_^#!: -1g00< > v # # # # # # # # # # # # # # # # # # # # #
|-"8"_v#-"2":_v#-"6":_v#-"4":~< <0 #
> 1 0>$1+2 0>$2+3%\0>$1+3%40v $4<#
v -"0"gp04:-1+g<v:-1+g00p03:p < $p #
>#v_"X">30g40gv0>40g10g+1-g:"#"-!|0 #
0>"x" ^v1g00p<^1g04p03:_v#`\ "9"<0 #
v:g-2++<>0gg"X"-#v_"0">00 g10gp30g^ #
4# ^g04g04g0<>" " ^v3< #
>8*-#v_$"o" v ^1-1+g0< >00g30g30v #
# >"0"-#v_v> ^v01-2++g< #
^>#$" " 0#<v>"@"50g1-50p^>g40g40gv #
v_^#!-"@"g-1+g04g01:-1+g03g00p-2++< #
>"0"50g1+50p>\10g40g+1-p30g02050g |#
>,#-:#3_@#"a\rx#glg#lw$Zhoo#Grqh$"0<#
#####################################
########
# #
# o@0o #
# #
# o#
### ###
#0 x 0#
########
</syntaxhighlight>
 
=={{header|C}}==
Long, long, long C99 code (plus GNU C nested functions). Doesn't output the movement keys, instead it animates the sequence for you. Solution is move optimized. For an even longer solution, see [[Sokoban/C]].
<langsyntaxhighlight lang="c">#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Line 420 ⟶ 564:
 
return 0;
}</langsyntaxhighlight>
 
=={{header|C sharp|C#}}==
<syntaxhighlight lang="csharp">using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace SokobanSolver
{
public class SokobanSolver
{
private class Board
{
public string Cur { get; internal set; }
public string Sol { get; internal set; }
public int X { get; internal set; }
public int Y { get; internal set; }
 
public Board(string cur, string sol, int x, int y)
{
Cur = cur;
Sol = sol;
X = x;
Y = y;
}
}
 
private string destBoard, currBoard;
private int playerX, playerY, nCols;
 
SokobanSolver(string[] board)
{
nCols = board[0].Length;
StringBuilder destBuf = new StringBuilder();
StringBuilder currBuf = new StringBuilder();
 
for (int r = 0; r < board.Length; r++)
{
for (int c = 0; c < nCols; c++)
{
 
char ch = board[r][c];
 
destBuf.Append(ch != '$' && ch != '@' ? ch : ' ');
currBuf.Append(ch != '.' ? ch : ' ');
 
if (ch == '@')
{
this.playerX = c;
this.playerY = r;
}
}
}
destBoard = destBuf.ToString();
currBoard = currBuf.ToString();
}
 
private string Move(int x, int y, int dx, int dy, string trialBoard)
{
 
int newPlayerPos = (y + dy) * nCols + x + dx;
 
if (trialBoard[newPlayerPos] != ' ')
return null;
 
char[] trial = trialBoard.ToCharArray();
trial[y * nCols + x] = ' ';
trial[newPlayerPos] = '@';
 
return new string(trial);
}
 
private string Push(int x, int y, int dx, int dy, string trialBoard)
{
 
int newBoxPos = (y + 2 * dy) * nCols + x + 2 * dx;
 
if (trialBoard[newBoxPos] != ' ')
return null;
 
char[] trial = trialBoard.ToCharArray();
trial[y * nCols + x] = ' ';
trial[(y + dy) * nCols + x + dx] = '@';
trial[newBoxPos] = '$';
 
return new string(trial);
}
 
private bool IsSolved(string trialBoard)
{
for (int i = 0; i < trialBoard.Length; i++)
if ((destBoard[i] == '.')
!= (trialBoard[i] == '$'))
return false;
return true;
}
 
private string Solve()
{
char[,] dirLabels = { { 'u', 'U' }, { 'r', 'R' }, { 'd', 'D' }, { 'l', 'L' } };
int[,] dirs = { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 } };
ISet<string> history = new HashSet<string>();
LinkedList<Board> open = new LinkedList<Board>();
 
history.Add(currBoard);
open.AddLast(new Board(currBoard, string.Empty, playerX, playerY));
 
while (!open.Count.Equals(0))
{
Board item = open.First();
open.RemoveFirst();
string cur = item.Cur;
string sol = item.Sol;
int x = item.X;
int y = item.Y;
 
for (int i = 0; i < dirs.GetLength(0); i++)
{
string trial = cur;
int dx = dirs[i, 0];
int dy = dirs[i, 1];
 
// are we standing next to a box ?
if (trial[(y + dy) * nCols + x + dx] == '$')
{
// can we push it ?
if ((trial = Push(x, y, dx, dy, trial)) != null)
{
// or did we already try this one ?
if (!history.Contains(trial))
{
 
string newSol = sol + dirLabels[i, 1];
 
if (IsSolved(trial))
return newSol;
 
open.AddLast(new Board(trial, newSol, x + dx, y + dy));
history.Add(trial);
}
}
// otherwise try changing position
}
else if ((trial = Move(x, y, dx, dy, trial)) != null)
{
if (!history.Contains(trial))
{
string newSol = sol + dirLabels[i, 0];
open.AddLast(new Board(trial, newSol, x + dx, y + dy));
history.Add(trial);
}
}
}
}
return "No solution";
}
 
public static void Main(string[] a)
{
string level = "#######," +
"# #," +
"# #," +
"#. # #," +
"#. $$ #," +
"#.$$ #," +
"#.# @#," +
"#######";
System.Console.WriteLine("Level:\n");
foreach (string line in level.Split(','))
{
System.Console.WriteLine(line);
}
System.Console.WriteLine("\nSolution:\n");
System.Console.WriteLine(new SokobanSolver(level.Split(',')).Solve());
}
}
}</syntaxhighlight>
Output:
<pre>
Level:
 
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
Solution:
 
ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
 
=={{header|C++}}==
Line 429 ⟶ 765:
 
This performs a breadth-first search by moves, so the results should be a move-optimal solution.
<langsyntaxhighlight lang="cpp">#include <iostream>
#include <string>
#include <vector>
Line 452 ⟶ 788:
int w = 0;
vector<string> data;
for(; iter != end; ++iter){
{
data.push_back((*iter)[1]);
w = max(w, (*iter)[1].length());
}
 
for(int v = 0; v < data.size(); ++v){
{
vector<char> sTemp, dTemp;
for(int u = 0; u < w; ++u){
if(u > data[v].size()){
if(u > data[v].size())
{
sTemp.push_back(' ');
dTemp.push_back(' ');
}else{
else
{
char s = ' ', d = ' ', c = data[v][u];
 
Line 477 ⟶ 807:
s = '.';
 
if(c == '@' || c == '+'){
{
d = '@';
px = u;
py = v;
}else if(c == '$' || c == '*')
else if(c == '$' || c == '*')
d = '*';
 
Line 542 ⟶ 870:
dirs[3] = make_tuple(-1, 0, 'l', 'L');
 
while(open.size() > 0){
{
vector<vector<char>> temp, cur = get<0>(open.front());
string cSol = get<1>(open.front());
Line 550 ⟶ 877:
open.pop();
 
for(int i = 0; i < 4; ++i){
{
temp = cur;
int dx = get<0>(dirs[i]);
int dy = get<1>(dirs[i]);
 
if(temp[y+dy][x+dx] == '*'){
if(push(x, y, dx, dy, temp) && (visited.find(temp) == visited.end())){
{
if(push(x, y, dx, dy, temp) && (visited.find(temp) == visited.end()))
{
if(isSolved(temp))
return cSol + get<3>(dirs[i]);
Line 565 ⟶ 889:
visited.insert(temp);
}
}else if(move(x, y, dx, dy, temp) && (visited.find(temp) == visited.end())){
}
else if(move(x, y, dx, dy, temp) && (visited.find(temp) == visited.end()))
{
if(isSolved(temp))
return cSol + get<2>(dirs[i]);
Line 596 ⟶ 918:
cout << level << endl << endl << b.solve() << endl;
return 0;
}</langsyntaxhighlight>
 
Output:
Line 610 ⟶ 932:
 
ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
 
===Unordered Set-based Version===
{{works with|C++11}}
Line 615 ⟶ 938:
{{works with|GCC 4.6}}
Alternative version, about twice faster (about 2.1 seconds runtime), same output.
<langsyntaxhighlight lang="cpp">#include <iostream>
#include <string>
#include <vector>
Line 754 ⟶ 1,077:
cout << board.solve() << endl;
return 0;
}</langsyntaxhighlight>
 
=={{header|D}}==
Line 760 ⟶ 1,083:
{{trans|C++}}
This version uses the queue defined in the Queue/Usage task.
<langsyntaxhighlight lang="d">import std.string, std.typecons, std.exception, std.algorithm;
import queue_usage2; // No queue in Phobos 2.064.
 
Line 771 ⟶ 1,094:
private immutable int playerx, playery;
 
this(in string[] board) immutable pure nothrow const@safe
in {
foreach (const row; board) {
Line 780 ⟶ 1,103:
}
} body {
/*static*/ immutable sMap = [' ':' ', '.':'.', '@':' ', '#':'#', '$':' '];
immutable dMap = [' ':' ', '.':' .', '@':'@ ', '#':' #', '$':'* '];
/*static*/ immutable dMap =
[' ':' ', '.':' ', '@':'@', '#':' ', '$':'*'];
ncols = board[0].length;
 
Line 805 ⟶ 1,130:
private bool move(in int x, in int y, in int dx,
in int dy, ref CTable data)
const pure nothrow /*nothrow@safe*/ {
if (sData[(y + dy) * ncols + x + dx] == El.wall ||
data[(y + dy) * ncols + x + dx] != El.floor)
return false;
 
auto data2 = data.dup; // Not nothrow.
data2[y * ncols + x] = El.floor;
data2[(y + dy) * ncols + x + dx] = El.player;
Line 819 ⟶ 1,144:
private bool push(in int x, in int y, in int dx,
in int dy, ref CTable data)
const pure nothrow /*nothrow@safe*/ {
if (sData[(y + 2 * dy) * ncols + x + 2 * dx] == El.wall ||
data[(y + 2 * dy) * ncols + x + 2 * dx] != El.floor)
return false;
 
auto data2 = data.dup; // Not nothrow.
data2[y * ncols + x] = El.floor;
data2[(y + dy) * ncols + x + dx] = El.player;
data2[(y + 2 * dy) * ncols + x + 2*dx] = El.boxOnGoal;
data = data2.assumeUnique; // Not enforced.
return true;
}
 
private bool isSolved(in CTable data) const pure nothrow {
const pure nothrow @safe @nogc {
foreach (immutable i, immutable d; data)
if ((sData[i] == El.goal) != (d == El.boxOnGoal))
Line 839 ⟶ 1,165:
}
 
string solve() pure nothrow /*@safe*/ {
bool[immutable CTable] visitedSet = [dData: true];
 
Line 846 ⟶ 1,172:
open.push(Four(dData, "", playerx, playery));
 
static immutable dirs = [tuple( 0, -1, 'u', 'U'),
tuple( 1, 0, 'r', 'R'),
tuple( 0, 1, 'd', 'D'),
tuple(-1, 0, 'l', 'L')];
 
while (!open.lengthempty) {
//immutable (cur, cSol, x, y) = open.pop;
immutable item = open.pop;
Line 869 ⟶ 1,195:
if (isSolved(temp))
return cSol ~ di[3];
open.push(Four(temp, cSol ~ di[3], x + dx, y + dy));
visitedSet[temp] = true;
}
} else if (move(x, y, dx, dy, temp) && temp !in visitedSet) {
temp !in visitedSet) {
if (isSolved(temp))
return cSol ~ di[2];
open.push(Four(temp, cSol ~ di[2], x + dx, y + dy));
visitedSet[temp] = true;
}
Line 888 ⟶ 1,213:
void main() {
import std.stdio, core.memory;
GC.disable; // Uses about twice the memory.
 
immutable level =
Line 900 ⟶ 1,225:
#######";
 
constimmutable b = constimmutable(Board)(level.splitLines);
writeln(level, "\n\n", b.solve);
}</langsyntaxhighlight>
{{out}}
<pre>#######
Line 914 ⟶ 1,239:
 
ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
Run-time about 0.5855 seconds with DMD compiler, 0.49 with LDC2 compiler.
 
===Faster Version===
{{trans|C}}
This code is not idiomatic D, it retains most of the style of the C version.
<langsyntaxhighlight lang="d">import core.stdc.stdio: printf, puts, fflush, stdout, putchar;
import core.stdc.stdlib: malloc, calloc, realloc, free, alloca, exit;
 
Line 934 ⟶ 1,259:
CellIndex[0] c_;
 
CellIndex get(in size_t i) inout pure nothrow @nogc {
return c_.ptr[i];
}
 
void set(in size_t i, in CellIndex v) pure nothrow @nogc {
c_.ptr[i] = v;
}
 
CellIndex[] slice(in size_t i, in size_t j) pure nothrow @nogc return {
return c_.ptr[i .. j];
}
Line 956 ⟶ 1,281:
 
 
State* newState(State* parent) nothrow @nogc {
static State* nextOf(State *s) nothrow @nogc {
return cast(State*)(cast(ubyte*)s + stateSize);
}
Line 985 ⟶ 1,310:
 
 
void unNewState(State* p) nothrow @nogc {
p.next = blockHead;
blockHead = p;
Line 992 ⟶ 1,317:
 
/// Mark up positions where a box definitely should not be.
void markLive(in size_t c) nothrow @nogc {
immutable y = c / w;
immutable x = c % w;
Line 1,014 ⟶ 1,339:
 
 
State* parseBoard(in size_t y, in size_t x, in char* s) nothrow @nogc {
static T[] myCalloc(T)(in size_t n) nothrow @nogc {
auto ptr = cast(T*)calloc(n, T.sizeof);
if (ptr == null)
Line 1,071 ⟶ 1,396:
 
/// K&R hash function.
void hash(State* s, in size_t nBoxes) pure nothrow @nogc {
if (!s.h) {
Thash ha = 0;
Line 1,081 ⟶ 1,406:
 
 
void extendTable() nothrow @nogc {
int oldSize = hashSize;
 
Line 1,115 ⟶ 1,440:
 
 
State* lookup(State *s) nothrow @nogc {
s.hash(nBoxes);
auto f = buckets[s.h & (hashSize - 1)];
Line 1,127 ⟶ 1,452:
 
 
bool addToTable(State* s) nothrow @nogc {
if (s.lookup) {
s.unNewState;
Line 1,144 ⟶ 1,469:
 
 
bool success(in State* s) nothrow @nogc {
foreach (immutable i; 1 .. nBoxes + 1)
if (!goals[s.get(i)])
Line 1,152 ⟶ 1,477:
 
 
State* moveMe(State* s, in int dy, in int dx) nothrow @nogc {
immutable int y = s.get(0) / w;
immutable int x = s.get(0) % w;
Line 1,205 ⟶ 1,530:
 
 
bool queueMove(State *s) nothrow @nogc {
if (!s || !s.addToTable)
return false;
Line 1,221 ⟶ 1,546:
 
 
bool doMove(State* s) nothrow @nogc {
return s.moveMe( 1, 0).queueMove ||
s.moveMe(-1, 0).queueMove ||
Line 1,229 ⟶ 1,554:
 
 
void showBoard(in State* s) nothrow @nogc {
static immutable glyphs1 = " #@$", glyphs2 = ".#@$";
 
Line 1,250 ⟶ 1,575:
 
 
void showMoves(in State* s) nothrow @nogc {
if (s.prev)
s.prev.showMoves;
Line 1,257 ⟶ 1,582:
}
 
int main() nothrow @nogc {
// Workaround for @nogc.
alias ctEval(alias expr) = expr;
 
int main() nothrow {
enum uint problem = 0;
 
static if (problem == 0) {
auto s = parseBoard(8, 7, ctEval!(
"#######"~
"# #"~
Line 1,270 ⟶ 1,597:
"#.$$ #"~
"#.# @#"~
"#######"));
 
} else static if (problem == 1) {
auto s = parseBoard(5, 13, ctEval!(
"#############"~
"# # #"~
"# $$$$$$$ @#"~
"#....... #"
"#############"));
 
} else static if (problem == 2) {
auto s = parseBoard(11, 19, ctEval!(
" ##### "~
" # # "~
Line 1,292 ⟶ 1,619:
"##### ### #@## .#"~
" # #########"~
" ####### "));
} else {
asset(0, "Not present problem.");
Line 1,330 ⟶ 1,657:
 
return 0;
}</langsyntaxhighlight>
 
=={{header|Elixir}}==
{{works with|Elixir|1.3}}
{{trans|Ruby}}
<syntaxhighlight lang="elixir">defmodule Sokoban do
defp setup(level) do
{leng, board} = normalize(level)
{player, goal} = check_position(board)
board = replace(board, [{".", " "}, {"+", " "}, {"*", "$"}])
lurd = [{-1, "l", "L"}, {-leng, "u", "U"}, {1, "r", "R"}, {leng, "d", "D"}]
dirs = [-1, -leng, 1, leng]
dead_zone = set_dead_zone(board, goal, dirs)
{board, player, goal, lurd, dead_zone}
end
defp normalize(level) do
board = String.split(level, "\n", trim: true)
|> Enum.map(&String.trim_trailing &1)
leng = Enum.map(board, &String.length &1) |> Enum.max
board = Enum.map(board, &String.pad_trailing(&1, leng)) |> Enum.join
{leng, board}
end
defp check_position(board) do
board = String.codepoints(board)
player = Enum.find_index(board, fn c -> c in ["@", "+"] end)
goal = Enum.with_index(board)
|> Enum.filter_map(fn {c,_} -> c in [".", "+", "*"] end, fn {_,i} -> i end)
{player, goal}
end
defp set_dead_zone(board, goal, dirs) do
wall = String.replace(board, ~r/[^#]/, " ")
|> String.codepoints
|> Enum.with_index
|> Enum.into(Map.new, fn {c,i} -> {i,c} end)
corner = search_corner(wall, goal, dirs)
set_dead_zone(wall, dirs, goal, corner, corner)
end
defp set_dead_zone(wall, dirs, goal, corner, dead) do
dead2 = Enum.reduce(corner, dead, fn pos,acc ->
Enum.reduce(dirs, acc, fn dir,acc2 ->
if wall[pos+dir] == "#", do: acc2,
else: acc2 ++ check_side(wall, dirs, pos+dir, dir, goal, dead, [])
end)
end)
if dead == dead2, do: :lists.usort(dead),
else: set_dead_zone(wall, dirs, goal, corner, dead2)
end
defp replace(string, replacement) do
Enum.reduce(replacement, string, fn {a,b},str ->
String.replace(str, a, b)
end)
end
defp search_corner(wall, goal, dirs) do
Enum.reduce(wall, [], fn {i,c},corner ->
if c == "#" or i in goal do
corner
else
case count_wall(wall, i, dirs) do
2 -> if wall[i-1] != wall[i+1], do: [i | corner], else: corner
3 -> [i | corner]
_ -> corner
end
end
end)
end
defp check_side(wall, dirs, pos, dir, goal, dead, acc) do
if wall[pos] == "#" or
count_wall(wall, pos, dirs) == 0 or
pos in goal do
[]
else
if pos in dead, do: acc, else: check_side(wall, dirs, pos+dir, dir, goal, dead, [pos|acc])
end
end
defp count_wall(wall, pos, dirs) do
Enum.count(dirs, fn dir -> wall[pos + dir] == "#" end)
end
defp push_box(board, pos, dir, route, goal, dead_zone) do
pos2dir = pos + 2 * dir
if String.at(board, pos2dir) == " " and not pos2dir in dead_zone do
board2 = board |> replace_at(pos, " ")
|> replace_at(pos+dir, "@")
|> replace_at(pos2dir, "$")
unless visited?(board2) do
if solved?(board2, goal) do
IO.puts route
exit(:normal)
else
queue_in({board2, pos+dir, route})
end
end
end
end
defp move_player(board, pos, dir) do
board |> replace_at(pos, " ") |> replace_at(pos+dir, "@")
end
defp replace_at(str, pos, c) do
{left, right} = String.split_at(str, pos)
{_, right} = String.split_at(right, 1)
left <> c <> right
# String.slice(str, 0, pos) <> c <> String.slice(str, pos+1..-1)
end
defp solved?(board, goal) do
Enum.all?(goal, fn g -> String.at(board, g) == "$" end)
end
@pattern :sokoban_pattern_set
@queue :sokoban_queue
defp start_link do
Agent.start_link(fn -> MapSet.new end, name: @pattern)
Agent.start_link(fn -> :queue.new end, name: @queue)
end
defp visited?(board) do
Agent.get_and_update(@pattern, fn set ->
{board in set, MapSet.put(set, board)}
end)
end
defp queue_in(data) do
Agent.update(@queue, fn queue -> :queue.in(data, queue) end)
end
defp queue_out do
Agent.get_and_update(@queue, fn q ->
case :queue.out(q) do
{{:value, data}, queue} -> {data, queue}
x -> x
end
end)
end
def solve(level) do
{board, player, goal, lurd, dead_zone} = setup(level)
start_link
visited?(board)
queue_in({board, player, ""})
solve(goal, lurd, dead_zone)
end
defp solve(goal, lurd, dead_zone) do
case queue_out do
{board, pos, route} ->
Enum.each(lurd, fn {dir,move,push} ->
case String.at(board, pos+dir) do
"$" -> push_box(board, pos, dir, route<>push, goal, dead_zone)
" " -> board2 = move_player(board, pos, dir)
unless visited?(board2) do
queue_in({board2, pos+dir, route<>move})
end
_ -> :not_move # wall
end
end)
_ ->
IO.puts "No solution"
exit(:normal)
end
solve(goal, lurd, dead_zone)
end
end
 
level = """
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
"""
IO.puts level
Sokoban.solve(level)</syntaxhighlight>
 
{{out}}
<pre>
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
luULLulDDurrrddlULrruLLrrUruLLLulD
</pre>
 
=={{header|Go}}==
{{trans|C++}}
Well, it started as a C++ translation, but turned out different. It's still the breadth-first set-based algorithm, but I dropped the sdata/ddata optimization and just maintained a single string as the board representation. Also dropped the code to handle non-rectangular boards, and probably some other stuff too.
<langsyntaxhighlight lang="go">package main
 
import (
Line 1,451 ⟶ 1,977:
}
return string(buffer)
}</langsyntaxhighlight>
{{out}}
<pre>
Line 1,466 ⟶ 1,992:
ulULLulDDurrrddlULrruLLrrUruLLLulD
</pre>
 
=={{header|Haskell}}==
<syntaxhighlight lang="haskell">import Control.Monad (liftM)
import Data.Array
import Data.List (transpose)
import Data.Maybe (mapMaybe)
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import Prelude hiding (Left, Right)
 
data Field = Space | Wall | Goal
deriving (Eq)
 
data Action = Up | Down | Left | Right | PushUp | PushDown | PushLeft | PushRight
 
instance Show Action where
show Up = "u"
show Down = "d"
show Left = "l"
show Right = "r"
show PushUp = "U"
show PushDown = "D"
show PushLeft = "L"
show PushRight = "R"
 
type Index = (Int, Int)
type FieldArray = Array Index Field
type BoxArray = Array Index Bool
type PlayerPos = Index
type GameState = (BoxArray, PlayerPos)
type Game = (FieldArray, GameState)
 
toField :: Char -> Field
toField '#' = Wall
toField ' ' = Space
toField '@' = Space
toField '$' = Space
toField '.' = Goal
toField '+' = Goal
toField '*' = Goal
 
toPush :: Action -> Action
toPush Up = PushUp
toPush Down = PushDown
toPush Left = PushLeft
toPush Right = PushRight
toPush n = n
 
toMove :: Action -> Index
toMove PushUp = ( 0, -1)
toMove PushDown = ( 0, 1)
toMove PushLeft = (-1, 0)
toMove PushRight = ( 1, 0)
toMove n = toMove $ toPush n
 
-- Parse the string-based game board into an easier-to-use format.
-- Assume that the board is valid (rectangular, one player, etc).
parseGame :: [String] -> Game
parseGame fieldStrs = (field, (boxes, player))
where
width = length $ head fieldStrs
height = length fieldStrs
bound = ((0, 0), (width - 1, height - 1))
flatField = concat $ transpose fieldStrs
charField = listArray bound flatField
field = fmap toField charField
boxes = fmap (`elem` "$*") charField
player = fst $ head $ filter (flip elem "@+" . snd) $ assocs charField
 
add :: (Num a, Num b) => (a, b) -> (a, b) -> (a, b)
add (a, b) (x, y) = (a + x, b + y)
 
-- Attempt to perform an action, returning the updated game and adjusted
-- action if the action was legal.
tryAction :: Game -> Action -> Maybe (Game, Action)
tryAction (field, (boxes, player)) action
| field ! vec == Wall = Nothing
| boxes ! vec =
if boxes ! vecB || field ! vecB == Wall
then Nothing
else Just ((field, (boxes // [(vec, False), (vecB, True)], vec)),
toPush action)
| otherwise = Just ((field, (boxes, vec)), action)
where
actionVec = toMove action
vec = player `add` actionVec
vecB = vec `add` actionVec
 
-- Search the game for a solution.
solveGame :: Game -> Maybe [Action]
solveGame (field, initState) =
liftM reverse $ bfs (Seq.singleton (initState, [])) (Set.singleton initState)
where
goals = map fst $ filter ((== Goal) . snd) $ assocs field
isSolved st = all (st !) goals
possibleActions = [Up, Down, Left, Right]
-- Breadth First Search of the game tree.
bfs :: Seq.Seq (GameState, [Action]) -> Set.Set GameState -> Maybe [Action]
bfs queue visited =
case Seq.viewl queue of
Seq.EmptyL -> Nothing
(game@(boxes, _), actions) Seq.:< queueB ->
if isSolved boxes
then Just actions
else
let newMoves = filter (flip Set.notMember visited . fst) $
map (\((_, g), a) -> (g, a)) $
mapMaybe (tryAction (field, game)) possibleActions
visitedB = foldl (flip Set.insert) visited $
map fst newMoves
queueC = foldl (Seq.|>) queueB $
map (\(g, a) -> (g, a:actions)) newMoves
in bfs queueC visitedB
 
exampleA :: [String]
exampleA =
["#######"
,"# #"
,"# #"
,"#. # #"
,"#. $$ #"
,"#.$$ #"
,"#.# @#"
,"#######"]
 
main :: IO ()
main =
case solveGame $ parseGame exampleA of
Nothing -> putStrLn "Unsolvable"
Just solution -> do
mapM_ putStrLn exampleA
putStrLn ""
putStrLn $ concatMap show solution</syntaxhighlight>
{{out}}
<pre>#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
 
=={{header|Java}}==
Translation of [[Sokoban#C++|C++]] via [[Sokoban#D|D]]
{{works with|Java|7}}
<syntaxhighlight lang="java">import java.util.*;
 
public class Sokoban {
String destBoard, currBoard;
int playerX, playerY, nCols;
 
Sokoban(String[] board) {
nCols = board[0].length();
StringBuilder destBuf = new StringBuilder();
StringBuilder currBuf = new StringBuilder();
 
for (int r = 0; r < board.length; r++) {
for (int c = 0; c < nCols; c++) {
 
char ch = board[r].charAt(c);
 
destBuf.append(ch != '$' && ch != '@' ? ch : ' ');
currBuf.append(ch != '.' ? ch : ' ');
 
if (ch == '@') {
this.playerX = c;
this.playerY = r;
}
}
}
destBoard = destBuf.toString();
currBoard = currBuf.toString();
}
 
String move(int x, int y, int dx, int dy, String trialBoard) {
 
int newPlayerPos = (y + dy) * nCols + x + dx;
 
if (trialBoard.charAt(newPlayerPos) != ' ')
return null;
 
char[] trial = trialBoard.toCharArray();
trial[y * nCols + x] = ' ';
trial[newPlayerPos] = '@';
 
return new String(trial);
}
 
String push(int x, int y, int dx, int dy, String trialBoard) {
 
int newBoxPos = (y + 2 * dy) * nCols + x + 2 * dx;
 
if (trialBoard.charAt(newBoxPos) != ' ')
return null;
 
char[] trial = trialBoard.toCharArray();
trial[y * nCols + x] = ' ';
trial[(y + dy) * nCols + x + dx] = '@';
trial[newBoxPos] = '$';
 
return new String(trial);
}
 
boolean isSolved(String trialBoard) {
for (int i = 0; i < trialBoard.length(); i++)
if ((destBoard.charAt(i) == '.')
!= (trialBoard.charAt(i) == '$'))
return false;
return true;
}
 
String solve() {
class Board {
String cur, sol;
int x, y;
 
Board(String s1, String s2, int px, int py) {
cur = s1;
sol = s2;
x = px;
y = py;
}
}
char[][] dirLabels = {{'u', 'U'}, {'r', 'R'}, {'d', 'D'}, {'l', 'L'}};
int[][] dirs = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
 
Set<String> history = new HashSet<>();
LinkedList<Board> open = new LinkedList<>();
 
history.add(currBoard);
open.add(new Board(currBoard, "", playerX, playerY));
 
while (!open.isEmpty()) {
Board item = open.poll();
String cur = item.cur;
String sol = item.sol;
int x = item.x;
int y = item.y;
 
for (int i = 0; i < dirs.length; i++) {
String trial = cur;
int dx = dirs[i][0];
int dy = dirs[i][1];
 
// are we standing next to a box ?
if (trial.charAt((y + dy) * nCols + x + dx) == '$') {
 
// can we push it ?
if ((trial = push(x, y, dx, dy, trial)) != null) {
 
// or did we already try this one ?
if (!history.contains(trial)) {
 
String newSol = sol + dirLabels[i][1];
 
if (isSolved(trial))
return newSol;
 
open.add(new Board(trial, newSol, x + dx, y + dy));
history.add(trial);
}
}
 
// otherwise try changing position
} else if ((trial = move(x, y, dx, dy, trial)) != null) {
 
if (!history.contains(trial)) {
String newSol = sol + dirLabels[i][0];
open.add(new Board(trial, newSol, x + dx, y + dy));
history.add(trial);
}
}
}
}
return "No solution";
}
 
public static void main(String[] a) {
String level = "#######,# #,# #,#. # #,#. $$ #,"
+ "#.$$ #,#.# @#,#######";
System.out.println(new Sokoban(level.split(",")).solve());
}
}</syntaxhighlight>
 
<pre>ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
 
=={{header|Julia}}==
{{trans|Go}}
<syntaxhighlight lang="julia">struct BoardState
board::String
csol::String
position::Int
end
 
function move(s::BoardState, dpos)
buffer = Vector{UInt8}(deepcopy(s.board))
if s.board[s.position] == '@'
buffer[s.position] = ' '
else
buffer[s.position] = '.'
end
newpos = s.position + dpos
if s.board[newpos] == ' '
buffer[newpos] = '@'
else
buffer[newpos] = '+'
end
String(buffer)
end
 
function push(s::BoardState, dpos)
newpos = s.position + dpos
boxpos = newpos + dpos
if s.board[boxpos] != ' ' && s.board[boxpos] != '.'
return ""
end
buffer = Vector{UInt8}(deepcopy(s.board))
if s.board[s.position] == '@'
buffer[s.position] = ' '
else
buffer[s.position] = '.'
end
if s.board[newpos] == '$'
buffer[newpos] = '@'
else
buffer[newpos] = '+'
end
if s.board[boxpos] == ' '
buffer[boxpos] = '$'
else
buffer[boxpos] = '*'
end
String(buffer)
end
 
function solve(board)
width = findfirst("\n", board[2:end])[1] + 1
dopt = (u = -width, l = -1, d = width, r = 1)
visited = Dict(board => true)
open::Vector{BoardState} = [BoardState(board, "", findfirst("@", board)[1])]
while length(open) > 0
s1 = open[1]
open = open[2:end]
for dir in keys(dopt)
newpos = s1.position + dopt[dir]
x = s1.board[newpos]
if x == '$' || x == '*'
newboard = push(s1, dopt[dir])
if newboard == "" || haskey(visited, newboard)
continue
end
newsol = s1.csol * uppercase(string(dir))
if findfirst(r"[\.\+]", newboard) == nothing
return newsol
end
elseif x == ' ' || x == '.'
newboard = move(s1, dopt[dir])
if haskey(visited, newboard)
continue
end
newsol = s1.csol * string(dir)
else
continue
end
open = push!(open, BoardState(newboard, newsol, newpos))
visited[newboard] = true
end
end
"No solution" # we should only get here if no solution to the sokoban
end
 
const testlevel = strip(raw"""
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######""")
println("For sokoban level:\n$testlevel\n...solution is :\n$(solve(testlevel))")
</syntaxhighlight>{{output}}<pre>
For sokoban level:
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
...solution is :
ulULLulDDurrrddlULrruLrUruLLLulD
</pre>
 
=={{header|Kotlin}}==
{{trans|Java}}
<syntaxhighlight lang="scala">// version 1.2.0
 
import java.util.LinkedList
 
class Sokoban(board: List<String>) {
val destBoard: String
val currBoard: String
val nCols = board[0].length
var playerX = 0
var playerY = 0
 
init {
val destBuf = StringBuilder()
val currBuf = StringBuilder()
for (r in 0 until board.size) {
for (c in 0 until nCols) {
val ch = board[r][c]
destBuf.append(if (ch != '$' && ch != '@') ch else ' ')
currBuf.append(if (ch != '.') ch else ' ')
if (ch == '@') {
playerX = c
playerY = r
}
}
}
destBoard = destBuf.toString()
currBoard = currBuf.toString()
}
 
fun move(x: Int, y: Int, dx: Int, dy: Int, trialBoard: String): String {
val newPlayerPos = (y + dy) * nCols + x + dx
if (trialBoard[newPlayerPos] != ' ') return ""
val trial = trialBoard.toCharArray()
trial[y * nCols + x] = ' '
trial[newPlayerPos] = '@'
return String(trial)
}
 
fun push(x: Int, y: Int, dx: Int, dy: Int, trialBoard: String): String {
val newBoxPos = (y + 2 * dy) * nCols + x + 2 * dx
if (trialBoard[newBoxPos] != ' ') return ""
val trial = trialBoard.toCharArray()
trial[y * nCols + x] = ' '
trial[(y + dy) * nCols + x + dx] = '@'
trial[newBoxPos] = '$'
return String(trial)
}
 
fun isSolved(trialBoard: String): Boolean {
for (i in 0 until trialBoard.length) {
if ((destBoard[i] == '.') != (trialBoard[i] == '$')) return false
}
return true
}
 
fun solve(): String {
data class Board(val cur: String, val sol: String, val x: Int, val y: Int)
val dirLabels = listOf('u' to 'U', 'r' to 'R', 'd' to 'D', 'l' to 'L')
val dirs = listOf(0 to -1, 1 to 0, 0 to 1, -1 to 0)
val history = mutableSetOf<String>()
history.add(currBoard)
val open = LinkedList<Board>()
open.add(Board(currBoard, "", playerX, playerY))
 
while (!open.isEmpty()) {
val (cur, sol, x, y) = open.poll()
for (i in 0 until dirs.size) {
var trial = cur
val dx = dirs[i].first
val dy = dirs[i].second
 
// are we standing next to a box ?
if (trial[(y + dy) * nCols + x + dx] == '$') {
 
// can we push it ?
trial = push(x, y, dx, dy, trial)
if (!trial.isEmpty()) {
 
// or did we already try this one ?
if (trial !in history) {
val newSol = sol + dirLabels[i].second
if (isSolved(trial)) return newSol
open.add(Board(trial, newSol, x + dx, y + dy))
history.add(trial)
}
}
} // otherwise try changing position
else {
trial = move(x, y, dx, dy, trial)
if (!trial.isEmpty() && trial !in history) {
val newSol = sol + dirLabels[i].first
open.add(Board(trial, newSol, x + dx, y + dy))
history.add(trial)
}
}
}
}
return "No solution"
}
}
 
fun main(args: Array<String>) {
val level = listOf(
"#######",
"# #",
"# #",
"#. # #",
"#. $$ #",
"#.$$ #",
"#.# @#",
"#######"
)
println(level.joinToString("\n"))
println()
println(Sokoban(level).solve())
}</syntaxhighlight>
 
{{out}}
<pre>
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
ulULLulDDurrrddlULrruLLrrUruLLLulD
</pre>
 
=={{header|Nim}}==
{{trans|Kotlin}}
We have chosen to use a double queue (deque) instead of a linked list.
 
<syntaxhighlight lang="nim">import deques, sets, strutils
 
type
 
Sokoban = object
destBoard: string
currBoard: string
nCols: Natural
playerX: Natural
playerY: Natural
 
Board = tuple[cur, sol: string; x, y: int]
 
 
func initSokoban(board: openArray[string]): Sokoban =
result.nCols = board[0].len
for row in 0..board.high:
for col in 0..<result.nCols:
let ch = board[row][col]
result.destBoard.add if ch notin ['$', '@']: ch else: ' '
result.currBoard.add if ch != '.': ch else: ' '
if ch == '@':
result.playerX = col
result.playerY = row
 
 
func move(sokoban: Sokoban; x, y, dx, dy: int; trialBoard: string): string =
let newPlayerPos = (y + dy) * sokoban.nCols + x + dx
if trialBoard[newPlayerPos] != ' ': return
result = trialBoard
result[y * sokoban.nCols + x] = ' '
result[newPlayerPos] = '@'
 
 
func push(sokoban: Sokoban; x, y, dx, dy: int; trialBoard: string): string =
let newBoxPos = (y + 2 * dy) * sokoban.nCols + x + 2 * dx
if trialBoard[newBoxPos] != ' ': return
result = trialBoard
result[y * sokoban.nCols + x] = ' '
result[(y + dy) * sokoban.nCols + x + dx] = '@'
result[newBoxPos] = '$'
 
 
func isSolved(sokoban: Sokoban; trialBoard: string): bool =
for i in 0..trialBoard.high:
if (sokoban.destBoard[i] == '.') != (trialBoard[i] == '$'): return false
result = true
 
 
func solve(sokoban: Sokoban): string =
var history: HashSet[string]
history.incl sokoban.currBoard
const Dirs = [(0, -1, 'u', 'U'), (1, 0, 'r', 'R'), (0, 1, 'd', 'D'), (-1, 0, 'l', 'L')]
var open: Deque[Board]
open.addLast (sokoban.currBoard, "", sokoban.playerX, sokoban.playerY)
 
while open.len != 0:
let (cur, sol, x, y) = open.popFirst()
for dir in Dirs:
var trial = cur
let dx = dir[0]
let dy = dir[1]
 
# Are we standing next to a box?
if trial[(y + dy) * sokoban.nCols + x + dx] == '$':
# Can we push it?
trial = sokoban.push(x, y, dx, dy, trial)
if trial.len != 0:
# Or did we already try this one?
if trial notin history:
let newSol = sol & dir[3]
if sokoban.isSolved(trial): return newSol
open.addLast (trial, newSol, x + dx, y + dy)
history.incl trial
 
else:
# Try to change position.
trial = sokoban.move(x, y, dx, dy, trial)
if trial.len != 0 and trial notin history:
let newSol = sol & dir[2]
open.addLast (trial, newSol, x + dx, y + dy)
history.incl trial
 
result = "no solution"
 
when isMainModule:
const Level = ["#######",
"# #",
"# #",
"#. # #",
"#. $$ #",
"#.$$ #",
"#.# @#",
"#######"]
 
echo Level.join("\n")
echo()
echo initSokoban(Level).solve()</syntaxhighlight>
 
{{out}}
<pre>#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
 
=={{header|OCaml}}==
{{trans|Python}}
This uses a breadth-first move search, so will find a move-optimal solution.
<langsyntaxhighlight OCamllang="ocaml">type dir = U | D | L | R
type move_t = Move of dir | Push of dir
 
Line 1,481 ⟶ 2,654:
 
let store = Hashtbl.create 251
let mark t = Hashtbl.addreplace store t true()
let marked t = Hashtbl.mem store t
 
Line 1,563 ⟶ 2,736:
"#.# @#";
"#######"] in
solve level</langsyntaxhighlight>
Output:
<pre>luULLulDDurrrddlULrruLLrrUruLLLulD</pre>
 
=={{header|Perl}}==
This performs simultaneous breadth first searches, starting from the initial state
Line 1,582 ⟶ 2,756:
would still be a valid solution. I could fix it, but at the cost of speed and memory.
 
<langsyntaxhighlight Perllang="perl">#!perl
use strict;
use warnings qw(FATAL all);
Line 1,786 ⟶ 2,960:
}
__END__
</syntaxhighlight>
</lang>
{{out}}
<pre>Solution found!
Line 1,805 ⟶ 2,979:
Although my code doesn't print out the actual final board, it would be easy enough
to compute from the move list.
 
=={{header|Perl 6}}==
=={{transheader|GoPhix}}==
Push-optimised, prunes (breadth-first) search space to reachable pushable-to-live boxes.<br>
<lang perl6>sub MAIN() {
Fairly fast, but often produces same-push-tally but longer results than move-optimised.
my $level = q:to//;
<syntaxhighlight lang="phix">-- demo\rosetta\Sokoban.exw
integer w, h -- (set from parsing the input grid)
sequence moves -- "", as +/-w and +/-1 (udlr)
string live -- "", Y if box can go there
 
function reachable(sequence pushes, string level)
integer p = find_any("@+",level)
string ok = repeat('N',length(level))
ok[p] = 'Y'
while true do
p = find('Y',ok)
if p=0 then exit end if
ok[p] = 'y'
for i=1 to length(moves) do
integer pn = p+moves[i]
if ok[pn]='N'
and find(level[pn]," .") then
ok[pn] = 'Y'
end if
end for
end while
for i=length(pushes)-1 to 1 by -2 do
if ok[pushes[i]-pushes[i+1]]!='y' then
pushes[i..i+1] = {}
end if
end for
return pushes
end function
 
function pushable(string level)
sequence res = {}
for i=1 to length(level) do
if find(level[i],"$*") then
if find(level[i-w]," .@+")
and find(level[i+w]," .@+") then
if live[i-w]='Y' then res &= {i,-w} end if
if live[i+w]='Y' then res &= {i,+w} end if
end if
if find(level[i-1]," .@+")
and find(level[i+1]," .@+") then
if live[i-1]='Y' then res &= {i,-1} end if
if live[i+1]='Y' then res &= {i,+1} end if
end if
end if
end for
return reachable(res,level)
end function
 
function solve(string level)
atom t2 = time()+2
integer seen = new_dict()
sequence solution = "No solution.", partial = {}
sequence todo = {{level,partial,pushable(level)}}, pushes
while length(todo) do
sequence t1 = todo[1]
todo = todo[2..$]
{level,partial,pushes} = t1
integer p = find_any("@+",level)
while length(pushes) do
integer {s,m} = pushes[1..2]
pushes = pushes[3..$]
level[p] = " ."[find(level[p],"@+")]
level[s] = "@+"[find(level[s],"$*")]
level[s+m] = "$*"[find(level[s+m]," .")]
if getd_index(level,seen)=0 then
sequence np = partial&{s,m}
if not find('$',level) then
solution = np
todo = {}
pushes = {}
exit
end if
setd(level,true,seen)
if time()>t2 then
printf(1,"working... (seen %d)\r",dict_size(seen))
t2 = time()+2
end if
todo = append(todo,{level,np,pushable(level)})
end if
level = t1[1] -- (reset)
end while
end while
destroy_dict(seen)
return solution
end function
 
procedure plays(string level, sequence solution)
-- This plays push-only solutions (see play() for lurd)
string res = level
integer p = find_any("@+",level)
for i=1 to length(solution) by 2 do
integer {s,m} = solution[i..i+1] m+=s
level[p] = " ."[find(level[p],"@+")]
level[s] = "@+"[find(level[s],"$*")]
level[m] = "$*"[find(level[m]," .")]
res &= level
p = s
end for
-- (replacing +0 with 1/2/3 may help in some cases)
puts(1,join_by(split(res,'\n'),h,floor(80/(w+2))+0))
end procedure
 
procedure mark_live(integer p, string level)
-- (idea cribbed from the C version)
if live[p]='N' then
live[p] = 'Y'
integer l = length(level)
if p-w*2>=1 and level[p-w]!='#' and level[p-w*2]!='#' then mark_live(p-w,level) end if
if p+w*2<=l and level[p+w]!='#' and level[p+w*2]!='#' then mark_live(p+w,level) end if
if p-2 >=1 and level[p-1]!='#' and level[p-2] !='#' then mark_live(p-1,level) end if
if p+2 <=l and level[p+1]!='#' and level[p+2] !='#' then mark_live(p+1,level) end if
end if
end procedure
 
function make_square(string level)
--
-- Sets {h, w, moves, live}, and returns an evened-out/rectangular level
--
if level[$]!='\n' then level &= '\n' end if -- (for the display)
sequence lines = split(level,'\n')
h = length(lines)-1 -- set height (ignore trailing \n)
sequence ln = repeat(0,h)
for i=1 to h do
ln[i] = {length(lines[i]),i}
for j=1 to length(lines[i]) do
-- validate each line, why not
if not find(lines[i,j]," #.$@*") then
crash("invalid input")
end if
end for
end for
ln = sort(ln)
w = ln[$][1]+1 -- set width (==longest, inc \n)
moves = {-w,+w,-1,+1} -- and make these (udlr) legal ...
for i=1 to h do
integer {l,n} = ln[i], pad = w-1-l
if pad=0 then exit end if
lines[n] &= repeat(' ',pad) -- ... by evening up the "grid"
end for
level = join(lines,'\n')
live = join(repeat(repeat('N',w-1),h),'\n')
for p=1 to length(level) do
if find(level[p],".+*") then
mark_live(p,level)
end if
end for
return level
end function
 
constant input = """
#######
# #
Line 1,817 ⟶ 3,141:
#.# @#
#######
"""
 
atom t0 = time()
say 'level:';
string level = make_square(input)
print $level;
sequence pushset = solve(level)
say 'solution:';
integer pop = length(pushset)/2
say solve($level);
if string(pushset) then
}
puts(1,level)
printf(1,"%s\n",{pushset}) -- ("No Solution.")
class State {
else
has Str $.board;
printf(1,"solution of %d pushes (%s)\n",{pop,elapsed(time()-t0)})
has Str $.sol;
plays(level,pushset)
has Int $.pos;
end if</syntaxhighlight>
{{out}}
Note that a full solution in LURD format would show as 48 moves, as opposed to
the move-optimal solutions of other entries of 34 moves, but both are 14 pushes.
<pre>
solution of 14 pushes (0.5s)
####### ####### ####### ####### ####### ####### ####### #######
# # # # # # # # # # # # # # # #
# # # # # $ # # $@ # # $@ # #$@ # #@ # # #
#. # # #. #$ # #. #@ # #. # # #. # # #. # # #* # # #* # #
#. $$ # #. $@ # #. $ # #. $ # #. $ # #. $ # #. $ # #.$@ #
#.$$ # #.$$ # #.$$ # #.$$ # #.$$ # #.$$ # #.$$ # #.$$ #
#.# @# #.# # #.# # #.# # #.# # #.# # #.# # #.# #
####### ####### ####### ####### ####### ####### ####### #######
 
####### ####### ####### ####### ####### ####### #######
method move(Int $delta --> Str) {
# # # # # # # # # # # # # #
my $new = $!board;
# # # # # # # # # # # # # #
if $new.substr($!pos,1) eq '@' {
#* # # #* # # #* # # #* # # #* # # #* # # #* # #
substr-rw($new,$!pos,1) = ' ';
#.$$ # #.$$ # #.@$ # #. $ # #.$@ # #*@ # #* #
} else {
#.$@ # #*@ # #*$ # substr-rw(#+$new, # #.$!pos,1) = ' # #.';$ # #*@ #
#.# # #.# # #.# # #*# # #*# # #*# # #*# #
}
####### ####### ####### ####### ####### ####### #######
my $pos := $!pos + $delta;
</pre>
if $new.substr($pos,1) eq ' ' {
substr-rw($new,$pos,1) = '@';
} else {
substr-rw($new,$pos,1) = '+';
}
return $new;
}
method push(Int $delta --> Str) {
my $pos := $!pos + $delta;
my $box := $pos + $delta;
return '' unless $!board.substr($box,1) eq ' ' | '.';
my $new = $!board;
if $new.substr($!pos,1) eq '@' {
substr-rw($new,$!pos,1) = ' ';
} else {
substr-rw($new,$!pos,1) = '.';
}
if $new.substr($pos,1) eq '$' {
substr-rw($new,$pos,1) = '@';
} else {
substr-rw($new,$pos,1) = '+';
}
if $new.substr($box,1) eq ' ' {
substr-rw($new,$box,1) = '$';
} else {
substr-rw($new,$box,1) = '*';
}
return $new;
}
}
sub solve(Str $start --> Str) {
my $board = $start;
my $width = $board.lines[0].chars + 1;
my @dirs =
["u", "U", -$width],
["r", "R", 1],
["d", "D", $width],
["l", "L", -1];
 
Other tests:
my %visited = $board => True;
<syntaxhighlight lang="phix">constant input = """
#############
# # #
# $$$$$$$ @#
#....... #
#############
"""</syntaxhighlight>
{{out}}
<pre>
solution of 30 pushes (14.6s)
############# ############# ############# ############# ############# ############# ############# #############
# # # # # # # # # # # # # # # # # # # # # # # #
# $$$$$$$ @# # @$$$$$$ # # $$$$$$ # # $$$$$$ # # $$$$$$ # # $$$$$$ # # $$$$$$ # # $$$$$$ #
#....... # #.*..... # #.+*.... # #..+*... # #...+*.. # #....+*. # #.....+* # #......+$ #
############# ############# ############# ############# ############# ############# ############# #############
 
############# ############# ############# ############# ############# ############# ############# #############
my $pos = $board.index('@');
# # # # # # # # # # # # # # # # # # # # # # # #
my @open = State.new(:$board, :sol(''), :$pos);
# $$$$$$ # # $$$$$$ # # $@$$$$ # # $@ $$$$ # # @ $$$$ # # $@$$ # # $@ $$ # # $@ $$ #
while @open {
#.......@$ # #....... @$ # #...*... $ # #...*... $ # #.*.*... $ # #.*.*.*. $ # #.*.*.*. $ # #.*.*.*. $ #
my $state = @open.shift;
############# ############# ############# ############# ############# ############# ############# #############
for @dirs -> [$move, $push, $delta] {
 
my $board;
############# ############# ############# ############# ############# ############# ############# #############
my $sol;
# # # # # # # # # # # # # # # # # # # # # # # #
my $pos = $state.pos + $delta;
# $@ $$ # #$@ $$ # #@ $$ # # $@ # # $@ # # $@ # # $@ # # $ #
given $state.board.substr($pos,1) {
#.*.*.*. $ # #.*.*.*. $ # #**.*.*. $ # #**.*.*.$ $ # #**.*.*.$ $ # #**.*.*.$ $ # #**.*.*.$ $ # #***+.*.$ $ #
when '$' | '*' {
############# ############# ############# ############# ############# ############# ############# #############
$board = $state.push($delta);
 
next if $board eq "" || %visited{$board};
############# ############# ############# ############# ############# ############# #############
$sol = $state.sol ~ $push;
# # # # # return $sol unless $board# ~~ /<[ .# + ]>/;# # # # # # # # # # # # # #
# @ # # # # # # # # # # # # #
}
#****.*.$ $ # #*****+.$ $ # #*****.*@ $ # #******+ $ # #******. $@ # #******.$@ # #*******@ #
when ' ' | '.' {
############# ############# ############# ############# ############# ############# #############
$board = $state.move($delta);
</pre>
next if %visited{$board};
Test #3
$sol = $state.sol ~ $move;
<syntaxhighlight lang="phix">constant input = """
}
####
default { next }
##. }##
##### . #
say $sol;
# # # #
@open.push: State.new: :$board, :$sol, :$pos;
# $ # # #
%visited{$board} = True;
# $ @ }#
###### ##
}
####
return "No solution";
"""</syntaxhighlight>
}</lang>
{{out}}
<pre>Level:
solution of 16 pushes (0.0s)
#######
#### #### #### #### #### #### #### #### ####
# #
##. ## ##. ## ##. ## ##. ## ##. ## ##. ## ##. ## ##. ## ##. ##
# #
##### . # ##### . # ##### . # ##### . # ##### . # ##### . # ##### . # ##### . # ##### * #
#. # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # $# # # # @# #
#. $$ #
# $ # # # # @$# # # # $# # # # $# # # # $# # # # $# # # # $# $# # # $# @# # # $# # #
#.$$ #
# $ @ # # $ # # @$ # # @$ # # @$ # # @$ # # @ # # # # #
#.# @#
###### ## ###### ## ###### ## ###### ## ###### ## ###### ## ###### ## ###### ## ###### ##
#######
#### #### #### #### #### #### #### #### ####
Solution:
 
ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
#### #### #### #### #### #### #### ####
##* ## ##* ## ##* ## ##* ## ##* ## ##* ## ##* ## ##* ##
##### + # ##### . # ##### . # ##### . # ##### . # ##### . # ##### . # ##### * #
# # # # # # # # # # # # # # # # # # # # # # # # # # $# # # # @# #
# $# # # # @# # # # # # # # # # # # # # # # # $# # # # @# # # # # #
# # # $ # # @$ # # @$ # # @$ # # @ # # # # #
###### ## ###### ## ###### ## ###### ## ###### ## ###### ## ###### ## ###### ##
#### #### #### #### #### #### #### ####
</pre>
Test #4
<syntaxhighlight lang="phix">constant input = """
#############
#... # #
#.$$$$$$$ @#
#... #
#############
"""</syntaxhighlight>
{{out}}
<pre>
"started"
solution of 40 pushes (58.5s)
############# ############# ############# ############# ############# #############
#... # # #... # # #.*. # # #.** # # #.** # # #.** # #
#.$$$$$$$ @# #.$$@$$$$ # #.@$ $$$$ # #. @ $$$$ # #. $$$$ # #. $$$$ #
#... # #...$ # #...$ # #...$ # #...@$ # #... @$ #
############# ############# ############# ############# ############# #############
<snip 30 pushes>
############# ############# ############# ############# #############
#*** # # #*** # # #*** # # #*** # # #*** # #
#* # #* # #* # #* # #* #
#**. $@ # #**. $@ # #**. $@ # #**.$@ # #***@ #
############# ############# ############# ############# #############
</pre>
Test #5
<syntaxhighlight lang="phix">constant input = """
#####
# #
# #
### #$##
# #
### #$## # ######
# # ## ##### .#
# $ $ ..#
##### ### #@## .#
# #########
#######
"""</syntaxhighlight>
{{out}}
<pre>
solution of 59 pushes (25.5s)
##### ##### ##### #####
# # # # # # # #
# # # # # # # #
### #$## ### #@## ### # ## ### # ##
# # # $ # # $@ # # $ #
### #$## # ###### ### #$## # ###### ### #$## # ###### ### #@## # ######
# # ## ##### .# # # ## ##### .# # # ## ##### .# # #$## ##### .#
# $ $ ..# # $ $ ..# # $ $ ..# # $ $ ..#
##### ### #@## .# ##### ### # ## .# ##### ### # ## .# ##### ### # ## .#
# ######### # ######### # ######### # #########
####### ####### ####### #######
 
<snip 52 pushes>
 
##### ##### ##### #####
# # # # # # # #
# # # # # # # #
### # ## ### # ## ### # ## ### # ##
# # # # # # # #
### # ## # ###### ### # ## # ###### ### # ## # ###### ### # ## # ######
# # ## ##### *# # # ## ##### *# # # ## ##### *# # # ## ##### *#
# @$.*# # @**# # **# # **#
##### ### # ## $ .# ##### ### # ## $ .# ##### ### # ## @$.# ##### ### # ## @*#
# ######### # ######### # ######### # #########
####### ####### ####### #######
</pre>
 
=={{header|PicoLisp}}==
This searches for a solution, without trying for the push-optimal one. The player moves between the pushes, however, are minimized.
<langsyntaxhighlight PicoLisplang="picolisp">(load "@lib/simul.l")
 
# Display board
Line 2,005 ⟶ 3,396:
(pushes) )
(display) # Display solution
(pack (flip Res)) ) ) )</langsyntaxhighlight>
Test:
<langsyntaxhighlight PicoLisplang="picolisp">(main
(quote
"#######"
Line 2,018 ⟶ 3,409:
"#######" ) )
(prinl)
(go)</langsyntaxhighlight>
Output:
<pre> 8 # # # # # # #
Line 2,045 ⟶ 3,436:
{{works with|Psyco}}
{{works with|Python 2.6}}
<langsyntaxhighlight lang="python">from array import array
from collections import deque
import psyco
Line 2,140 ⟶ 3,531:
psyco.full()
init(level)
print level, "\n\n", solve()</langsyntaxhighlight>
Output:
<pre>#######
Line 2,157 ⟶ 3,548:
This was originally inspired by PicoLisp's solution. Modified to use a priority queue as mentioned on the Sokoban wiki for the main breadth first search on pushes but just a plain queue for the move bfs. This uses personal libraries. Vector2 isn't strictly needed but the math/array library is not currently optimized for untyped Racket. push! is comparable to lisp's, awhen is anaphoric when, ret uses the bound value as the result of its expression, and tstruct is short for struct with the #:transparent option.
 
<syntaxhighlight lang="racket">
<lang Racket>
#lang racket
(require data/heap
Line 2,265 ⟶ 3,656:
(pushes s clear))
(bfs)])))
</syntaxhighlight>
</lang>
 
{{out}}
Line 2,274 ⟶ 3,665:
"uuulDLLrrrddllUdrruulLrrdLuuulldlDDuuurrrddlLrrddlULrruLdlUUdrruulLulD"
</pre>
 
=={{header|Raku}}==
(formerly Perl 6)
{{trans|Go}}
<syntaxhighlight lang="raku" line>sub MAIN() {
my $level = q:to//;
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
say 'level:';
print $level;
say 'solution:';
say solve($level);
}
class State {
has Str $.board;
has Str $.sol;
has Int $.pos;
 
method move(Int $delta --> Str) {
my $new = $!board;
if $new.substr($!pos,1) eq '@' {
substr-rw($new,$!pos,1) = ' ';
} else {
substr-rw($new,$!pos,1) = '.';
}
my $pos := $!pos + $delta;
if $new.substr($pos,1) eq ' ' {
substr-rw($new,$pos,1) = '@';
} else {
substr-rw($new,$pos,1) = '+';
}
return $new;
}
method push(Int $delta --> Str) {
my $pos := $!pos + $delta;
my $box := $pos + $delta;
return '' unless $!board.substr($box,1) eq ' ' | '.';
my $new = $!board;
if $new.substr($!pos,1) eq '@' {
substr-rw($new,$!pos,1) = ' ';
} else {
substr-rw($new,$!pos,1) = '.';
}
if $new.substr($pos,1) eq '$' {
substr-rw($new,$pos,1) = '@';
} else {
substr-rw($new,$pos,1) = '+';
}
if $new.substr($box,1) eq ' ' {
substr-rw($new,$box,1) = '$';
} else {
substr-rw($new,$box,1) = '*';
}
return $new;
}
}
sub solve(Str $start --> Str) {
my $board = $start;
my $width = $board.lines[0].chars + 1;
my @dirs =
["u", "U", -$width],
["r", "R", 1],
["d", "D", $width],
["l", "L", -1];
 
my %visited = $board => True;
 
my $pos = $board.index('@');
my @open = State.new(:$board, :sol(''), :$pos);
while @open {
my $state = @open.shift;
for @dirs -> [$move, $push, $delta] {
my $board;
my $sol;
my $pos = $state.pos + $delta;
given $state.board.substr($pos,1) {
when '$' | '*' {
$board = $state.push($delta);
next if $board eq "" || %visited{$board};
$sol = $state.sol ~ $push;
return $sol unless $board ~~ /<[ . + ]>/;
}
when ' ' | '.' {
$board = $state.move($delta);
next if %visited{$board};
$sol = $state.sol ~ $move;
}
default { next }
}
@open.push: State.new: :$board, :$sol, :$pos;
%visited{$board} = True;
}
}
return "No solution";
}</syntaxhighlight>
{{out}}
<pre>Level:
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
Solution:
ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
 
=={{header|Ring}}==
<syntaxhighlight lang="ring">
#--------------------------------------------------#
# Sokoban Game #
#--------------------------------------------------#
 
# Game Data
 
aPlayer = [ :Row = 3, :Col = 4 ]
 
aLevel1 = [
[1,1,1,2,2,2,2,2,1,1,1,1,1,1],
[1,2,2,2,1,1,1,2,1,1,1,1,1,1],
[1,2,4,3,5,1,1,2,1,1,1,1,1,1],
[1,2,2,2,1,5,4,2,1,1,1,1,1,1],
[1,2,4,2,2,5,1,2,1,1,1,1,1,1],
[1,2,1,2,1,4,1,2,2,1,1,1,1,1],
[1,2,5,1,6,5,5,4,2,1,1,1,1,1],
[1,2,1,1,1,4,1,1,2,1,1,1,1,1],
[1,2,2,2,2,2,2,2,2,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1]
]
 
aLevel2 = [
[1,1,1,2,2,2,2,2,2,2,2,2,1,1],
[1,2,2,2,1,5,1,4,1,1,1,2,1,1],
[1,2,4,3,5,1,1,1,5,1,1,2,1,1],
[1,2,2,2,1,1,4,1,1,1,1,2,1,1],
[1,2,4,2,2,1,5,4,1,5,1,2,1,1],
[1,2,1,2,1,4,1,5,1,1,2,2,1,1],
[1,2,5,1,6,5,1,4,1,1,1,2,1,1],
[1,2,1,1,1,4,1,4,1,5,1,2,1,1],
[1,2,2,2,2,2,2,2,2,2,2,2,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1]
]
 
aLevel = aLevel1
nActiveLevel = 1
 
# For Game Restart
aLevel1Copy = aLevel1
aLevel2Copy = aLevel2
aPlayerCopy = aPlayer
 
C_LEVEL_ROWSCOUNT = 10
C_LEVEL_COLSCOUNT = 14
 
C_EMPTY = 1
C_WALL = 2
C_PLAYER = 3
C_DOOR = 4
C_BOX = 5
C_BOXONDOOR = 6
C_PLAYERONDOOR = 7
 
nKeyClock = clock()
 
# Will be used when moving a Box
aCurrentBox = [ :Row = 0, :Col = 0 ]
nRowDiff = 0
nColDiff = 0
 
# When the player win
lPlayerWin = False
 
load "gameengine.ring"
 
func main
 
oGame = New Game
{
 
title = "Sokoban"
 
Map {
 
blockwidth = 60
blockheight = 60
 
aMap = aLevel
 
aImages = [
"images/empty.jpg",
"images/wall.jpg",
"images/player.jpg",
"images/door.jpg",
"images/box.jpg",
"images/boxondoor.jpg",
"images/player.jpg" # Player on Door
]
 
keypress = func oGame,oSelf,nkey {
# Avoid getting many keys in short time
if (clock() - nKeyClock) < clockspersecond()/4 return ok
nKeyClock = Clock()
Switch nkey
on Key_Esc
oGame.Shutdown()
on Key_Space
# Restart the Level
if nActiveLevel = 1
aLevel = aLevel1Copy
else
aLevel = aLevel2Copy
ok
aPlayer = aPlayerCopy
UpdateGameMap(oGame)
lPlayerWin = False
on Key_Right
if aPlayer[:col] < C_LEVEL_COLSCOUNT
nRowDiff = 0 nColDiff = 1
MoveObject(oGame,PlayerType(),aPlayer[:row],aPlayer[:col]+1)
ok
on Key_Left
if aPlayer[:col] > 1
nRowDiff = 0 nColDiff = -1
MoveObject(oGame,PlayerType(),aPlayer[:row],aPlayer[:col]-1)
ok
on Key_Up
if aPlayer[:row] > 1
nRowDiff = -1 nColDiff = 0
MoveObject(oGame,PlayerType(),aPlayer[:row]-1,aPlayer[:col])
ok
on Key_Down
if aPlayer[:row] < C_LEVEL_ROWSCOUNT
nRowDiff = 1 nColDiff = 0
MoveObject(oGame,PlayerType(),aPlayer[:row]+1,aPlayer[:col])
ok
off
if lPlayerWin = False
if CheckWin()
lPlayerWin = True
DisplayYouWin(oGame)
ok
ok
}
 
}
 
text {
x = 70 y=550
animate = false
size = 20
file = "fonts/pirulen.ttf"
text = "Level:"
color = rgb(0,0,0)
}
NewButton(oGame,180,550,150,30,"Level 1",:Click1)
NewButton(oGame,350,550,150,30,"Level 2",:Click2)
}
 
func MoveObject oGame,nObjectType,nNewRow,nNewCol
lMove = False
switch nObjectType
on C_PLAYER
switch aLevel[nNewRow][nNewCol]
on C_EMPTY
aLevel[aPlayer[:row]][aPlayer[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_PLAYER
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
on C_DOOR
aLevel[aPlayer[:row]][aPlayer[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_PLAYERONDOOR
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
on C_BOX
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOX,nNewRow+nRowDiff,nNewCol+nColDiff)
aLevel[aPlayer[:row]][aPlayer[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_PLAYER
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
ok
on C_BOXONDOOR
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOXONDOOR,nNewRow+nRowDiff,nNewCol+nColDiff)
aLevel[aPlayer[:row]][aPlayer[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_PLAYERONDOOR
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
ok
off
on C_PLAYERONDOOR
switch aLevel[nNewRow][nNewCol]
on C_EMPTY
aLevel[aPlayer[:row]][aPlayer[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_PLAYER
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
on C_DOOR
aLevel[aPlayer[:row]][aPlayer[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_PLAYERONDOOR
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
on C_BOX
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOX,nNewRow+nRowDiff,nNewCol+nColDiff)
aLevel[aPlayer[:row]][aPlayer[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_PLAYER
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
ok
on C_BOXONDOOR
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOXONDOOR,nNewRow+nRowDiff,nNewCol+nColDiff)
aLevel[aPlayer[:row]][aPlayer[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_PLAYER
UpdateGameMap(oGame)
aPlayer[:row] = nNewRow
aPlayer[:col] = nNewCol
lMove = True
ok
off
on C_BOX
switch aLevel[nNewRow][nNewCol]
on C_EMPTY
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_BOX
UpdateGameMap(oGame)
lMove = True
on C_DOOR
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_BOXONDOOR
UpdateGameMap(oGame)
lMove = True
on C_BOX
aOldBox = aCurrentBox
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOX,nNewRow+nRowDiff,nNewCol+nColDiff)
aCurrentBox = aOldBox
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_BOX
UpdateGameMap(oGame)
lMove = True
ok
on C_BOXONDOOR
aOldBox = aCurrentBox
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOXONDOOR,nNewRow+nRowDiff,nNewCol+nColDiff)
aCurrentBox = aOldBox
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_EMPTY
aLevel[nNewRow][nNewCol] = C_BOXONDOOR
UpdateGameMap(oGame)
lMove = True
ok
off
on C_BOXONDOOR
switch aLevel[nNewRow][nNewCol]
on C_EMPTY
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_BOX
UpdateGameMap(oGame)
lMove = True
on C_DOOR
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_BOXONDOOR
UpdateGameMap(oGame)
lMove = True
on C_BOX
aOldBox = aCurrentBox
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOX,nNewRow+nRowDiff,nNewCol+nColDiff)
aCurrentBox = aOldBox
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_BOX
UpdateGameMap(oGame)
lMove = True
ok
on C_BOXONDOOR
aOldBox = aCurrentBox
aCurrentBox[:row] = nNewRow
aCurrentBox[:col] = nNewCol
if MoveObject(oGame,C_BOXONDOOR,nNewRow+nRowDiff,nNewCol+nColDiff)
aCurrentBox = aOldBox
aLevel[aCurrentBox[:row]][aCurrentBox[:col]] = C_DOOR
aLevel[nNewRow][nNewCol] = C_BOXONDOOR
UpdateGameMap(oGame)
lMove = True
ok
 
off
off
return lMove
 
func UpdateGameMap oGame
# The Map is our first object in Game Objects
oGame.aObjects[1].aMap = aLevel
 
func PlayerType
# It could be (Player) or (Player on door)
return aLevel[aPlayer[:row]][aPlayer[:col]]
 
func CheckWin
for aRow in aLevel
if find(aRow,C_DOOR) or find(aRow,C_PLAYERONDOOR)
return False
ok
next
return True
 
func DisplayYouWin oGame
oGame {
text {
point = 400
size = 30
nStep = 9
file = "fonts/pirulen.ttf"
text = "You Win !!!"
x = 500 y=10
state = func ogame,oself {
if oself.y >= 400
ogame.remove(oSelf.nIndex)
ok
}
}
}
 
func NewButton oGame,nX,nY,nWidth,nHeight,cText,cFunc
oGame {
Object {
x = nX y=nY width = nWidth height=nHeight
AddAttribute(self,:Text)
AddAttribute(self,:EventCode)
Text = cText
EventCode = cFunc
draw = func oGame,oSelf {
oSelf {
gl_draw_filled_rectangle(x,y,x+width,y+height,gl_map_rgb(0,100,255))
gl_draw_rectangle(x,y,x+width,y+height,gl_map_rgb(0,0,0),2)
oFont = oResources.LoadFont("fonts/pirulen.ttf",20)
gl_draw_text(oFont,gl_map_rgb(0,0,0),x+width/2,y+5,1,Text)
}
}
mouse = func oGame,oSelf,nType,aMouseList {
if nType = GE_MOUSE_UP
MouseX = aMouseList[GE_MOUSE_X]
MouseY = aMouseList[GE_MOUSE_Y]
oSelf {
if MouseX >= x and MouseX <= X+270 and
MouseY >= y and MouseY <= Y+40
call EventCode(oGame,oSelf)
ok
}
ok
}
}
}
return len(oGame.aObjects)
 
func Click1 oGame,oSelf
aLevel = aLevel1
nActiveLevel = 1
aPlayer = aPlayerCopy
UpdateGameMap(oGame)
lPlayerWin = False
 
func Click2 oGame,oSelf
aLevel = aLevel2
nActiveLevel = 2
aPlayer = aPlayerCopy
UpdateGameMap(oGame)
lPlayerWin = False
</syntaxhighlight>
 
Output image:
 
[https://www.mediafire.com/view/6dk3ai36vapua2a/SokobanGame.jpg/file Sokoban game (Level 1)]
 
[https://www.mediafire.com/view/go66tyi6ij6jtup/SokobanGame2.jpg/file Sokoban game (Level 2)]
 
=={{header|Ruby}}==
===Simple Version===
{{trans|Python}}
<syntaxhighlight lang="ruby">require 'set'
 
class Sokoban
def initialize(level)
board = level.each_line.map(&:rstrip)
@nrows = board.map(&:size).max
board.map!{|line| line.ljust(@nrows)}
board.each_with_index do |row, r|
row.each_char.with_index do |ch, c|
@px, @py = c, r if ch == '@' or ch == '+'
end
end
@goal = board.join.tr(' .@#$+*', ' . ..')
.each_char.with_index.select{|ch, c| ch == '.'}
.map(&:last)
@board = board.join.tr(' .@#$+*', ' @#$ $')
end
def pos(x, y)
y * @nrows + x
end
def push(x, y, dx, dy, board) # modify board
return if board[pos(x+2*dx, y+2*dy)] != ' '
board[pos(x , y )] = ' '
board[pos(x + dx, y + dy)] = '@'
board[pos(x+2*dx, y+2*dy)] = '$'
end
def solved?(board)
@goal.all?{|i| board[i] == '$'}
end
DIRS = [[0, -1, 'u', 'U'], [ 1, 0, 'r', 'R'], [0, 1, 'd', 'D'], [-1, 0, 'l', 'L']]
def solve
queue = [[@board, "", @px, @py]]
visited = Set[@board]
until queue.empty?
current, csol, x, y = queue.shift
for dx, dy, cmove, cpush in DIRS
work = current.dup
case work[pos(x+dx, y+dy)] # next character
when '$'
next unless push(x, y, dx, dy, work)
next unless visited.add?(work)
return csol+cpush if solved?(work)
queue << [work, csol+cpush, x+dx, y+dy]
when ' '
work[pos(x, y)] = ' '
work[pos(x+dx, y+dy)] = '@'
queue << [work, csol+cmove, x+dx, y+dy] if visited.add?(work)
end
end
end
"No solution"
end
end</syntaxhighlight>
'''Test:'''
<syntaxhighlight lang="ruby">level = <<EOS
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
EOS
puts level, "", Sokoban.new(level).solve</syntaxhighlight>
 
{{out}}
<pre>
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
ulULLulDDurrrddlULrruLLrrUruLLLulD
</pre>
Runtime: about 3.2 seconds.
 
===Faster Version===
It examines beforehand the place where the box can not move to the goal.
When a box is pushed there, it doesn't process after that.
 
<syntaxhighlight lang="ruby">class Sokoban
def initialize(level)
board = level.lines.map(&:rstrip)
leng = board.map(&:length).max
board = board.map{|line| line.ljust(leng)}.join
@goal = []
board.each_char.with_index do |c, i|
@player = i if c == '@' or c == '+'
@goal << i if c == '.' or c == '+' or c == '*'
end
@board = board.tr(' .@#$+*', ' @#$ $')
@lurd = [[-1, 'l', 'L'], [-leng, 'u', 'U'], [1, 'r', 'R'], [leng, 'd', 'D']]
@dirs = @lurd.map(&:first)
set_dead_zone(board.tr('^#', ' '))
end
def set_dead_zone(wall)
corner = search_corner(wall)
@dead = corner.dup
begin
size = @dead.size
corner.each do |pos|
@dirs.each do |dir|
next if wall[pos + dir] == '#'
@dead.concat(check_side(wall, pos+dir, dir))
end
end
end until size == @dead.size
end
def search_corner(wall)
wall.size.times.with_object([]) do |i, corner|
next if wall[i] == '#' or @goal.include?(i)
case count_wall(wall, i)
when 2
corner << i if wall[i-1] != wall[i+1]
when 3
corner << i
end
end
end
def check_side(wall, pos, dir)
wk = []
until wall[pos] == '#' or count_wall(wall, pos) == 0 or @goal.include?(pos)
return wk if @dead.include?(pos)
wk << pos
pos += dir
end
[]
end
def count_wall(wall, pos)
@dirs.count{|dir| wall[pos + dir] == '#'}
end
def push_box(pos, dir, board)
return board if board[pos + 2*dir] != ' '
board[pos ] = ' '
board[pos + dir] = '@'
board[pos + 2*dir] = '$'
board
end
def solved?(board)
@goal.all?{|i| board[i] == '$'}
end
def solve
queue = [[@board, "", @player]]
# When the key doesn't exist in Hash, it subscribes a key but it returns false.
visited = Hash.new{|h,k| h[k]=true; false}
visited[@board] # first subscription
until queue.empty?
board, route, pos = queue.shift
@lurd.each do |dir, move, push|
work = board.dup
case work[pos+dir]
when '$' # push
work = push_box(pos, dir, work)
next if visited[work]
return route+push if solved?(work)
queue << [work, route+push, pos+dir] unless @dead.include?(pos+2*dir)
when ' ' # move
work[pos ] = ' '
work[pos+dir] = '@'
next if visited[work]
queue << [work, route+move, pos+dir]
end
end
end
"No solution"
end
end</syntaxhighlight>
Runtime: about 0.20 seconds.
 
=={{header|Tcl}}==
This code does a breadth-first search so it finds a solution with a minimum number of moves.
{{trans|OCaml}}<!-- the big difference is that this is all in one procedure for speed as it reduces the amount of packing/unpacking of tuples/lists, and the queue isn't shortened as that's significantly slower for these sorts of board sizes -->
<langsyntaxhighlight lang="tcl">package require Tcl 8.5
 
proc solveSokoban b {
Line 2,347 ⟶ 4,438:
}
error "no solution"
}</langsyntaxhighlight>
Demonstration code:
<langsyntaxhighlight lang="tcl">set level {
"#######"
"# #"
Line 2,359 ⟶ 4,450:
"#######"
}
puts [solveSokoban $level]</langsyntaxhighlight>
Output: <pre>ulULLulDDurrrddlULrruLLrrUruLLLulD</pre>
Runtime with stock Tcl 8.5 installation: ≅2.2 seconds<!-- on what is now a fairly old machine -->
 
=={{header|Wren}}==
{{trans|Kotlin}}
{{libheader|Wren-dynamic}}
{{libheader|Wren-llist}}
{{libheader|Wren-set}}
This works but at a rather sedate pace - 26.7 seconds.
<syntaxhighlight lang="wren">import "./dynamic" for Tuple
import "./llist" for DLinkedList
import "./set" for Set
 
var Board = Tuple.create("Board", ["cur", "sol", "x", "y"])
 
class Sokoban {
construct new(board) {
_destBoard = ""
_currBoard = ""
_nCols = board[0].count
_playerX = 0
_playerY = 0
for (r in 0...board.count) {
for (c in 0..._nCols) {
var ch = board[r][c]
_destBoard = _destBoard + ((ch != "$" && ch != "@") ? ch : " ")
_currBoard = _currBoard + ((ch != ".") ? ch : " ")
if (ch == "@") {
_playerX = c
_playerY = r
}
}
}
}
 
move(x, y, dx, dy, trialBoard) {
var newPlayerPos = (y + dy) * _nCols + x + dx
if (trialBoard[newPlayerPos] != " ") return ""
var trial = trialBoard.toList
trial[y * _nCols + x] = " "
trial[newPlayerPos] = "@"
return trial.join()
}
 
push(x, y, dx, dy, trialBoard) {
var newBoxPos = (y + 2 * dy) * _nCols + x + 2 * dx
if (trialBoard[newBoxPos] != " ") return ""
var trial = trialBoard.toList
trial[y * _nCols + x] = " "
trial[(y + dy) * _nCols + x + dx] = "@"
trial[newBoxPos] = "$"
return trial.join("")
}
 
isSolved(trialBoard) {
for (i in 0...trialBoard.count) {
if ((_destBoard[i] == ".") != (trialBoard[i] == "$")) return false
}
return true
}
 
solve() {
var dirLabels = [ ["u", "U"], ["r", "R"], ["d", "D"], ["l", "L"] ]
var dirs = [ [0, -1], [1, 0], [0, 1], [-1, 0] ]
var history = Set.new()
history.add(_currBoard)
var open = DLinkedList.new()
open.add(Board.new(_currBoard, "", _playerX, _playerY))
 
while (!open.isEmpty) {
var b = open.removeAt(0)
for (i in 0...dirs.count) {
var trial = b.cur
var dx = dirs[i][0]
var dy = dirs[i][1]
 
// are we standing next to a box ?
if (trial[(b.y + dy) * _nCols + b.x + dx] == "$") {
// can we push it ?
trial = push(b.x, b.y, dx, dy, trial)
if (!trial.isEmpty) {
// or did we already try this one ?
if (!history.contains(trial)) {
var newSol = b.sol + dirLabels[i][1]
if (isSolved(trial)) return newSol
open.add(Board.new(trial, newSol, b.x + dx, b.y + dy))
history.add(trial)
}
}
} else { // otherwise try changing position
trial = move(b.x, b.y, dx, dy, trial)
if (!trial.isEmpty && !history.contains(trial)) {
var newSol = b.sol + dirLabels[i][0]
open.add(Board.new(trial, newSol, b.x + dx, b.y + dy))
history.add(trial)
}
}
}
}
return "No solution"
}
}
 
var level = [
"#######",
"# #",
"# #",
"#. # #",
"#. $$ #",
"#.$$ #",
"#.# @#",
"#######"
]
System.print(level.join("\n"))
System.print()
System.print(Sokoban.new(level).solve())</syntaxhighlight>
 
{{out}}
<pre>
#######
# #
# #
#. # #
#. $$ #
#.$$ #
#.# @#
#######
 
ulULLulDDurrrddlULrruLLrrUruLLLulD
</pre>
9,476

edits