Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
tigerwingxys committed Feb 14, 2020
0 parents commit d030786
Showing 21 changed files with 2,918 additions and 0 deletions.
12 changes: 12 additions & 0 deletions signpost.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="gjdb" level="project" />
</component>
</module>
15 changes: 15 additions & 0 deletions src/signpost/About.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<html>

<body style="font-family: Courier; background-color: #EFEFEF; color: black; font-size: 14px; font-weight: bold;">

<p>
Signpost CS61B, Version 1.0.
</p>

<p>
This is a reimplementation of the Signpost puzzle from Simon Tatham's Portable
Puzzle Collection.
</p>

</body>
</html>
343 changes: 343 additions & 0 deletions src/signpost/BoardWidget.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
package signpost;

import ucb.gui2.Pad;

import java.util.concurrent.ArrayBlockingQueue;

import java.awt.Font;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import static java.awt.RenderingHints.*;

import static signpost.Place.pl;
import signpost.Model.Sq;

/** A widget that displays a Signpost puzzle.
* @author
*/
class BoardWidget extends Pad {

/* Parameters controlling sizes, speeds, colors, and fonts. */

/** Pause in milliseconds before each turn of arrows to signal solved
* puzzle. */
static final long ARROW_BUMP_INTERVAL = 120;

/** Colors of squares, arrows, and grid lines. */
static final Color
BACKGROUND_COLOR = new Color(220, 220, 220),
GRID_LINE_COLOR = Color.black,
ARROW_COLOR = Color.black,
NUMBERED_SQUARE_COLOR = Color.white,
CONNECTED_COLOR = new Color(180, 180, 180),
NUM_COLOR = Color.black,
FIXED_NUM_COLOR = Color.blue;

/** Basic cell background RGB color values (from Simon Tatham). */
static final int[] BG_BASE_RGB = {
0xffffff,
0xffa07a,
0x98fb98,
0x7fffd4,
0x9370db,
0xffa500,
0x87cefa,
0xffff00,
};

/** Background colors used by runs of unnumbered squares. */
static final Color[] BG_COLORS = new Color[BG_BASE_RGB.length * 3];

static {
int p = BG_BASE_RGB.length;
for (int i = 0; i < BG_COLORS.length; i += 1) {
if (i < p) {
BG_COLORS[i] = new Color(BG_BASE_RGB[i]);
} else {
BG_COLORS[i] =
new Color((BG_COLORS[i - p].getRGB()
+ BG_COLORS[i - p + 1].getRGB()) / 2);
}
}
}

/** Bar width separating squares and other dimensions (pixels). */
static final int
CELL_SIDE = 50,
GRID_LINE_WIDTH = 1,
PADDING = CELL_SIDE / 2,
TEXT_OFFSET = 4,
OFFSET = 2,
DOT_SIZE = 7;

/** Separation between cell centers and half separation between cell
* centers. */
static final int
CELL_SEP = CELL_SIDE + GRID_LINE_WIDTH,
HALF_SEP = CELL_SEP / 2;

/** Strokes for ordinary grid lines and those that are parts of
* boundaries. */
static final BasicStroke
GRIDLINE_STROKE = new BasicStroke(GRID_LINE_WIDTH);

/** Font for square numbers. */
static final Font NUM_FONT = new Font("Dejavu Serif", Font.BOLD, 18);

/** Font for square group-sequence numbers (e.g., "a+1"). */
static final Font GROUP_TEXT_FONT = new Font("Dejavu Serif", Font.BOLD, 12);

/** Font for dots. */
static final Font DOT_FONT = new Font("SansSerif", Font.BOLD, 36);

/** Arrow vertex coordinates: x in first row, y in second. */
static final int [][] ARROW = {
{ 0, 7, 7, 14, 14, 21, 10, },
{ 10, 10, 0, 0, 10, 10, 21, }
};

/** Star vertex coordinates. */
static final int[][] STAR = {
{ 22, 15, 18, 11, 4, 7, 0, 8, 11, 14 },
{ 8, 13, 21, 16, 21, 13, 8, 8, 0, 8 }
};

/** Amount of rotation between arrow positions. */
static final double PI_4 = 0.25 * Math.PI;

/** A graphical representation of a Signpost board that sends commands
* derived from mouse clicks to COMMANDS. */
BoardWidget(ArrayBlockingQueue<String> commands) {
_commands = commands;
setMouseHandler("press", this::mousePressed);
setMouseHandler("release", this::mouseReleased);
}

/** Set the size of the board to WIDTH x HEIGHT. */
public void setSize(int width, int height) {
synchronized (me) {
_width = width; _height = height;
_boardWidth = width * CELL_SEP + 3 * GRID_LINE_WIDTH;
_boardHeight = height * CELL_SEP + 3 * GRID_LINE_WIDTH;
setPreferredSize(_boardWidth, _boardHeight);
}
repaint();
}

/** Draw the grid lines on G. */
private void drawGrid(Graphics2D g) {
g.setColor(GRID_LINE_COLOR);
g.setStroke(GRIDLINE_STROKE);
for (int k = 0; k <= _width; k += 1) {
g.drawLine(cx(k), cy(0), cx(k), cy(_height));
}
for (int k = 0; k <= _height; k += 1) {
g.drawLine(cx(0), cy(k), cx(_width), cy(k));
}
}

/** Return the appropriate color for arrow in SQ. */
private Color arrowColor(Sq sq) {
return sq.successor() == null ? ARROW_COLOR : CONNECTED_COLOR;
}

/** Return the appropriate color for numeral in SQ. */
private Color numberColor(Sq sq) {
return sq.hasFixedNum() ? FIXED_NUM_COLOR
: sq.successor() == null || sq.predecessor() == null ? NUM_COLOR
: CONNECTED_COLOR;
}

/** Draw star in SQ on G. */
private void drawStar(Graphics2D g, Sq sq) {
g.setColor(arrowColor(sq));
int px = cx(sq.x), py = cy(sq.y);
int[] x = new int[STAR[0].length], y = new int[STAR[0].length];
for (int i = 0; i < x.length; i += 1) {
x[i] = px + STAR[0][i] + CELL_SIDE / 2 + 2;
y[i] = py + STAR[1][i] - CELL_SIDE / 2 - 2;
}
g.fillPolygon(x, y, x.length);
}

/** Draw arrow in SQ on G. */
private void drawArrow(Graphics2D g, Sq sq) {
if (sq.direction() == 0) {
drawStar(g, sq);
return;
}
g.setColor(arrowColor(sq));
int px = cx(sq.x), py = cy(sq.y);
int[] x = new int[ARROW[0].length], y = new int[ARROW[0].length];
for (int i = 0; i < x.length; i += 1) {
x[i] = px + CELL_SIDE / 2 + 2 + ARROW[0][i];
y[i] = py - CELL_SIDE / 2 + 2 + ARROW[1][i];
}
AffineTransform init = g.getTransform();
int dir = (sq.direction() + _dirBump - 1) % 8 + 1;
g.rotate((dir - 4) * PI_4,
px + 3 * CELL_SIDE / 4, py - CELL_SIDE / 4);
g.fillPolygon(x, y, x.length);
g.setTransform(init);
}

/** Draw SQ on G. */
private void drawSquare(Graphics2D g, Sq sq) {
int px = cx(sq.x), py = cy(sq.y);
if (sq.group() >= 0) {
g.setColor(groupColor(sq.group()));
g.fillRect(px + GRID_LINE_WIDTH, py + GRID_LINE_WIDTH - CELL_SIDE,
CELL_SIDE - GRID_LINE_WIDTH,
CELL_SIDE - GRID_LINE_WIDTH);
}
drawArrow(g, sq);
if (sq.predecessor() == null && sq.sequenceNum() != 1) {
g.setColor(ARROW_COLOR);
g.fillOval(px + 3 * TEXT_OFFSET, py - 4 * TEXT_OFFSET,
DOT_SIZE, DOT_SIZE);
}
if (sq.sequenceNum() != 0) {
g.setColor(numberColor(sq));
g.setFont(NUM_FONT);
} else if (sq.group() > 0) {
g.setColor(NUM_COLOR);
g.setFont(GROUP_TEXT_FONT);
} else {
return;
}
g.drawString(sq.seqText(),
px + TEXT_OFFSET, py - CELL_SIDE / 2 - 2 * TEXT_OFFSET);
}

/** Give a visual signal that the puzzle is solved. */
private void signalSolved() {
while (true) {
try {
Thread.sleep(ARROW_BUMP_INTERVAL);
} catch (InterruptedException excp) {
/* Ignore InterruptedException. */
}
synchronized (this) {
_dirBump = (_dirBump + 1) % 8;
repaint();
if (_dirBump == 0) {
break;
}
}
}
}

@Override
public synchronized void paintComponent(Graphics2D g) {
g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, _boardWidth, _boardHeight);

drawGrid(g);
int n = 2;

for (Sq sq : _model) {
drawSquare(g, sq);
}
}

