Pure Erlang implementation of Sokoban in a terminal, based on that workaround.
EDIT: This works on Windows as-is, but with a very noticeable flicker as the screen is cleared. Performance will vary across terminals and computers, and vary especially if it’s run over an ssh connection. This is where ncurses-style tricks are vital.
-module(sokoban).
-compile(export_all).
-record(game, {grid, moves, bound}).
sokoban(0) ->
[" #####",
" #.. #",
"### #",
"# $ #",
"# $ ##",
"#@ #",
"#####"];
sokoban(1) ->
[" #####",
" # #",
" #$ #",
" ### $##",
" # $ $ #",
"### # ## # ######",
"# # ## ##### ..#",
"# $ $ ..#",
"##### ### #@## ..#",
" # #########",
" #######"].
new_game(N) ->
G = grid_from(sokoban(N)),
B = lists:foldl(fun ({X, Y}, {Bx, By}) ->
{max(X, Bx), max(Y, By)}
end, {0, 0}, maps:keys(G)),
#game{moves=0, grid=G, bound=B}.
move("@ "++S) -> " @"++S;
move("@."++S) -> " &"++S;
move("& "++S) -> ".@"++S;
move("&."++S) -> ".&"++S;
move("@$ "++S) -> " @$"++S;
move("@$."++S) -> " @*"++S;
move("&$ "++S) -> ".@$"++S;
move("&$."++S) -> ".@*"++S;
move("@* "++S) -> " &$"++S;
move("@*."++S) -> " &*"++S;
move("&* "++S) -> ".&$"++S;
move("&*."++S) -> ".&*"++S;
move(S) -> S.
look(Grid, Loc={X, Y}, Delta={Dx, Dy}) ->
case maps:get(Loc, Grid, false) of
false -> [];
Char -> [Char|look(Grid, {X+Dx, Y+Dy}, Delta)]
end.
place(G, [], _, _) -> G;
place(G0, [Char|Look], Loc={X, Y}, Delta={Dx, Dy}) ->
G1 = maps:put(Loc, Char, G0),
place(G1, Look, {X+Dx, Y+Dy}, Delta).
move(Game, Delta) ->
Soko = soko(Game#game.grid),
L0 = look(Game#game.grid, Soko, Delta),
L1 = move(L0),
if
L0 == L1 -> Game;
true ->
G1 = place(Game#game.grid, L1, Soko, Delta),
Game#game{grid=G1, moves=Game#game.moves+1}
end.
soko(G) ->
case find($@, G) of
false -> find($&, G);
Loc -> Loc
end.
find(Char, G) -> find(Char, G, maps:keys(G)).
find(Char, G, [Loc|T]) ->
case maps:get(Loc, G) of
Char -> Loc;
_ -> find(Char, G, T)
end;
find(_, _, []) -> false.
grid_from(L) ->
{_, Grid} = lists:foldl(fun (Row, {Y, G0}) ->
{_, G1} = lists:foldl(
fun (Char, {X, G2}) ->
{X+1, maps:put({X, Y}, Char, G2)}
end, {1, G0}, Row),
{Y+1, G1}
end, {1, #{}}, L),
Grid.
redraw(Term, #game{grid=G, moves=Moves, bound={_, By}}) ->
write(Term, "\e[1;1H\e[2JE/H for easier/harder level, arrow keys to move\r\nMoves: ~p\r\n", [Moves]),
draw(Term, G, 1, By).
draw(_, _, Row, Bound) when Row > Bound -> ok;
draw(Term, G, Row, Bound) ->
write(Term, "~s\r\n", [look(G, {1, Row}, {1, 0})]),
draw(Term, G, Row+1, Bound).
write(Term, Fmt, Args) ->
ok = prim_tty:write(Term, unicode:characters_to_list(io_lib:format(Fmt, Args))).
start() -> play(prim_tty:init(#{}), new_game(0)).
wait(Term) ->
receive
{_, {data, <<"E">>}} ->
play(Term, new_game(0));
{_, {data, <<"H">>}} ->
play(Term, new_game(1));
_ ->
wait(Term)
end.
play(Term, Game) ->
redraw(Term, Game),
case find($$, Game#game.grid) of
false ->
write(Term, "*** You won in ~p moves ***\r\n", [Game#game.moves]),
wait(Term);
_ ->
receive
{_, {data, <<"\e[A">>}} -> play(Term, move(Game, {0, -1}));
{_, {data, <<"\e[B">>}} -> play(Term, move(Game, {0, 1}));
{_, {data, <<"\e[C">>}} -> play(Term, move(Game, {1, 0}));
{_, {data, <<"\e[D">>}} -> play(Term, move(Game, {-1, 0}));
{_, {data, <<"E">>}} -> play(Term, new_game(0));
{_, {data, <<"H">>}} -> play(Term, new_game(1));
_ -> play(Term, Game)
end
end.