この記事では、有名なソコバンのおもちゃの独自の実装を作成する方法と、それを最初から解決するためのアルゴリズムについて説明します。同時に、いくつかのデザインパターンとSOLIDの原則を実践します。
バックグラウンド
プッシュボタン式の電話を使用しています。エンターテインメントからはラジオと15レベルの唯一のゲームソコバン。これらのうち、私は10を解決しただけで、11番目に行き詰まりました。一日中電車に乗って、この不運な第11レベルを解決したが、成功しなかった。それから、このタスクに十分なプログラミング経験があるので、コンピューターを使用することにしました。私は目標を設定しました:このおもちゃの解決策を自分で実装することです。その結果、この記事が登場しました。
同じ11レベル(記事の最後にある解決策):
おもちゃ自体は、ボックス、壁、タグが点在する長方形の2Dフィールドです。ボックスは押すことはできますが、引くことはできません。これが全体の難しさです。あなたの目標は、すべてのボックスをマークのあるセルに移動することです。ゲームの例:
実装を書きます
GUI . - Rogue-like, . . :
- # , .
- O .
- X , .
- @ .
- . .
- G .
— . , . MVC — , , . , , . . . — - . , SOLID. , . — , — , — . , . . . :
- , , GUI . .
- .
— , :
public class ConsoleController implements Controller
{
private final Model model;
private final View view;
public ConsoleController(final Model model)
{
this.model = model;
this.view = new ConsoleView(model);
}
// methods
}
model , , view . , ConsoleController View, ConsoleView, . ConsoleView ConsoleController, , :
public class ConsoleController implements Controller
{
private final Model model;
private final View view;
public ConsoleController(final Model model, final View view)
{
this.model = model;
this.view = view;
}
// methods
}
ConsoleController . D SOLID , . . , — . , - Spring , . .
. , ?
-
(row, col)
,row
,col
, . — , . , , , . .
, . , , , . "" — , , . :
, , Model. Sokoban , . move()
. Sokoban . getCrates()
getMarks()
. , : A* (A star algorithm).
run()
" -> -> -> "
, :
###########
#.@..O..X.#
###########
- . " ". : CharSokobanFactory
, , , . , Sokoban
, , :
public Sokoban(final Field field, final Set<Point> crates, final Set<Point> marks, final Point player)
{
this.field = field;
this.crates = crates;
this.marks = marks;
this.player = player;
}
CharSokobanFactory
. , . .
vi-like :
- j —
- k —
- h —
- l —
, :
class Sokoban {
// some stuff
public boolean solved()
{
return marks.equals(crates);
}
// other stuff
}
if- , Direction, . Move, Direction move(direction)
. Move.resolve
, .
" ". : , 4 .
O SOLID, Open-Closed Principle: . , . , , . - , , .
:
:
class ConsoleController {
//
@Override
public void run()
{
view.say("Sokoban Starts");
char symbol = '0';
view.render();
final List<Move> history = new LinkedList<>();
while (symbol != 'q')
{
final Move move = Move.resolve(symbol);
if (move != null)
{
history.add(move);
move.perform(sokoban);
view.render();
if (sokoban.solved())
{
view.say("YOU WIN!");
break;
}
}
try
{
symbol = (char) System.in.read();
}
catch (IOException io)
{
view.say(" :");
throw new IllegalStateException(io);
}
}
view.say("Your moves: " + Move.compress(history));
}
//
}
, , . , "" , , . .
:
class ConsoleView {
//
@Override
public void render()
{
clearConsole();
for (int i = 0; i < sokoban.numberOfFieldRows(); i++)
{
for (int j = 0; j < sokoban.numberOfFieldCols(); j++)
{
final Point at = new Point(i, j);
final Tile tileAt = sokoban.tile(at);
if (tileAt == null)
break;
final char symbolToPrint = tileAt == Tile.CRATE && sokoban.isMarkAt(at) ? Tile.CRATE_ON_MARK.symbol() : tileAt.symbol();
System.out.print(symbolToPrint);
}
System.out.println();
}
}
//
}
15 . G (Good) , , . , .
. :
- , . , .
, "walkable" Tile:
public enum Tile {
WALL('#', false),
GRASS('.', true),
CRATE('O', false),
MARK('X', true),
CRATE_ON_MARK('G', false),
PLAYER('@', true);
private final char symbol;
private final boolean walkable;
Tile(final char symbol, final boolean walkable) {
this.symbol = symbol;
this.walkable = walkable;
}
public boolean isWalkable() {
return walkable;
}
}
, :
public Sokoban {
// Sokoban
public Tile tile(final Point at) {
if (player.equals(at))
return Tile.PLAYER;
//
if (crates.contains(at))
return Tile.CRATE;
//
return field.tileAt(at);
}
public boolean isWalkableTileAt(final Point at) {
return tile(at).isWalkable();
}
// Sokoban
}
, , , . :
public class Main {
public static void main(String[] args) {
final Sokoban sokoban = CharSokobanFactory.fromFile(args[0]).make();
final View view = new ConsoleView(sokoban);
final Controller game = new ConsoleController(sokoban, view);
//
game.run();
}
}
:
############
#.XO.......#
#.XO......@#
#.XO.......#
############
, :
, , , . . , — .
? . . , , :
, , . . , , , . . ,
, :
, . , (1:1), .
. — .
. . , . , , . , — Breadth-First Search (BFS).
BFS
"" — (Depth-First Search) — BFS , . , . BFS (FIFO), DFS (LIFO), . BFS:
BFS(G, s)
1. , :
* v.p = null //
* v.marked = false //
2. Q
3. Q.enqueue(s)
4. Q :
4.1 v = q.poll()
4.2 v.marked = true
4.3 v ,
4.4 v , child:
4.4.1 child.marked, :
4.4.1.2 child.p = v
4.4.1.3. q.enqueue(child)
v.p
v
. v.marked
, v
. v
"" v -> v.p -> v.p.p -> ... -> s
-. .
. .
. , , . . , :
, :
public class BFSSolver {
//
protected List<Pair<Sokoban, List<Direction>>> deriveChildren(final Sokoban parent) {
final Map<Point, Point> walkablePoints = shortestPathsFromPlayer(parent);
final List<Pair<Sokoban, List<Direction>>> result = new LinkedList<>();
for (final Point crate : parent.getCrates()) {
final Point[][] pairs = new Point[][]{{crate.left(), crate.right()}, {crate.right(), crate.left()},
{crate.up(), crate.down()}, {crate.down(), crate.up()}};
for (Point[] pair : pairs) {
final Point playerWillStand = pair[0];
final Point crateWillGo = pair[1];
if (canMoveCrate(parent, playerWillStand, crateWillGo, walkablePoints) && !isDeadPosition(parent, crateWillGo)) {
final LinkedList<Direction> pathToChild = unwindWalk(walkablePoints, playerWillStand);
pathToChild.add(crate.derive(crateWillGo));
final Sokoban child = parent.derive(crate, crateWillGo);
result.add(Pair.pair(child, pathToChild));
}
}
}
return result;
}
//
}
shortestPathsFromPlayer
parent
. walkablePoints
v
v.p
. isDeadPosition
. derive
Sokoban
"" :
public Sokoban derive(final Point crateToRemove, final Point crateToInsert)
{
final Set<Point> childConfiguration = new HashSet<>(crates);
childConfiguration.remove(crateToRemove);
childConfiguration.add(crateToInsert);
return new Sokoban(this.field, childConfiguration, Collections.unmodifiableSet(this.marks), crateToRemove);
}
, "" . , Point
(immutable). "", , BFS, . v.p
v.marked
.
:
public class BFSSolver {
//
public List<Move> solve(final Sokoban start) {
final Map<Sokoban, Pair<Sokoban, List<Direction>>> childToParentAndDirection = new HashMap<>();
final Set<Sokoban> visited = new HashSet<>();
final Queue<Sokoban> toVisit = new LinkedList<>();
toVisit.add(start);
boolean found = false;
Sokoban parent = null;
while (!toVisit.isEmpty()) {
parent = toVisit.remove();
if (parent.solved()) {
found = true;
break;
}
visited.add(parent);
for (final Pair<Sokoban, List<Direction>> pair : deriveChildren(parent)) {
final Sokoban child = pair.first;
final List<Direction> walkFromParentToChild = pair.second;
if (!visited.contains(child)) {
childToParentAndDirection.put(child, Pair.pair(parent, walkFromParentToChild));
toVisit.add(child);
}
}
}
return found? unwind(parent, childToParentAndDirection) : new LinkedList<>();
}
//
}
, , BFS , . , , , . , , . ,
"" , . , . .
* (A star), . * .. h
. h
. — , h
.
"" . . AStarSolver
. , .
新しいAIアルゴリズムを開始するためにAIController
、コンソールからコマンドを読み取らないが、指定されたアルゴリズムでレベルを解決し、タイマーで解決策を失う新しいコントローラーを作成しました。で1行だけ変更する必要がありますmain
。アーキテクチャへの投資は成果を上げています。
public class Main {
public static void main(String[] args) {
final Sokoban sokoban = CharSokobanFactory.fromFile(args[0]).make();
final View view = new ConsoleView(sokoban);
final Solver solver = new AStarSolver();
final int sleepBetweenMovesMillis = 300;
final Controller game = new AIController(sokoban, view, sleepBetweenMovesMillis, solver);
//
game.run();
}
}
結論
Sokobanおもちゃの独自の実装を作成し、Abstract Factory、Factory Method、およびStrategyの設計パターンを実際に適用し、SOLIDのS、O、およびDの原則を考慮し、BFSおよびA *アルゴリズムを実装しました。
コードと記事自体の両方についてコメントをいただければ幸いです。またね!
私は電報を送っています:@outofbound