/** Handle mouse pressed event E, recording the starting square of a
* connection, if appropriate. */
private synchronized void mousePressed(String unused, MouseEvent e) {
int x = x(e), y = y(e);
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
if (_model.isCell(x, y)) {
_connStart = pl(x, y);
} else {
_connStart = null;
}
}

/** Handle mouse released event E, reporting a connection,
* if appropriate. */
private synchronized void mouseReleased(String unused, MouseEvent e) {
int x = x(e), y = y(e);
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
Place endPlace;
if (_connStart != null) {
if (_model.isCell(x, y)) {
Sq sq0 = _model.get(_connStart), sq1 = _model.get(x, y);
_commands.offer(String.format("CONN %d %d %d %d",
sq0.x, sq0.y, x, y));
} else {
_commands.offer(String.format("BRK %d %d",
_connStart.x, _connStart.y));
}
}
_connStart = null;
}

/** Return the column index of the square on which EVENT occurred. */
private int x(MouseEvent event) {
return (int) Math.floorDiv(event.getX() - OFFSET, CELL_SEP);
}

/** Return the row index of the square on which EVENT occurred. */
private int y(MouseEvent event) {
return _height - 1
- (int) Math.floorDiv(event.getY() - OFFSET, CELL_SEP);
}

/** Return the color associated with group N. */
private Color groupColor(int n) {
if (n == 0) {
return NUMBERED_SQUARE_COLOR;
} else {
return BG_COLORS[(n - 1) % (BG_COLORS.length - 1) + 1];
}
}

/** Revise the displayed board according to MODEL. */
void update(Model model) {
synchronized (this) {
_model = new Model(model);
_model = model; // FIXME: Remove this line.
_dirBump = 0;
}

repaint();
if (_model.solved()) {
signalSolved();
}
}

/** Return pixel coordinates of vertical board coordinate Y relative
* to window. */
private int cy(int y) {
return OFFSET + (_height - y) * CELL_SEP;
}

/** Return pixel coordinates of horizontal board coordinate X relative
* to window. */
private int cx(int x) {
return OFFSET + x * CELL_SEP;
}

/** Number of height and of columns. */
private int _height, _width;

/** Queue on which to post commands (from mouse clicks). */
private ArrayBlockingQueue<String> _commands;

/** Current model being displayed. */
private Model _model;
/** Length (in pixels) of the side of the board. */
private int _boardWidth, _boardHeight;
/** Place where mouse action started. */
private Place _connStart;
/** Amount to add to direction value for each displayed arrow (used for
* special effect signaling completion. */
private int _dirBump;
}
29 changes: 29 additions & 0 deletions src/signpost/CommandSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package signpost;

/** Describes a source of input commands. The possible text commands are as
* follows (parts of a command are separated by whitespace):
* - TYPE w h [FREE]: Replace the current board with one that is w cells
* wide and h cells high. Then start a new puzzle, allowing
* free ends iff FREE is present. Requires that w, h >= 3 and
* that they are properly formed numerals.
* - NEW: Start a new puzzle with current parameters.
* - RESTART: Clear all work on the current puzzle, returning to its initial
* state.
* - CONN X0 Y0 X1 Y1:
* Connect square (X0, Y0) to (X1, Y1).
* - BRK X0 Y0:
* Remove any connections to and from (X0, Y0).
* - UNDO: Go back one move.
* - REDO: Go forward one previously undone move.
* - SEED s: Set a new random seed.
* - SOLVE: Show sequence numbers of a solution.
* - QUIT: Exit the program.
* @author P. N. Hilfinger
*/
interface CommandSource {

/** Returns one command string, trimmed of preceding and following
* whitespace and converted to upper case. */
String getCommand();

}
254 changes: 254 additions & 0 deletions src/signpost/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package signpost;

import java.util.ArrayList;

import static signpost.Utils.*;
import static signpost.Place.*;
import signpost.Model.Sq;

/** The input/output and GUI controller for play of a Signpost puzzle.
* @author P. N. Hilfinger. */
public class Controller {

/** The default number of squares on a side of the board. */
static final int DEFAULT_SIZE = 4;

/** Controller for a game represented by MODEL, using COMMANDS as the
* the source of commands, and PUZZLES to supply puzzles. If LOGGING,
* prints commands received on standard output. If TESTING, prints
* the board when possibly changed. If VIEW is non-null, update it
* at appropriate points when the model changes. */
public Controller(View view,
CommandSource commands, PuzzleSource puzzles,
boolean logging, boolean testing) {
_view = view;
_commands = commands;
_puzzles = puzzles;
_logging = logging;
_testing = testing;
_solving = true;
_width = _height = DEFAULT_SIZE;
}

/** Return true iff we have not received a Quit command. */
boolean solving() {
return _solving;
}

/** Clear the board and solve one puzzle, until receiving a quit,
* new-game, or board-type change request. Update the viewer with
* each visible modification to the model. */
void solvePuzzle() {
_model = _puzzles.getPuzzle(_width, _height, _allowFreeEnds);
initUndo();
logPuzzle();
logBoard();
while (_solving) {
if (_view != null) {
_view.update(_model);
}

String cmnd = _commands.getCommand();
if (_logging) {
System.out.println(cmnd);
}
String[] parts = cmnd.split("\\s+");
switch (parts[0]) {
case "QUIT": case "Q":
_solving = false;
return;
case "NEW":
return;
case "TYPE":
setType(toInt(parts[1]), toInt(parts[2]),
parts.length > 3 && parts[3].equals("FREE"));
return;
case "SEED":
_puzzles.setSeed(toLong(parts[1]));
break;
case "CONN": case "C":
connect(toInt(parts[1]), toInt(parts[2]),
toInt(parts[3]), toInt(parts[4]));
break;
case "BRK": case "B":
disconnect(toInt(parts[1]), toInt(parts[2]));
break;
case "RESTART":
restart();
break;
case "UNDO": case "U":
undo();
break;
case "REDO": case "R":
redo();
break;
case "SOLVE":
solve();
break;
case "":
break;
default:
System.err.printf("Bad command: '%s'%n", cmnd);
break;
}
}
}

/** Connect (X0, Y0) to (X1, Y1). Has no effect if (X0, Y0) is connected
* already, something is already connected to (X1, Y1), or the connection
* is not allowed. */
private void connect(int x0, int y0, int x1, int y1) {
Sq sq0 = _model.get(x0, y0), sq1 = _model.get(x1, y1);
if (sq0.connectable(sq1)) {
sq0.connect(sq1);
_model.autoconnect();
saveForUndo();
}
logBoard();
}

/** Disconnect (X, Y) from its successor and predecessor, if connected.
* Otherwise has no effect. */
private void disconnect(int x, int y) {
Sq sq = _model.get(x, y),
next = sq.successor(),
prev = sq.predecessor();
int unconnected0 = _model.unconnected();
if (next != null) {
sq.disconnect();
}
if (prev != null) {
prev.disconnect();
}
_model.autoconnect();
if (unconnected0 != _model.unconnected()) {
saveForUndo();
}
logBoard();
}

/** Restart current puzzle. */
private void restart() {
_model.restart();
_model.autoconnect();
initUndo();
logBoard();
}

/** Set current puzzle bpard to show a solution. */
private void solve() {
_model.solve();
logBoard();
}

/** Set current puzzle type to WIDTH x HEIGHT and with free ends iff FREE.
*/
private void setType(int width, int height, boolean free) {
_width = width;
_height = height;
_allowFreeEnds = free;
}

/** Back up one move, if possible. Does nothing otherwise. */
private void undo() {
if (_undoIndex > 0) {
_undoIndex -= 1;
_model = new Model(_undoStack.get(_undoIndex));
}
logBoard();
}

/** Redo one move, if possible. Does nothing otherwise. */
private void redo() {
if (_undoIndex + 1 < _undoStack.size()) {
_undoIndex += 1;
_model = new Model(_undoStack.get(_undoIndex));
}
logBoard();
}

/** Initialize _undoStack to contain just current model. */
private void initUndo() {
_undoStack.clear();
_undoStack.add(new Model(_model));
_undoIndex = 0;
}

/** Save current board position for possible undo. */
private void saveForUndo() {
_undoStack.subList(_undoIndex + 1, _undoStack.size()).clear();
_undoStack.add(new Model(_model));
_undoIndex += 1;
}

/** If testing, print the contents of the board. */
private void logBoard() {
if (_testing) {
System.out.printf("B[ %dx%d%s%n%s]%n",
_model.width(), _model.height(),
_model.solved() ? " (SOLVED)" : "", _model);
}
}

/** If logging, print a representation of the puzzle suitable for input
* from a TestSource. */
private void logPuzzle() {
if (_logging) {
System.out.printf("PUZZLE%n%d %d%n",
_model.width(), _model.height());
int[][] soln = _model.solution();
for (int y = _model.height() - 1; y >= 0; y -= 1) {
for (int x = 0; x < _model.width(); x += 1) {
System.out.printf("%d ", soln[x][y]);
}
System.out.println();
}
for (int y = _model.height() - 1; y >= 0; y -= 1) {
for (int x = 0; x < _model.width(); x += 1) {
Sq sq = _model.get(x, y);
if (sq.hasFixedNum()) {
System.out.printf("%d ", soln[x][y]);
}
}
}
System.out.printf("%nENDPUZZLE%n");
}
}

/** The board. */
private Model _model;

/** The sequence of board states, used to implement undo/redo operations.
* Item #_undoIndex is always a copy of the current model. */
private ArrayList<Model> _undoStack = new ArrayList<>();

/** Current position in the undoStack of a copy of the current model.
* Lower indices are previous models, accessible by undoing, and
* higher indices are models accessible by redoing. */
private int _undoIndex;

/** Our view of _model. */
private View _view;

/** Puzzle dimensions. */
private int _width, _height;

/** Input source from standard input. */
private CommandSource _commands;

/** Input source from standard input. */
private PuzzleSource _puzzles;

/** True while user is still working on a puzzle. */
private boolean _solving;

/** True iff we are logging commands on standard output. */
private boolean _logging;

/** True iff we are testing the program and printing board contents. */
private boolean _testing;

/** True iff we allow generated puzzles to have free ends. */
private boolean _allowFreeEnds;

}
210 changes: 210 additions & 0 deletions src/signpost/GUI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package signpost;

import ucb.gui2.TopLevel;
import ucb.gui2.LayoutSpec;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import java.util.concurrent.ArrayBlockingQueue;

import java.awt.Dimension;
import java.io.InputStream;
import java.io.IOException;
import java.io.StringWriter;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JScrollPane;

/** The GUI controller for a Signpost board and buttons.
* @author P. N. Hilfinger
*/
class GUI extends TopLevel implements View {

/** Minimum size of board in pixels. */
private static final int MIN_SIZE = 500;

/** Size of pane used to contain help text. */
static final Dimension TEXT_BOX_SIZE = new Dimension(500, 700);

/** Resource name of "About" message. */
static final String ABOUT_TEXT = "signpost/About.html";

/** Resource name of Signpost help text. */
static final String HELP_TEXT = "signpost/Help.html";

/** A new window with given TITLE providing a view of MODEL. */
GUI(String title) {
super(title, true);
addMenuButton("Game->New", this::newGame);
addMenuButton("Game->Restart", this::restartGame);
addMenuButton("Game->Solve", this::showSolution);
addSeparator("Game");
addMenuButton("Game->Undo", this::undo);
addMenuButton("Game->Redo", this::redo);
addSeparator("Game");
addMenuButton("Type->Set Size", (s) -> newSize(false));
addMenuButton("Type->Set Size (free ends)", (s) -> newSize(true));
addMenuButton("Type->Seed", this::newSeed);
addMenuButton("Game->Quit", this::quit);
addMenuButton("Help->About", (s) -> displayText("About", ABOUT_TEXT));
addMenuButton("Help->Signpost", (s) -> displayText("Signpost Help",
HELP_TEXT));
}

/** Response to "Quit" button click. */
private void quit(String dummy) {
_pendingCommands.offer("QUIT");
}

/** Response to "New Game" button click. */
private void newGame(String dummy) {
_pendingCommands.offer("NEW");
}

/** Response to "Undo" button click. */
private void undo(String dummy) {
_pendingCommands.offer("UNDO");
}

/** Response to "Redo" button click. */
private void redo(String dummy) {
_pendingCommands.offer("REDO");
}

/** Response to "New Game" button click. */
private void restartGame(String dummy) {
_pendingCommands.offer("RESTART");
}

/** Response to "Solve" button click. */
private void showSolution(String dummy) {
_pendingCommands.offer("SOLVE");
}

/** Display text in resource named TEXTRESOURCE in a new window titled
* TITLE. */
private void displayText(String title, String textResource) {
/* Implementation note: It would have been more convenient to avoid
* having to read the resource and simply use dispPane.setPage on the
* resource's URL. However, we wanted to use this application with
* a nonstandard ClassLoader, and arranging for straight Java to
* understand non-standard URLS that access such a ClassLoader turns
* out to be a bit more trouble than it's worth. */
JFrame frame = new JFrame(title);
JEditorPane dispPane = new JEditorPane();
dispPane.setEditable(false);
dispPane.setContentType("text/html");
InputStream resource =
GUI.class.getClassLoader().getResourceAsStream(textResource);
StringWriter text = new StringWriter();
try {
while (true) {
int c = resource.read();
if (c < 0) {
dispPane.setText(text.toString());
break;
}
text.write(c);
}
} catch (IOException e) {
return;
}
JScrollPane scroller = new JScrollPane(dispPane);
scroller.setVerticalScrollBarPolicy(scroller.VERTICAL_SCROLLBAR_ALWAYS);
scroller.setPreferredSize(TEXT_BOX_SIZE);
frame.add(scroller);
frame.pack();
frame.setVisible(true);
}

/** Pattern describing the 'size' command's arguments. */
private static final Pattern SIZE_PATN =
Pattern.compile("\\s*(\\d{1,2})\\s*[xX]\\s*(\\d{1,2})\\s*$");

/** Pattern describing the 'seed' command's arguments. */
private static final Pattern SEED_PATN =
Pattern.compile("\\s*(-?\\d{1,18})\\s*$");

/** Response to "Set size" button clicks. FREE indicates that free
* ends are allowed. */
private void newSize(boolean free) {
String response =
getTextInput("Enter new size (<width>x<height>).",
"New size", "plain",
String.format("%dx%d", _width, _height));
if (response != null) {
Matcher mat = SIZE_PATN.matcher(response);
if (mat.matches()) {
int width = Integer.parseInt(mat.group(1)),
height = Integer.parseInt(mat.group(2));
if (width >= 1 && height >= 1) {
_pendingCommands.offer(String.format("TYPE %d %d%s",
width, height,
free ? " free" : ""));
}
} else {
showMessage("Bad board size chosen.", "Error", "error");
}
}
}

/** Response to "Seed" button click. */
private void newSeed(String dummy) {
String response =
getTextInput("Enter new random seed.", "New seed", "plain", "");
if (response != null) {
Matcher mat = SEED_PATN.matcher(response);
if (mat.matches()) {
_pendingCommands.offer(String.format("SEED %s", mat.group(1)));
} else {
showMessage("Enter an integral seed value.", "Error", "error");
}
}
}

/** Return the next command from our widget, waiting for it as necessary.
* Press/release pairs are reported as "CONN" or "BRK" commands.
* Menu-button clicks result in the messages "QUIT", "NEW", "UNDO",
* "REDO", "RESTART", "SEED", "SOLVE", or "TYPE". */
String readCommand() {
try {
return _pendingCommands.take();
} catch (InterruptedException excp) {
throw new Error("unexpected interrupt");
}
}

@Override
public void update(Model model) {
if (_widget == null) {
_widget = new BoardWidget(_pendingCommands);
_widget.setSize(model.width(), model.height());
_width = model.width();
_height = model.height();
int pad = _widget.PADDING;
add(_widget,
new LayoutSpec("y", 0,
"ileft", pad, "iright", pad,
"itop", pad, "ibottom", pad,
"height", "REMAINDER",
"width", "REMAINDER"));
} else if (model.height() != _height || model.width() != _width) {
_width = model.width();
_height = model.height();
_widget.setSize(_width, _height);
}
display(true);
_widget.update(model);
}

/** The board widget. */
private BoardWidget _widget;
/** The current size of the model. */
private int _width, _height;

/** Queue of pending key presses. */
private ArrayBlockingQueue<String> _pendingCommands =
new ArrayBlockingQueue<>(5);

}
21 changes: 21 additions & 0 deletions src/signpost/GUISource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package signpost;

/** A type of InputSource that receives commands from a GUI.
* @author P. N. Hilfinger
*/
class GUISource implements CommandSource {

/** Provides input from SOURCE. */
GUISource(GUI source) {
_source = source;
}

@Override
public String getCommand() {
return _source.readCommand().toUpperCase();
}

/** Input source. */
private GUI _source;

}
31 changes: 31 additions & 0 deletions src/signpost/Help.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<html>

<body style="font-family: Courier; background-color: #EFEFEF; color: black; font-size: 14px; font-weight: bold;">

<h1> Signpost Puzzle </h1>

<p> The goal of this puzzle is to link the board squares by
queen moves so that each square appears once in the sequence of linked
squares and the positions of all the initially numbered squares are equal
to their positions in the sequence. The term "queen move" here refers to
the queen in chess, which can move any number of squares horizontally,
vertically, or diagonally.
</p>

<p> To link two squares, press and hold mouse button 1 on the first square,
drag to the second square, and release. To break a link between two squares,
press on the first square, move the mouse off the board, and release.
</p>

<p>
By default, the upper-left square is initially numbered 1 and the lower-right
square contains the last number (the total number of squares on the board).
The "Type" menu allows you to change the dimensions of the board and to change
the default so that the first and last squares in the sequence are "free ends",
which can appear anywhere on the board.
</p>

</body>
</html>


101 changes: 101 additions & 0 deletions src/signpost/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package signpost;

import java.util.Scanner;
import java.io.FileInputStream;
import java.io.IOException;

import ucb.util.CommandArgs;

/** The main class for the Signpost puzzle.
* @author P. N. Hilfinger
*/
public class Main {

/** The main program. ARGS may contain the options --seed=NUM,
* (random seed); --log (record commands, clicks);
* --testing (take puzzles and commands from standard input);
* --setup (take puzzles from standard input and commands from GUI);
* and --no-display. */
public static void main(String... args) {

CommandArgs options =
new CommandArgs("--seed=(\\d+) --log --setup --testing "
+ "--no-display --=(.*)",
args);
if (!options.ok()) {
System.err.println("Usage: java signpost.Main [ --seed=NUM ] "
+ "[ --setup ] "
+ "[ --log ] [ --testing ] [ --no-display ]"
+ " [ INPUT ]");
System.exit(1);
}

if (options.contains("--")) {
String inpFile = options.getFirst("--");
try {
System.setIn(new FileInputStream(inpFile));
} catch (IOException excp) {
System.err.printf("Could not open %s%n", inpFile);
System.exit(1);
}
}

Controller puzzler = getController(options);

try {
while (puzzler.solving()) {
puzzler.solvePuzzle();
}
} catch (IllegalStateException excp) {
System.err.printf("Internal error: %s%n", excp.getMessage());
System.exit(1);
}

if (options.contains("--no-display") || options.contains("--testing")) {
System.exit(0);
}

}

/** Return an appropriate Controller as indicated by OPTIONS. */
private static Controller getController(CommandArgs options) {
GUI gui;
CommandSource cmds;
PuzzleSource puzzles;

if (options.contains("--no-display")) {
gui = null;
} else {
gui = new GUI("Signpost 61B");
}

if (gui == null && !options.contains("--testing")) {
System.err.println("Error: no input source.");
System.exit(1);
return null;
} else if (options.contains("--testing")) {
TestSource src = new TestSource(new Scanner(System.in));
cmds = src;
puzzles = src;
} else if (options.contains("--setup")) {
cmds = new GUISource(gui);
puzzles = new TestSource(new Scanner(System.in));
} else {
cmds = new GUISource(gui);
long seed;
if (options.contains("--seed")) {
seed = options.getLong("--seed");
} else {
seed = (long) (Math.random() * SEED_RANGE);
}
puzzles = new PuzzleGenerator(seed);
}

return new Controller(gui, cmds, puzzles,
options.contains("--log"),
options.contains("--testing"));
}

/** Maximum default seed. */
private static final double SEED_RANGE = 1e12;
}
79 changes: 79 additions & 0 deletions src/signpost/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# This a Makefile, an input file for the GNU 'make' program. For you
# command-line and Emacs enthusiasts, this makes it possible to build
# this program with a single command:
# make
# You can also clean up junk files and .class files with
# make clean
# To run style61b (our style enforcer) over your source files, type
# make style
# Finally, you can run any tests you'd care to with
# make check

SHELL = bash

STYLEPROG = style61b

PYTHON = python3

PACKAGE = signpost

# A non-standard classpath that works on Linux, Mac, and Windows.
# To Unix-like systems (Linux and Mac) it has the form
# <valid classpath>:<garbage classpath (ignored)>
# while to Windows systems it looks like
# <garbage classpath (ignored)>;<valid classpath>
CPATH = "..:$(CLASSPATH):;..;$(CLASSPATH)"

# Flags to pass to Java compilations (include debugging info and report
# "unsafe" operations.)
JFLAGS = -g -Xlint:unchecked -cp $(CPATH) -d .. -encoding utf8

CLASSDEST = ..

# All .java files to be compiled.
SRCS = $(wildcard *.java)

CLASSES = $(SRCS:.java=.class)

# Tell make that these are not really files.
.PHONY: clean default compile style \
check unit integration

%.class: %.java
javac $(JFLAGS) -d "$(CLASSDEST)" $^ || { $(RM) $@; false; }

# By default, make sure all classes are present and check if any sources have
# changed since the last build.
default: compile

compile: Main.class

style:
$(STYLEPROG) $(SRCS)

Main.class: $(SRCS)
javac $(JFLAGS) -d "$(CLASSDEST)" $(SRCS) || { $(RM) $@; false; }

# Run Tests.
check:
code=0; \
"$(MAKE)" unit || code=1; \
"$(MAKE)" integration || code=1; \
exit $$code

# Run unit tests in this directory
unit: compile
cd ..; java -ea signpost.UnitTests

integration: compile
"$(MAKE)" -C ../testing PYTHON=$(PYTHON) check

unit-jar: unit-tests.jar

unit-tests.jar: compile
jar cf $@ *Tests.class


# Find and remove all *~ and *.class files.
clean:
$(RM) *.class *~ unit-tests.jar
675 changes: 675 additions & 0 deletions src/signpost/Model.java

Large diffs are not rendered by default.

341 changes: 341 additions & 0 deletions src/signpost/ModelTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
package signpost;

import static java.util.Arrays.asList;
import java.util.Collection;
import java.util.HashSet;
import java.util.HashMap;

import org.junit.Test;
import static org.junit.Assert.*;

import static signpost.Place.pl;
import signpost.Model.Sq;
import static signpost.Utils.msg;
import static signpost.Utils.tr;
import static signpost.Utils.assertSetEquals;

/** Tests of the Model class.
* @author P. N. Hilfinger
* @author Will Wang
*/
public class ModelTests {

/** Check that MODEL is a valid representation of the partial solution in
* SOLUTION, and that the fixed numbers in it are precisely FIXED.
* SOLUTION may contain 0's, indicating squares that are not yet
* sequenced. */
static void checkNumbers(int[][] solution, Model model,
Collection<Integer> fixed) {
assertEquals("Wrong model width", solution.length, model.width());
assertEquals("Wrong model height", solution[0].length, model.height());
assertEquals("Wrong model size", solution.length * solution[0].length,
model.size());
HashSet<Integer> actualFixed = new HashSet<>();
for (int x = 0; x < model.width(); x += 1) {
for (int y = 0; y < model.height(); y += 1) {
Sq sq = model.get(x, y);
assertEquals(msg("Wrong at (%d, %d)", x, y),
solution[x][y], sq.sequenceNum());
if (sq.hasFixedNum()) {
actualFixed.add(sq.sequenceNum());
}
}
}
assertEquals("Fixed positions differ", new HashSet<Integer>(fixed),
actualFixed);
}

/** Check that the arrow directions in MODEL agree with DIRS. The
* direction of the arrow at (x, y) in MODEL should be DIRS[x][y]. */
static void checkArrows(int[][] dirs, Model model) {
for (int x = 0; x < model.width(); x += 1) {
for (int y = 0; y < model.height(); y += 1) {
assertEquals(msg("Arrows differ at (%d, %d)", x, y),
dirs[x][y], model.get(x, y).direction());
}
}
}

/** Check that the model.Sq has the correct attributes. */
static void checkSquare(Sq sq, Sq head, Sq predecessor, Sq successor,
int seqNum, int group) {
assertEquals("Sq has incorrect group head.", head, sq.head());
assertEquals("Sq has incorrect predecessor.",
predecessor, sq.predecessor());
assertEquals("Sq has incorrect successor.", successor, sq.successor());
assertEquals("Sq has incorrect sequence number.",
seqNum, sq.sequenceNum());
assertEquals("Sq has incorrect group number.", group, sq.group());
}

@Test
public void initTest1() {
Model model = new Model(tr(SOLN1));
checkNumbers(tr(BOARD1), model, asList(1, 16));
}

@Test
public void initTest2() {
Model model = new Model(SOLN2);
checkNumbers(BOARD2, model, asList(1, 20));
}

@Test
public void initTest3() {
int[][] soln = tr(SOLN2);
Model model = new Model(soln);
model.solve();
for (int x = 0; x < model.width(); x += 1) {
YLoop:
for (int y = 0; y < model.height(); y += 1) {
for (Sq sq : model) {
if (x == sq.x && y == sq.y) {
assertEquals(msg("Wrong number at (%d, %d)", x, y),
soln[x][y], sq.sequenceNum());
continue YLoop;
}
}
fail(msg("Did not find square at (%d, %d)", x, y));
}
}
}

@Test
public void allPlacesTest() {
Model model = new Model(tr(SOLN2));
for (Sq sq : model) {
assertEquals(msg("Wrong square at (%d, %d)", sq.x, sq.y),
sq, model.get(sq.x, sq.y));
assertEquals(msg("Wrong square at Place %s", sq.pl),
sq, model.get(sq.pl));
assertEquals(msg("Wrong Place at (%d, %d)", sq.x, sq.y),
pl(sq.x, sq.y), sq.pl);
}
}

@Test
public void arrowTest1() {
Model model = new Model(tr(SOLN1));
checkArrows(tr(ARROWS1), model);
}

@Test
public void copyTest() {
Model model1 = new Model(tr(SOLN1));
Model model2 = new Model(model1);
checkNumbers(tr(BOARD1), model2, asList(1, 16));
for (int x = 0; x < model1.width(); x += 1) {
for (int y = 0; y < model1.height(); y += 1) {
Sq sq1 = model1.get(x, y);
Sq sq2 = model2.get(x, y);
assertFalse("Sq should not be the same instance", sq1 == sq2);
assertTrue("Sq should be equivalent objects", sq1.equals(sq2));
}
}

HashMap<Sq, Sq> model1Sqs = new HashMap<Sq, Sq>();
HashSet<Sq> model2Sqs = new HashSet<Sq>();
for (Sq sq : model1) {
model1Sqs.put(sq, sq);
}
for (Sq sq : model2) {
assertFalse("Sq should not be the same instance",
sq == model1Sqs.get(sq));
model2Sqs.add(sq);
}
assertSetEquals("Model iterators should have equivalent Sqs",
model1Sqs.keySet(), model2Sqs);
}

@Test
public void solvedTest1() {
Model model = new Model(tr(SOLN1));
assertFalse("Model not solved yet.", model.solved());
model.solve();
assertTrue("Model should be solved.", model.solved());
checkNumbers(tr(SOLN1), model, asList(1, 16));
}

@Test
public void autoConnectTest1() {
Model model = new Model(tr(new int[][] { { 1, 2 } }));
model.autoconnect();
assertTrue("Trivial puzzle should be solved at birth.", model.solved());
}

/* In sqConnectTest and sqDisconnectTest, we disregard the solution
board passed into Model and instead instantiate our own squares.
This avoids depending on a working Model constructor.
The squares are placed on a 3x3 board shown below, where each
tuple represents (square, direction).
(s5, S ) ( ) (s3, SW)
(s6, S ) (s4, S ) ( )
(s1, NE) ( ) (s2, N )
*/

@Test
public void sqConnectTest() {
Model model = new Model(tr(SOLN1));
Sq s1 = model.new Sq(0, 0, 0, false, 1, -1);
Sq s2 = model.new Sq(2, 0, 0, false, 8, -1);
Sq s3 = model.new Sq(2, 2, 0, false, 5, -1);
Sq s4 = model.new Sq(1, 1, 4, true, 5, 0);
Sq s5 = model.new Sq(0, 2, 8, true, 4, 0);
Sq s6 = model.new Sq(0, 1, 1, true, 4, 0);

assertFalse("A square is not connectable to itself.", s1.connect(s1));
checkSquare(s1, s1, null, null, 0, -1);

assertFalse("Squares must be one queen move away and in the "
+ "correct direction.", s1.connect(s2));
checkSquare(s1, s1, null, null, 0, -1);
checkSquare(s2, s2, null, null, 0, -1);

assertTrue("These squares should be connectable.", s1.connect(s3));
checkSquare(s1, s1, null, s3, 0, 1);
checkSquare(s3, s1, s1, null, 0, 1);

assertFalse("Unnumbered squares in same group are not connectable.",
s3.connect(s1));
checkSquare(s1, s1, null, s3, 0, 1);
checkSquare(s3, s1, s1, null, 0, 1);

assertFalse("Next square cannot already have a predecessor.",
s2.connect(s3));
checkSquare(s2, s2, null, null, 0, -1);
checkSquare(s3, s1, s1, null, 0, 1);

assertFalse("Current square cannot already have a successor.",
s1.connect(s4));
checkSquare(s1, s1, null, s3, 0, 1);
checkSquare(s4, s4, null, null, 4, 0);

assertTrue("These squares should be connectable.", s3.connect(s4));
checkSquare(s3, s1, s1, s4, 3, 0);
checkSquare(s4, s1, s3, null, 4, 0);
checkSquare(s1, s1, null, s3, 2, 0);

assertFalse("Non-sequential numbered squares are not connectable.",
s5.connect(s1));
checkSquare(s1, s1, null, s3, 2, 0);
checkSquare(s5, s5, null, null, 8, 0);

assertTrue("These squares should be connectable.", s6.connect(s1));
checkSquare(s1, s6, s6, s3, 2, 0);
checkSquare(s6, s6, null, s1, 1, 0);
}

/* We disregard the solution board passed into Model and instead
instantiate our own squares. This bypasses the dependency
on a working Model constructor. The squares are placed on
a 3x3 board shown below, where each tuple represents
(square, direction).
(s3, E ) (s4, N ) (s9, N )
(s2, N ) ( ) (s8, N )
(s1, N ) (s6, E ) (s7, N )
This test requires that you pass sqConnectTest.
*/



@Test
public void sqDisconnectTest() {
Model model = new Model(tr(SOLN1));
Sq s1 = model.new Sq(0, 0, 1, true, 8, -1);
Sq s2 = model.new Sq(0, 1, 0, false, 8, -1);
Sq s3 = model.new Sq(0, 2, 0, false, 2, -1);
Sq s4 = model.new Sq(1, 2, 0, false, 8, -1);
Sq s6 = model.new Sq(1, 0, 0, false, 2, -1);
Sq s7 = model.new Sq(2, 0, 0, false, 8, -1);
Sq s8 = model.new Sq(2, 1, 0, false, 8, -1);
Sq s9 = model.new Sq(2, 2, 9, true, 8, -1);

assertTrue("These squares should be connectable.", s1.connect(s2));
assertTrue("These squares should be connectable.", s2.connect(s3));
assertTrue("These squares should be connectable.", s3.connect(s4));
checkSquare(s1, s1, null, s2, 1, 0);
checkSquare(s2, s1, s1, s3, 2, 0);
checkSquare(s3, s1, s2, s4, 3, 0);
checkSquare(s4, s1, s3, null, 4, 0);

assertTrue("These squares should be connectable.", s8.connect(s9));
assertTrue("These squares should be connectable.", s7.connect(s8));
assertTrue("These squares should be connectable.", s6.connect(s7));
checkSquare(s6, s6, null, s7, 6, 0);
checkSquare(s7, s6, s6, s8, 7, 0);
checkSquare(s8, s6, s7, s9, 8, 0);
checkSquare(s9, s6, s8, null, 9, 0);

s2.disconnect();
checkSquare(s1, s1, null, s2, 1, 0);
checkSquare(s2, s1, s1, null, 2, 0);
checkSquare(s3, s3, null, s4, 0, 1);
checkSquare(s4, s3, s3, null, 0, 1);

s1.disconnect();
checkSquare(s1, s1, null, null, 1, 0);
checkSquare(s2, s2, null, null, 0, -1);

s4.disconnect();
checkSquare(s3, s3, null, s4, 0, 1);
checkSquare(s4, s3, s3, null, 0, 1);

s3.disconnect();
checkSquare(s3, s3, null, null, 0, -1);
checkSquare(s4, s4, null, null, 0, -1);

s7.disconnect();
checkSquare(s6, s6, null, s7, 0, 1);
checkSquare(s7, s6, s6, null, 0, 1);
checkSquare(s8, s8, null, s9, 8, 0);
checkSquare(s9, s8, s8, null, 9, 0);

s8.disconnect();
checkSquare(s8, s8, null, null, 0, -1);
checkSquare(s9, s9, null, null, 9, 0);
}

/* The following array data is written to look on the page like
* the arrangement of data on the screen, with the first row
* corresponding to the top row of the puzzle board, etc. They are
* transposed by tr into the actual data, in which the first array
* dimension indexes columns, and the second indexes rows from bottom to
* top. */

private static final int[][] SOLN1 = {
{ 1, 13, 3, 2 },
{ 12, 4, 8, 15 },
{ 5, 9, 7, 14 },
{ 11, 6, 10, 16 }
};

private static final int[][] ARROWS1 = {
{ 2, 3, 5, 6 },
{ 1, 5, 5, 4 },
{ 3, 3, 8, 8 },
{ 8, 1, 6, 0 }
};

private static final int[][] BOARD1 = {
{ 1, 0, 0, 0 },
{ 0, 0, 0, 0 },
{ 0, 0, 0, 0 },
{ 0, 0, 0, 16 } };

private static final int[][] SOLN2 = {
{ 1, 2, 17, 16, 3 },
{ 9, 7, 15, 6, 8 },
{ 12, 11, 18, 5, 4 },
{ 10, 13, 14, 19, 20 }
};

private static final int[][] BOARD2 = {
{ 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 20} };

}
132 changes: 132 additions & 0 deletions src/signpost/Place.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package signpost;

import java.util.ArrayList;
import java.util.List;

import static java.lang.Math.max;

/** An (X, Y) position on a Signpost puzzle board. We require that
* X, Y >= 0. Each Place object is unique; no other has the same x and y
* values. As a result, "==" may be used for comparisons.
* @author
*/
class Place {

/** Convenience list-of-Place class. (Defining this allows one to create
* arrays of lists without compiler warnings.) */
static class PlaceList extends ArrayList<Place> {
/** Initialize empty PlaceList. */
PlaceList() {
}

/** Initialze PlaceList from a copy of INIT. */
PlaceList(List<Place> init) {
super(init);
}
}

/** The position (X0, Y0), where X0, Y0 >= 0. */
private Place(int x0, int y0) {
x = x0; y = y0;
}

/** Return the position (X, Y). This is a factory method that
* creates a new Place only if needed by caching those that are
* created. */
static Place pl(int x, int y) {
assert x >= 0 && y >= 0;
int s = max(x, y);
if (s >= _places.length) {
Place[][] newPlaces = new Place[s + 1][s + 1];
for (int i = 0; i < _places.length; i += 1) {
System.arraycopy(_places[i], 0, newPlaces[i], 0,
_places.length);
}
_places = newPlaces;
}
if (_places[x][y] == null) {
_places[x][y] = new Place(x, y);
}
return _places[x][y];
}

/** Returns the direction from (X0, Y0) to (X1, Y1), if we are a queen
* move apart. If not, returns 0. The direction returned (if not 0)
* will be an integer 1 <= dir <= 8 corresponding to the definitions
* in Model.java */
static int dirOf(int x0, int y0, int x1, int y1) {
int dx = x1 < x0 ? -1 : x0 == x1 ? 0 : 1;
int dy = y1 < y0 ? -1 : y0 == y1 ? 0 : 1;
if (dx == 0 && dy == 0) {
return 0;
}
if (dx != 0 && dy != 0 && Math.abs(x0 - x1) != Math.abs(y0 - y1)) {
return 0;
}

return dx > 0 ? 2 - dy : dx == 0 ? 6 + 2 * dy : 6 + dy;
}

/** Returns the direction from me to PLACE, if we are a queen
* move apart. If not, returns 0. */
int dirOf(Place place) {
return dirOf(x, y, place.x, place.y);
}

/** If (x1, y1) is the adjacent square in direction DIR from me, returns
* x1 - x. */
static int dx(int dir) {
return DX[dir];
}

/** If (x1, y1) is the adjacent square in direction DIR from me, returns
* y1 - y. */
static int dy(int dir) {
return DY[dir];
}

/** Return an array, M, such that M[x][y][dir] is a list of Places that are
* one queen move away from square (x, y) in direction dir on a
* WIDTH x HEIGHT board. Additionally, M[x][y][0] is a list of all Places
* that are a queen move away from (x, y) in any direction (the union of
* the lists of queen moves in directions 1-8). */
static PlaceList[][][] successorCells(int width, int height) {
PlaceList[][][] M = new PlaceList[width][height][9];

// FIXME
return M;
}

@Override
public boolean equals(Object obj) {
if (!(obj instanceof Place)) {
return false;
}
Place other = (Place) obj;
return x == other.x && y == other.y;
}

@Override
public int hashCode() {
return (x << 16) + y;
}

@Override
public String toString() {
return String.format("(%d, %d)", x, y);
}

/** X displacement of adjacent squares, indexed by direction. */
static final int[] DX = { 0, 1, 1, 1, 0, -1, -1, -1, 0 };

/** Y displacement of adjacent squares, indexed by direction. */
static final int[] DY = { 0, 1, 0, -1, -1, -1, 0, 1, 1 };

/** Coordinates of this Place. */
protected final int x, y;

/** Places already generated. */
private static Place[][] _places = new Place[10][10];


}
48 changes: 48 additions & 0 deletions src/signpost/PlaceTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package signpost;

import org.junit.Test;
import static org.junit.Assert.*;

import java.util.Arrays;

import static signpost.Place.PlaceList;
import static signpost.Utils.*;

/** Tests of the Place class.
* @author Will Wang
*/
public class PlaceTests {

private void checkSuccessors(Place[][] expected, PlaceList[] actual) {
for (int dir = 8; dir >= 0; dir -= 1) {
assertSetEquals(msg("Mismatch sucessors at direction %d", dir),
Arrays.asList(expected[dir]), actual[dir]);
}
}

@Test
public void successorCellsTest() {
PlaceList[][][] sucessors = Place.successorCells(WIDTH, HEIGHT);
checkSuccessors(EXPECTED, sucessors[PL.x][PL.y]);
}

private static final int WIDTH = 4;
private static final int HEIGHT = 4;

private static final Place PL = Place.pl(1, 1);

private static final Place[][] EXPECTED = {
{ Place.pl(0, 0), Place.pl(0, 1), Place.pl(0, 2), Place.pl(1, 2),
Place.pl(1, 3), Place.pl(2, 2), Place.pl(3, 3), Place.pl(2, 1),
Place.pl(3, 1), Place.pl(2, 0), Place.pl(1, 0)},
{ Place.pl(2, 2), Place.pl(3, 3) },
{ Place.pl(2, 1), Place.pl(3, 1) },
{ Place.pl(2, 0) },
{ Place.pl(1, 0) },
{ Place.pl(0, 0) },
{ Place.pl(0, 1) },
{ Place.pl(0, 2) },
{ Place.pl(1, 2), Place.pl(1, 3) },
};

}
240 changes: 240 additions & 0 deletions src/signpost/PuzzleGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package signpost;

import java.util.Collections;
import java.util.Random;

import signpost.Model.Sq;
import static signpost.Place.PlaceList;
import static signpost.Utils.*;

/** A creator of random Signpost puzzles.
* @author
*/
class PuzzleGenerator implements PuzzleSource {

/** A new PuzzleGenerator whose random-number source is seeded
* with SEED. */
PuzzleGenerator(long seed) {
_random = new Random(seed);
}

@Override
public Model getPuzzle(int width, int height, boolean allowFreeEnds) {
Model model =
new Model(makePuzzleSolution(width, height, allowFreeEnds));
// FIXME: Remove the "//" on the following two lines.
// makeSolutionUnique(model);
// model.autoconnect();
return model;
}

/** Return an array representing a WIDTH x HEIGHT Signpost puzzle.
* The first array index indicates x-coordinates (column numbers) on
* the board, and the second index represents y-coordinates (row numbers).
* Its values will be the sequence numbers (1 to WIDTH x HEIGHT)
* appearing in a sequence queen moves on the resulting board.
* Unless ALLOWFREEENDS, the first and last sequence numbers will
* appear in the upper-left and lower-right corners, respectively. */
private int[][] makePuzzleSolution(int width, int height,
boolean allowFreeEnds) {
_vals = new int[width][height];
_successorCells = Place.successorCells(width, height);
int last = width * height;
int x0, y0, x1, y1;
if (allowFreeEnds) {
int r0 = _random.nextInt(last),
r1 = (r0 + 1 + _random.nextInt(last - 1)) % last;
x0 = r0 / height; y0 = r0 % height;
x1 = r1 / height; y1 = r1 % height;
} else {
x0 = 0; y0 = height - 1;
x1 = width - 1; y1 = 0;
}
_vals[x0][y0] = 1;
_vals[x1][y1] = last;
// FIXME: Remove the following return statement and uncomment the
// next three lines.
return new int[][] {
{ 14, 9, 8, 1 },
{ 15, 10, 7, 2 },
{ 13, 11, 6, 3 },
{ 16, 12, 5, 4 }
};
//boolean ok = findSolutionPathFrom(x0, y0);
//assert ok;
//return _vals;
}

/** Try to find a random path of queen moves through VALS from (X0, Y0)
* to the cell with number LAST. Assumes that
* + The dimensions of VALS conforms to those of MODEL;
* + There are cells (separated by queen moves) numbered from 1 up to
* and including the number in (X0, Y0);
* + There is a cell numbered LAST;
* + All other cells in VALS contain 0.
* Does not change the contents of any non-zero cell in VALS.
* Returns true and leaves the path that is found in VALS. Otherwise
* returns false and leaves VALS unchanged. Does not change MODEL. */
private boolean findSolutionPathFrom(int x0, int y0) {
int w = _vals.length, h = _vals[0].length;
int v;
int start = _vals[x0][y0] + 1;
PlaceList moves = _successorCells[x0][y0][0];
Collections.shuffle(moves, _random);
for (Place p : moves) {
v = _vals[p.x][p.y];
if (v == 0) {
_vals[p.x][p.y] = start;
if (findSolutionPathFrom(p.x, p.y)) {
return true;
}
_vals[p.x][p.y] = 0;
} else if (v == start && start == w * h) {
return true;
}
}
return false;
}

/** Extend unambiguous paths in MODEL (add all connections where there is
* a single possible successor or predecessor). Return true iff any change
* was made. */
static boolean extendSimple(Model model) {
boolean found;
found = false;
while (makeForwardConnections(model)
|| makeBackwardConnections(model)) {
found = true;
}
return found;
}

/** Make all unique forward connections in MODEL (those in which there is
* a single possible successor). Return true iff changes were made. */
static boolean makeForwardConnections(Model model) {
int w = model.width(), h = model.height();
boolean result;
result = false;
for (Sq sq : model) {
if (sq.successor() == null && sq.direction() != 0) {
Sq found = findUniqueSuccessor(model, sq);
if (found != null) {
sq.connect(found);
result = true;
}
}
}
return result;
}

/** Return the unique square in MODEL to which unconnected square START
* can connect, or null if there isn't such a unique square. The unique
* square is either (1) the only connectable square in the proper
* direction from START, or (2) if START is numbered, a connectable
* numbered square in the proper direction from START (with the next
* number in sequence). */
static Sq findUniqueSuccessor(Model model, Sq start) {
// FIXME: Fill in to satisfy the comment.
return null;
}

/** Make all unique backward connections in MODEL (those in which there is
* a single possible predecessor). Return true iff changes made. */
static boolean makeBackwardConnections(Model model) {
int w = model.width(), h = model.height();
boolean result;
result = false;
for (Sq sq : model) {
if (sq.predecessor() == null && sq.sequenceNum() != 1) {
Sq found = findUniquePredecessor(model, sq);
if (found != null) {
found.connect(sq);
result = true;
}
}
}
return result;
}

/** Return the unique square in MODEL that can connect to unconnected
* square END, or null if there isn't such a unique square.
* This function does not handle the case in which END and one of its
* predecessors is numbered, except when the numbered predecessor is
* the only unconnected predecessor. This is because findUniqueSuccessor
* already finds the other cases of numbered, unconnected cells. */
static Sq findUniquePredecessor(Model model, Sq end) {
// FIXME: Replace the following to satisfy the comment.
return null;
}

/** Remove all links in MODEL and unfix numbers (other than the first and
* last) that do not affect solvability. Not all such numbers are
* necessarily removed. */
private void trimFixed(Model model) {
int w = model.width(), h = model.height();
boolean changed;
do {
changed = false;
for (Sq sq : model) {
if (sq.hasFixedNum() && sq.sequenceNum() != 1
&& sq.direction() != 0) {
model.restart();
int n = sq.sequenceNum();
sq.unfixNum();
extendSimple(model);
if (model.solved()) {
changed = true;
} else {
sq.setFixedNum(n);
}
}
}
} while (changed);
}

/** Fix additional numbers in MODEL to make the solution from which
* it was formed unique. Need not result in a minimal set of
* fixed numbers. */
private void makeSolutionUnique(Model model) {
model.restart();
AddNum:
while (true) {
extendSimple(model);
if (model.solved()) {
trimFixed(model);
model.restart();
return;
}
PlaceList unnumbered = new PlaceList();
for (Sq sq : model) {
if (sq.sequenceNum() == 0) {
unnumbered.add(sq.pl);
}
}
Collections.shuffle(unnumbered, _random);
for (Place p : unnumbered) {
Model model1 = new Model(model);
model1.get(p).setFixedNum(model.solution()[p.x][p.y]);
if (extendSimple(model1)) {
model.get(p).setFixedNum(model1.get(p).sequenceNum());
continue AddNum;
}
}
throw badArgs("no solution found");
}
}

@Override
public void setSeed(long seed) {
_random.setSeed(seed);
}

/** Solution board currently being filled in by findSolutionPathFrom. */
private int[][] _vals;
/** Mapping of positions and directions to lists of queen moves on _vals. */
private PlaceList[][][] _successorCells;

/** My PNRG. */
private Random _random;

}
165 changes: 165 additions & 0 deletions src/signpost/PuzzleGeneratorTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package signpost;

import static java.util.Arrays.asList;

import org.junit.Test;
import static org.junit.Assert.*;

import static signpost.Utils.tr;
import static signpost.Utils.setUp;
import static signpost.PuzzleGenerator.*;
import static signpost.ModelTests.checkNumbers;

/** Tests of the Model class.
* @author P. N. Hilfinger
*/
public class PuzzleGeneratorTests {

/** Check that SOLN is a valid WIDTH x HEIGHT puzzle that has fixed ends
* unless ALLOWLOOSE. */
private void checkPuzzle(int[][] soln,
int width, int height, boolean allowLoose) {
assertTrue("Bad size",
soln.length == width && soln[0].length == height);
int last = width * height;
for (int x0 = 0; x0 < width; x0 += 1) {
for (int y0 = 0; y0 < width; y0 += 1) {
int v = soln[x0][y0];
if (v == last) {
continue;
}
assertTrue("Value out of range", v >= 1 && v <= last);
int c;
for (int x1 = c = 0; x1 < width; x1 += 1) {
for (int y1 = 0; y1 < height; y1 += 1) {
if (soln[x1][y1] == v + 1) {
assertTrue("Values not in line",
x0 == x1 || y0 == y1
|| Math.abs(x0 - x1)
== Math.abs(y0 - y1));
c += 1;
}
}
}
assertEquals("Duplicate or unconnected values", 1, c);
}
}
if (!allowLoose) {
assertTrue("End points incorrect",
soln[0][height - 1] == 1 && soln[width - 1][0] == last);
}
}

@Test
public void puzzleTest() {
PuzzleGenerator puzzler = new PuzzleGenerator(314159);
Model model;
model = puzzler.getPuzzle(5, 5, false);
checkPuzzle(model.solution(), 5, 5, false);
model = puzzler.getPuzzle(4, 6, false);
checkPuzzle(model.solution(), 4, 6, false);
model = puzzler.getPuzzle(5, 5, true);
checkPuzzle(model.solution(), 5, 5, true);
}

@Test
public void uniquePuzzleTest() {
PuzzleGenerator puzzler = new PuzzleGenerator(314159);
Model model;
model = puzzler.getPuzzle(1, 2, false);
checkPuzzle(model.solution(), 1, 2, false);
model = puzzler.getPuzzle(1, 3, false);
checkPuzzle(model.solution(), 1, 3, false);
}

@Test
public void uniqueSuccessorTest() {
Model M = setUp(tr(SOLN1), SOLN1_NUMBERS, CONNECT1);
assertEquals("Unique connection to edge Sq", M.get(2, 6),
findUniqueSuccessor(M, M.get(3, 5)));
assertEquals("Unique connection through connected Sq", M.get(5, 0),
findUniqueSuccessor(M, M.get(3, 2)));
assertEquals("Ambiguous connection", null,
findUniqueSuccessor(M, M.get(3, 4)));
assertEquals("Successive numbered squares", M.get(3, 1),
findUniqueSuccessor(M, M.get(2, 0)));
assertEquals("Unique connection to numbered Sq", M.get(1, 1),
findUniqueSuccessor(M, M.get(3, 3)));
assertEquals("Unique connection to numbered Sq", M.get(1, 1),
findUniqueSuccessor(M, M.get(3, 3)));
assertEquals("Unique connection of numbered to unnumbered Sq",
M.get(4, 2),
findUniqueSuccessor(M, M.get(6, 4)));
}

@Test
public void uniquePredecessorTest() {
Model M = setUp(tr(SOLN2), SOLN2_NUMBERS, CONNECT2);
assertEquals("Unique predecessor", M.get(5, 6),
findUniquePredecessor(M, M.get(1, 6)));
assertEquals("Predecessor not unique", null,
findUniquePredecessor(M, M.get(3, 3)));
}

@Test
public void extendSimpleTest1() {
Model M = setUp(tr(SOLN3), new int[] { 1, 16 }, new int[] {});
assertTrue("Extend simple on ambiguous puzzle.", extendSimple(M));
checkNumbers(tr(PARTIAL_SOLN3), M, asList(1, 16));
}

@Test
public void extendSimpleTest2() {
Model M = setUp(tr(SOLN3), new int[] { 1, 9, 16 }, new int[] {});
assertTrue("Extend simple on unambiguous puzzle.", extendSimple(M));
checkNumbers(tr(SOLN3), M, asList(1, 9, 16));
}

static final int[][] SOLN1 = {
{ 3, 9, 29, 4, 49, 8, 5 },
{ 47, 12, 46, 28, 48, 6, 27 },
{ 15, 13, 16, 40, 41, 39, 24 },
{ 14, 44, 45, 36, 26, 7, 23 },
{ 2, 31, 1, 32, 25, 10, 30 },
{ 21, 37, 20, 35, 19, 11, 22 },
{ 42, 38, 34, 18, 43, 33, 17 }
};
static final int[] SOLN1_NUMBERS = {
9, 49, 27, 15, 40, 24, 44, 1, 30, 37, 35, 34
};
static final int[] CONNECT1 = { 18, 19, 41, 42, 7, 8, 8, 9 };

static final int[][] SOLN2 = {
{ 23, 48, 46, 18, 19, 47, 45 },
{ 42, 49, 4, 32, 8, 35, 36 },
{ 21, 6, 33, 11, 7, 15, 34 },
{ 5, 20, 9, 28, 1, 39, 38 },
{ 22, 10, 27, 41, 29, 30, 26 },
{ 43, 44, 16, 17, 13, 14, 12 },
{ 24, 2, 3, 31, 25, 40, 37 }
};

static final int[] SOLN2_NUMBERS = {
23, 46, 18, 47, 49, 32, 11, 34, 1, 44, 17, 2, 25
};

static final int[] CONNECT2 = { 45, 46 };

/** An ambiguous puzzle. The arrows corresponding to SOLN3, where the
* only fixed numbers are 1 and 16 has multiple solutions. */
static final int[][] SOLN3 = {
{ 1, 4, 13, 2 },
{ 8, 5, 3, 14 },
{ 6, 7, 12, 11 },
{ 9, 15, 10, 16 }
};

/** This should be the result of extendSimple on the puzzle SOLN3,
* SOLN3_NUMBERS. */
static final int[][] PARTIAL_SOLN3 = {
{ 1, 4, 0, 2 },
{ 0, 5, 3, 0 },
{ 6, 0, 0, 0 },
{ 0, 0, 0, 16 }
};
}
16 changes: 16 additions & 0 deletions src/signpost/PuzzleSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package signpost;

/** Describes a source of Signpost puzzles.
* @author P. N. Hilfinger
*/
interface PuzzleSource {

/** Returns a WIDTH x HEIGHT Model containing a puzzle. Unless
* ALLOWFREEENDS, the upper-left square will be numbered 1 and the
* lower-right will be numbered with the number of cells in the model. */
Model getPuzzle(int width, int height, boolean allowFreeEnds);

/** Reseed the random number generator with SEED. */
void setSeed(long seed);

}
94 changes: 94 additions & 0 deletions src/signpost/TestSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package signpost;

import java.util.Scanner;
import java.util.NoSuchElementException;

import static signpost.Utils.*;

/** A type of InputSource that receives commands from a Scanner. This kind
* of source is intended for testing.
* @author P. N. Hilfinger
*/
class TestSource implements CommandSource, PuzzleSource {

/** Provides commands and puzzles from SOURCE. */
TestSource(Scanner source) {
source.useDelimiter("[ \t\n\r(,)]+");
_source = source;
}

/** Returns a command string read from my source. At EOF, returns QUIT.
* Allows comment lines starting with "#", which are discarded. */
@Override
public String getCommand() {
while (_source.hasNext()) {
String line = _source.nextLine().trim().toUpperCase();
if (!line.startsWith("#")) {
return line;
}
}
return "QUIT";
}

/** Initialize MODEL to a puzzle. Throws IllegalStateException if there is
* no valid puzzle available, or if ALLOWFREEENDS is false and the
* puzzle does not have the default starting and ending squares in the
* upper-left and lower-right corners. */
@Override
public Model getPuzzle(int width, int height, boolean allowFreeEnds) {
try {
while (_source.hasNext(".*#.*")) {
_source.next();
_source.nextLine();
}

if (_source.hasNext("AUTOPUZZLE")) {
_source.next();
return _randomPuzzler.getPuzzle(width, height, allowFreeEnds);
}

_source.next("PUZZLE");
int w = _source.nextInt(), h = _source.nextInt();
if (w != width || h != height) {
throw badArgs("wrong puzzle size");
}
int[][] soln = new int[w][h];
for (int y = h - 1; y >= 0; y -= 1) {
for (int x = 0; x < w; x += 1) {
soln[x][y] = _source.nextInt();
}
}
if (!allowFreeEnds) {
if (soln[0][h - 1] != 1 || soln[w - 1][0] != w * h) {
throw new NoSuchElementException();
}
}
Model model = new Model(soln);
model.restart();
while (_source.hasNextInt()) {
int n = _source.nextInt();
for (int x = 0; x < w; x += 1) {
for (int y = 0; y < h; y += 1) {
if (soln[x][y] == n) {
model.get(x, y).setFixedNum(n);
}
}
}
}
_source.next("ENDPUZZLE");
return model;
} catch (NoSuchElementException excp) {
throw new IllegalStateException("missing or malformed puzzle");
}
}

@Override
public void setSeed(long seed) {
_randomPuzzler.setSeed(seed);
}

/** Input source. */
private Scanner _source;
/** Source for random puzzles. */
private PuzzleGenerator _randomPuzzler = new PuzzleGenerator(0);
}
17 changes: 17 additions & 0 deletions src/signpost/UnitTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package signpost;

import ucb.junit.textui;

/** The suite of all JUnit tests for the signpost package.
* @author P. N. Hilfinger
*/
public class UnitTests {

/** Run the JUnit tests in this package. Add xxxTest.class entries to
* the arguments of runClasses to run other JUnit tests. */
public static void main(String[] ignored) {
System.exit(textui.runClasses(ModelTests.class,
PuzzleGeneratorTests.class, PlaceTests.class));
}

}
85 changes: 85 additions & 0 deletions src/signpost/Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package signpost;

import static org.junit.Assert.*;
import static java.lang.System.arraycopy;
import java.util.Collection;
import java.util.HashSet;

/** Various utility methods.
* @author P. N. Hilfinger
*/
class Utils {

/** Returns String.format(FORMAT, ARGS...). */
static String msg(String format, Object... args) {
return String.format(format, args);
}

/** Copy contents of SRC into DEST. SRC and DEST must both be
* rectangular, with identical dimensions. */
static void deepCopy(int[][] src, int[][] dest) {
assert src.length == dest.length && src[0].length == dest[0].length;
for (int i = 0; i < src.length; i += 1) {
arraycopy(src[i], 0, dest[i], 0, src[i].length);
}
}

/** Check that the set of T's in EXPECTED is the same as that in ACTUAL.
* Use MSG as the error message if the check fails. */
static <T> void assertSetEquals(String msg,
Collection<T> expected,
Collection<T> actual) {
assertNotNull(msg, actual);
assertEquals(msg, new HashSet<T>(expected), new HashSet<T>(actual));
}

/** Return an IllegalArgumentException whose message is formed from
* MSGFORMAT and ARGS as for String.format. */
static IllegalArgumentException badArgs(String msgFormat, Object... args) {
return new IllegalArgumentException(String.format(msgFormat, args));
}

/** Return integer denoted by NUMERAL. */
static int toInt(String numeral) {
return Integer.parseInt(numeral);
}

/** Return long integer denoted by NUMERAL. */
static long toLong(String numeral) {
return Long.parseLong(numeral);
}

/** Given H x W array A, return a W x H array in which the columns of
* A, each reversed, are the rows of the result. That is, returns B
* so that B[x][y] is A[H - y - 1][x]. This is a convenience method
* that allows our test arrays to be arranged on the page to look as
* they do when displayed. */
static int[][] tr(int[][] A) {
int[][] B = new int[A[0].length][A.length];
for (int x = 0; x < A[0].length; x += 1) {
for (int y = 0; y < A.length; y += 1) {
B[x][y] = A[A.length - y - 1][x];
}
}
return B;
}

/** Returns a Model whose solution is SOLN, whose initially fixed numbers
* are FIXED, and which also has initial connections given by
* CONNECT. CONNECT is a 2k array denoting k connections:
* { u0, v0, u1, v1, ... }, where ui, vi indicates that the square
* containing ui in SOLN should be connected to the square containing
* Vi in SOLN. */
static Model setUp(int[][] soln, int[] fixed, int[] connect) {
Model result = new Model(soln);
for (int n : fixed) {
result.get(result.solnNumToPlace(n)).setFixedNum(n);
}
for (int i = 0; i < connect.length; i += 2) {
result.solnNumToSq(connect[i])
.connect(result.solnNumToSq(connect[i + 1]));
}
return result;
}
}

10 changes: 10 additions & 0 deletions src/signpost/View.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package signpost;

/** A view of a Signost model.
* @author P. N. Hilfinger */
interface View {

/** Update the current view according to MODEL. */
void update(Model model);

}

0 comments on commit d030786

Please sign in to comment.