This commit is contained in:
Anya Helene Bagge
2018-02-28 23:51:40 +01:00
parent c6692845f5
commit c1ad21da8c
53 changed files with 3884 additions and 2228 deletions

View File

@ -9,11 +9,6 @@ public interface IPaintLayer {
*/
void clear();
/**
* Send this layer to the front, so it will be drawn on top of any other layers.
*/
void layerToFront();
/**
* Send this layer to the back, so it will be drawn behind any other layers.
*
@ -24,4 +19,10 @@ public interface IPaintLayer {
* {@link Screen#getBackgroundContext()}.
*/
void layerToBack();
/**
* Send this layer to the front, so it will be drawn on top of any other layers.
*/
void layerToFront();
}

View File

@ -108,330 +108,15 @@ public class Screen {
public static final int CONFIG_FLAG_NO_AUTOHIDE_MOUSE = 2 << CONFIG_FLAG_SHIFT;
public static final int CONFIG_FLAG_DEBUG = 4 << CONFIG_FLAG_SHIFT;
private static final int CONFIG_FLAG_MASK = 7;
private final double rawCanvasWidth;
private final double rawCanvasHeight;
private boolean logKeyEvents = false;
private final SubScene subScene;
private final List<Canvas> canvases = new ArrayList<>();
private final Map<IPaintLayer, Canvas> layerCanvases = new IdentityHashMap<>();
private final Canvas background;
private final Group root;
private Paint bgColor = Color.CORNFLOWERBLUE;
private int aspect = 0;
private double scaling = 0;
private double currentScale = 1.0;
private double currentFit = 1.0;
private double resolutionScale = 1.0;
private int maxScale = 1;
private Predicate<KeyEvent> keyOverride = null;
private Predicate<KeyEvent> keyPressedHandler = null;
private Predicate<KeyEvent> keyTypedHandler = null;
private Predicate<KeyEvent> keyReleasedHandler = null;
private boolean debug = true;
private List<Double> aspects;
private boolean hideFullScreenMouseCursor = true;
private Cursor oldCursor;
/** @return the keyTypedHandler */
public Predicate<KeyEvent> getKeyTypedHandler() {
return keyTypedHandler;
}
/**
* @param keyTypedHandler
* the keyTypedHandler to set
*/
public void setKeyTypedHandler(Predicate<KeyEvent> keyTypedHandler) {
this.keyTypedHandler = keyTypedHandler;
}
/** @return the keyReleasedHandler */
public Predicate<KeyEvent> getKeyReleasedHandler() {
return keyReleasedHandler;
}
/**
* @param keyReleasedHandler
* the keyReleasedHandler to set
*/
public void setKeyReleasedHandler(Predicate<KeyEvent> keyReleasedHandler) {
this.keyReleasedHandler = keyReleasedHandler;
}
/** @return the keyOverride */
public Predicate<KeyEvent> getKeyOverride() {
return keyOverride;
}
/**
* @param keyOverride
* the keyOverride to set
*/
public void setKeyOverride(Predicate<KeyEvent> keyOverride) {
this.keyOverride = keyOverride;
}
/** @return the keyHandler */
public Predicate<KeyEvent> getKeyPressedHandler() {
return keyPressedHandler;
}
/**
* @param keyHandler
* the keyHandler to set
*/
public void setKeyPressedHandler(Predicate<KeyEvent> keyHandler) {
this.keyPressedHandler = keyHandler;
}
public Screen(double width, double height, double pixWidth, double pixHeight, double canvasWidth,
double canvasHeight) {
root = new Group();
subScene = new SubScene(root, Math.floor(width), Math.floor(height));
resolutionScale = pixWidth / canvasWidth;
this.rawCanvasWidth = Math.floor(pixWidth);
this.rawCanvasHeight = Math.floor(pixHeight);
double aspectRatio = width / height;
aspect = 0;
for (double a : STD_ASPECTS)
if (Math.abs(aspectRatio - a) < 0.01) {
break;
} else {
aspect++;
}
aspects = new ArrayList<>(STD_ASPECTS);
if (aspect >= STD_ASPECTS.size()) {
aspects.add(aspectRatio);
}
background = new Canvas(rawCanvasWidth, rawCanvasHeight);
background.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
setBackground(bgColor);
clearBackground();
root.getChildren().add(background);
subScene.layoutBoundsProperty()
.addListener((ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) -> {
recomputeLayout(false);
});
}
public void clearBackground() {
getBackgroundContext().setFill(bgColor);
getBackgroundContext().fillRect(0.0, 0.0, background.getWidth(), background.getHeight());
}
public void cycleAspect() {
aspect = (aspect + 1) % aspects.size();
recomputeLayout(false);
}
public void zoomCycle() {
scaling++;
if (scaling > maxScale)
scaling = ((int) scaling) % maxScale;
recomputeLayout(true);
}
public void zoomIn() {
scaling = Math.min(10, currentScale + 0.2);
recomputeLayout(false);
}
public void zoomOut() {
scaling = Math.max(0.1, currentScale - 0.2);
recomputeLayout(false);
}
public void zoomFit() {
scaling = 0;
recomputeLayout(false);
}
public void zoomOne() {
scaling = 1;
recomputeLayout(false);
}
public void fitScaling() {
scaling = 0;
recomputeLayout(true);
}
public int getAspect() {
return aspect;
}
public GraphicsContext getBackgroundContext() {
return background.getGraphicsContext2D();
}
public TurtlePainter createPainter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new TurtlePainter(this, canvas);
}
public Printer createPrinter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new Printer(this, canvas);
}
private void recomputeLayout(boolean resizeWindow) {
double xScale = subScene.getWidth() / getRawWidth();
double yScale = subScene.getHeight() / getRawHeight();
double xMaxScale = getDisplayWidth() / getRawWidth();
double yMaxScale = getDisplayHeight() / getRawHeight();
currentFit = Math.min(xScale, yScale);
maxScale = (int) Math.max(1, Math.ceil(Math.min(xMaxScale, yMaxScale)));
currentScale = scaling == 0 ? currentFit : scaling;
if (resizeWindow) {
Scene scene = subScene.getScene();
Window window = scene.getWindow();
double hBorder = window.getWidth() - scene.getWidth();
double vBorder = window.getHeight() - scene.getHeight();
double myWidth = getRawWidth() * currentScale;
double myHeight = getRawHeight() * currentScale;
if (debug)
System.out.printf(
"Resizing before: screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", //
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
// this.setWidth(myWidth);
// this.setHeight(myHeight);
window.setWidth(myWidth + hBorder);
window.setHeight(myHeight + vBorder);
if (debug)
System.out.printf(
"Resizing after : screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n",
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
}
if (debug)
System.out.printf("Rescaling: subscene %1.2fx%1.2f, scale %1.2f, aspect %.4f (%d), canvas %1.0fx%1.0f%n",
subScene.getWidth(), subScene.getHeight(), currentScale, aspects.get(aspect), aspect, getRawWidth(),
getRawHeight());
for (Node n : root.getChildren()) {
n.relocate(Math.floor(subScene.getWidth() / 2),
Math.floor(subScene.getHeight() / 2 + (rawCanvasHeight - getRawHeight()) * currentScale / 2));
n.setTranslateX(-Math.floor(rawCanvasWidth / 2));
n.setTranslateY(-Math.floor(rawCanvasHeight / 2));
if (debug)
System.out.printf(" * layout %1.2fx%1.2f, translate %1.2fx%1.2f%n", n.getLayoutX(), n.getLayoutY(),
n.getTranslateX(), n.getTranslateY());
n.setScaleX(currentScale);
n.setScaleY(currentScale);
}
}
public void setAspect(int aspect) {
this.aspect = (aspect) % aspects.size();
recomputeLayout(false);
}
public void setBackground(Paint bgColor) {
this.bgColor = bgColor;
subScene.setFill(bgColor instanceof Color ? ((Color) bgColor).darker() : bgColor);
}
public boolean minimalKeyHandler(KeyEvent event) {
KeyCode code = event.getCode();
if (event.isShortcutDown()) {
if (code == KeyCode.Q) {
System.exit(0);
} else if (code == KeyCode.PLUS) {
zoomIn();
return true;
} else if (code == KeyCode.MINUS) {
zoomOut();
return true;
}
} else if (!(event.isAltDown() || event.isControlDown() || event.isMetaDown() || event.isShiftDown())) {
if (code == KeyCode.F11) {
setFullScreen(!isFullScreen());
return true;
}
}
return false;
}
public boolean isFullScreen() {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage)
return ((Stage) window).isFullScreen();
else
return false;
}
public void setFullScreen(boolean fullScreen) {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage) {
((Stage) window).setFullScreenExitHint("");
((Stage) window).setFullScreen(fullScreen);
if (hideFullScreenMouseCursor) {
if (fullScreen) {
oldCursor = subScene.getScene().getCursor();
subScene.getScene().setCursor(Cursor.NONE);
} else if (oldCursor != null) {
subScene.getScene().setCursor(oldCursor);
oldCursor = null;
} else {
subScene.getScene().setCursor(Cursor.DEFAULT);
}
}
}
}
/**
* Get the native physical width of the screen, in pixels.
* Get the resolution of this screen, in DPI (pixels per inch).
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
* @return The primary display's DPI
* @see javafx.stage.Screen#getDpi()
*/
public static double getRawDisplayWidth() {
return javafx.stage.Screen.getPrimary().getBounds().getWidth();
}
/**
* Get the native physical height of the screen, in pixels.
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
*/
public static double getRawDisplayHeight() {
return javafx.stage.Screen.getPrimary().getBounds().getHeight();
}
/**
* Get the width of the display, in pixels.
*
* <p>
* This takes into account such things as toolbars, menus and such (on a
* desktop), and pixel density (e.g., on high resolution mobile devices).
*
* @return Width of the display
* @see javafx.stage.Screen#getVisualBounds()
*/
public static double getDisplayWidth() {
return javafx.stage.Screen.getPrimary().getVisualBounds().getWidth();
public static double getDisplayDpi() {
return javafx.stage.Screen.getPrimary().getDpi();
}
/**
@ -449,13 +134,45 @@ public class Screen {
}
/**
* Get the resolution of this screen, in DPI (pixels per inch).
* Get the width of the display, in pixels.
*
* @return The primary display's DPI
* @see javafx.stage.Screen#getDpi()
* <p>
* This takes into account such things as toolbars, menus and such (on a
* desktop), and pixel density (e.g., on high resolution mobile devices).
*
* @return Width of the display
* @see javafx.stage.Screen#getVisualBounds()
*/
public static double getDisplayDpi() {
return javafx.stage.Screen.getPrimary().getDpi();
public static double getDisplayWidth() {
return javafx.stage.Screen.getPrimary().getVisualBounds().getWidth();
}
/**
* Get the native physical height of the screen, in pixels.
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
*/
public static double getRawDisplayHeight() {
return javafx.stage.Screen.getPrimary().getBounds().getHeight();
}
/**
* Get the native physical width of the screen, in pixels.
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
*/
public static double getRawDisplayWidth() {
return javafx.stage.Screen.getPrimary().getBounds().getWidth();
}
/**
@ -607,27 +324,174 @@ public class Screen {
return pScene;
}
public double getRawWidth() {
return rawCanvasWidth;
private final double rawCanvasWidth;
private final double rawCanvasHeight;
private boolean logKeyEvents = false;
private final SubScene subScene;
private final List<Canvas> canvases = new ArrayList<>();
private final Map<IPaintLayer, Canvas> layerCanvases = new IdentityHashMap<>();
private final Canvas background;
private final Group root;
private Paint bgColor = Color.CORNFLOWERBLUE;
private int aspect = 0;
private double scaling = 0;
private double currentScale = 1.0;
private double currentFit = 1.0;
private double resolutionScale = 1.0;
private int maxScale = 1;
private Predicate<KeyEvent> keyOverride = null;
private Predicate<KeyEvent> keyPressedHandler = null;
private Predicate<KeyEvent> keyTypedHandler = null;
private Predicate<KeyEvent> keyReleasedHandler = null;
private boolean debug = true;
private List<Double> aspects;
private boolean hideFullScreenMouseCursor = true;
private Cursor oldCursor;
public Screen(double width, double height, double pixWidth, double pixHeight, double canvasWidth,
double canvasHeight) {
root = new Group();
subScene = new SubScene(root, Math.floor(width), Math.floor(height));
resolutionScale = pixWidth / canvasWidth;
this.rawCanvasWidth = Math.floor(pixWidth);
this.rawCanvasHeight = Math.floor(pixHeight);
double aspectRatio = width / height;
aspect = 0;
for (double a : STD_ASPECTS)
if (Math.abs(aspectRatio - a) < 0.01) {
break;
} else {
aspect++;
}
aspects = new ArrayList<>(STD_ASPECTS);
if (aspect >= STD_ASPECTS.size()) {
aspects.add(aspectRatio);
}
background = new Canvas(rawCanvasWidth, rawCanvasHeight);
background.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
setBackground(bgColor);
clearBackground();
root.getChildren().add(background);
subScene.layoutBoundsProperty()
.addListener((ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) -> {
recomputeLayout(false);
});
}
public double getRawHeight() {
return Math.floor(rawCanvasWidth / aspects.get(aspect));
public void clearBackground() {
getBackgroundContext().setFill(bgColor);
getBackgroundContext().fillRect(0.0, 0.0, background.getWidth(), background.getHeight());
}
public double getWidth() {
return Math.floor(getRawWidth() / resolutionScale);
public TurtlePainter createPainter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new TurtlePainter(this, canvas);
}
public Printer createPrinter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new Printer(this, canvas);
}
public void cycleAspect() {
aspect = (aspect + 1) % aspects.size();
recomputeLayout(false);
}
public void fitScaling() {
scaling = 0;
recomputeLayout(true);
}
public int getAspect() {
return aspect;
}
public GraphicsContext getBackgroundContext() {
return background.getGraphicsContext2D();
}
public double getHeight() {
return Math.floor(getRawHeight() / resolutionScale);
}
public void moveToFront(IPaintLayer layer) {
Canvas canvas = layerCanvases.get(layer);
if (canvas != null) {
canvas.toFront();
/** @return the keyOverride */
public Predicate<KeyEvent> getKeyOverride() {
return keyOverride;
}
/** @return the keyHandler */
public Predicate<KeyEvent> getKeyPressedHandler() {
return keyPressedHandler;
}
/** @return the keyReleasedHandler */
public Predicate<KeyEvent> getKeyReleasedHandler() {
return keyReleasedHandler;
}
/** @return the keyTypedHandler */
public Predicate<KeyEvent> getKeyTypedHandler() {
return keyTypedHandler;
}
public double getRawHeight() {
return Math.floor(rawCanvasWidth / aspects.get(aspect));
}
public double getRawWidth() {
return rawCanvasWidth;
}
public double getWidth() {
return Math.floor(getRawWidth() / resolutionScale);
}
public void hideMouseCursor() {
subScene.getScene().setCursor(Cursor.NONE);
}
public boolean isFullScreen() {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage)
return ((Stage) window).isFullScreen();
else
return false;
}
public boolean minimalKeyHandler(KeyEvent event) {
KeyCode code = event.getCode();
if (event.isShortcutDown()) {
if (code == KeyCode.Q) {
System.exit(0);
} else if (code == KeyCode.PLUS) {
zoomIn();
return true;
} else if (code == KeyCode.MINUS) {
zoomOut();
return true;
}
} else if (!(event.isAltDown() || event.isControlDown() || event.isMetaDown() || event.isShiftDown())) {
if (code == KeyCode.F11) {
setFullScreen(!isFullScreen());
return true;
}
}
return false;
}
public void moveToBack(IPaintLayer layer) {
@ -638,16 +502,93 @@ public class Screen {
}
}
public void hideMouseCursor() {
subScene.getScene().setCursor(Cursor.NONE);
public void moveToFront(IPaintLayer layer) {
Canvas canvas = layerCanvases.get(layer);
if (canvas != null) {
canvas.toFront();
}
}
public void showMouseCursor() {
subScene.getScene().setCursor(Cursor.DEFAULT);
private void recomputeLayout(boolean resizeWindow) {
double xScale = subScene.getWidth() / getRawWidth();
double yScale = subScene.getHeight() / getRawHeight();
double xMaxScale = getDisplayWidth() / getRawWidth();
double yMaxScale = getDisplayHeight() / getRawHeight();
currentFit = Math.min(xScale, yScale);
maxScale = (int) Math.max(1, Math.ceil(Math.min(xMaxScale, yMaxScale)));
currentScale = scaling == 0 ? currentFit : scaling;
if (resizeWindow) {
Scene scene = subScene.getScene();
Window window = scene.getWindow();
double hBorder = window.getWidth() - scene.getWidth();
double vBorder = window.getHeight() - scene.getHeight();
double myWidth = getRawWidth() * currentScale;
double myHeight = getRawHeight() * currentScale;
if (debug)
System.out.printf(
"Resizing before: screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", //
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
// this.setWidth(myWidth);
// this.setHeight(myHeight);
window.setWidth(myWidth + hBorder);
window.setHeight(myHeight + vBorder);
if (debug)
System.out.printf(
"Resizing after : screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n",
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
}
if (debug)
System.out.printf("Rescaling: subscene %1.2fx%1.2f, scale %1.2f, aspect %.4f (%d), canvas %1.0fx%1.0f%n",
subScene.getWidth(), subScene.getHeight(), currentScale, aspects.get(aspect), aspect, getRawWidth(),
getRawHeight());
for (Node n : root.getChildren()) {
n.relocate(Math.floor(subScene.getWidth() / 2),
Math.floor(subScene.getHeight() / 2 + (rawCanvasHeight - getRawHeight()) * currentScale / 2));
n.setTranslateX(-Math.floor(rawCanvasWidth / 2));
n.setTranslateY(-Math.floor(rawCanvasHeight / 2));
if (debug)
System.out.printf(" * layout %1.2fx%1.2f, translate %1.2fx%1.2f%n", n.getLayoutX(), n.getLayoutY(),
n.getTranslateX(), n.getTranslateY());
n.setScaleX(currentScale);
n.setScaleY(currentScale);
}
}
public void setMouseCursor(Cursor cursor) {
subScene.getScene().setCursor(cursor);
public void setAspect(int aspect) {
this.aspect = (aspect) % aspects.size();
recomputeLayout(false);
}
public void setBackground(Paint bgColor) {
this.bgColor = bgColor;
subScene.setFill(bgColor instanceof Color ? ((Color) bgColor).darker() : bgColor);
}
public void setFullScreen(boolean fullScreen) {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage) {
((Stage) window).setFullScreenExitHint("");
((Stage) window).setFullScreen(fullScreen);
if (hideFullScreenMouseCursor) {
if (fullScreen) {
oldCursor = subScene.getScene().getCursor();
subScene.getScene().setCursor(Cursor.NONE);
} else if (oldCursor != null) {
subScene.getScene().setCursor(oldCursor);
oldCursor = null;
} else {
subScene.getScene().setCursor(Cursor.DEFAULT);
}
}
}
}
public void setHideFullScreenMouseCursor(boolean hideIt) {
@ -664,4 +605,71 @@ public class Screen {
}
hideFullScreenMouseCursor = hideIt;
}
/**
* @param keyOverride
* the keyOverride to set
*/
public void setKeyOverride(Predicate<KeyEvent> keyOverride) {
this.keyOverride = keyOverride;
}
/**
* @param keyHandler
* the keyHandler to set
*/
public void setKeyPressedHandler(Predicate<KeyEvent> keyHandler) {
this.keyPressedHandler = keyHandler;
}
/**
* @param keyReleasedHandler
* the keyReleasedHandler to set
*/
public void setKeyReleasedHandler(Predicate<KeyEvent> keyReleasedHandler) {
this.keyReleasedHandler = keyReleasedHandler;
}
/**
* @param keyTypedHandler
* the keyTypedHandler to set
*/
public void setKeyTypedHandler(Predicate<KeyEvent> keyTypedHandler) {
this.keyTypedHandler = keyTypedHandler;
}
public void setMouseCursor(Cursor cursor) {
subScene.getScene().setCursor(cursor);
}
public void showMouseCursor() {
subScene.getScene().setCursor(Cursor.DEFAULT);
}
public void zoomCycle() {
scaling++;
if (scaling > maxScale)
scaling = ((int) scaling) % maxScale;
recomputeLayout(true);
}
public void zoomFit() {
scaling = 0;
recomputeLayout(false);
}
public void zoomIn() {
scaling = Math.min(10, currentScale + 0.2);
recomputeLayout(false);
}
public void zoomOne() {
scaling = 1;
recomputeLayout(false);
}
public void zoomOut() {
scaling = Math.max(0.1, currentScale - 0.2);
recomputeLayout(false);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -132,6 +132,7 @@ public class Direction {
return Math.atan2(yDir, xDir);
}
@Override
public String toString() {
return String.format("%.2f", toDegrees());
}

View File

@ -5,13 +5,13 @@ import javafx.scene.paint.Paint;
public interface IPainter extends IPaintLayer {
IShape shape();
ITurtle turtle();
IPainter restore();
IPainter save();
IPainter setInk(Paint ink);
IShape shape();
ITurtle turtle();
}

View File

@ -6,13 +6,51 @@ import javafx.scene.shape.Shape;
public interface IShape {
void draw();
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(double x, double y);
void draw(GraphicsContext context);
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(Point xy);
Shape toFXShape();
/**
* Set the arc angle for the subsequent draw commands
*
* <p>
* For use with {@link #arc()}
*
* @param a
* The angle, in degrees
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape angle(double a);
String toSvg();
/**
* Draw an arc with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape arc();
/**
* Set the (x,y)-coordinates of the next draw command
@ -24,6 +62,160 @@ public interface IShape {
*/
IShape at(Point p);
/**
* Close the line path, turning it into a polygon.
*
* @return
*/
IShape close();
void draw();
void draw(GraphicsContext context);
/**
* Draw an ellipse with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape ellipse();
/**
* Fill the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fill();
/**
* Set fill colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fillPaint(Paint p);
/**
* Set gravity for the subsequent draw commands
*
* Gravity determines the point on the shape that will be used for positioning
* and rotation.
*
* @param g
* The gravity
* @return
*/
IShape gravity(Gravity g);
/**
* Set the height of the next draw command
*
* @param h
* The height
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape height(double h);
/**
* Set the length of the following draw commands
*
* <p>
* For use with {@link #line()} and {@link #arc()}
*
* @param l
* The length
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape length(double l);
/**
* Draw a line with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)} (flattened to the horizontal axis, so, e.g.,
* {@link Gravity#NORTH} = {@link Gravity#SOUTH} = {@link Gravity#CENTER})
* <li>{@link #stroke(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape line();
/**
* Draw a rectangle with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape rectangle();
/**
* Sets rotation for subsequent draw commands.
*
* <p>
* Shapes will be rotate around the {@link #gravity(Gravity)} point.
*
* @param angle
* Rotation in degrees
* @return
*/
IShape rotation(double angle);
/**
* Stroke the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape stroke();
/**
* Set stroke colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape strokePaint(Paint p);
Shape toFXShape();
String toSvg();
/**
* Set the width of the next draw command
*
* @param w
* The width
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape width(double w);
/**
* Set the x-coordinate of the next draw command
*
@ -44,196 +236,4 @@ public interface IShape {
*/
IShape y(double y);
/**
* Set gravity for the subsequent draw commands
*
* Gravity determines the point on the shape that will be used for positioning
* and rotation.
*
* @param g
* The gravity
* @return
*/
IShape gravity(Gravity g);
/**
* Sets rotation for subsequent draw commands.
*
* <p>
* Shapes will be rotate around the {@link #gravity(Gravity)} point.
*
* @param angle
* Rotation in degrees
* @return
*/
IShape rotation(double angle);
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(Point xy);
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(double x, double y);
/**
* Close the line path, turning it into a polygon.
*
* @return
*/
IShape close();
/**
* Draw an ellipse with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape ellipse();
/**
* Draw a rectangle with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape rectangle();
/**
* Draw an arc with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape arc();
/**
* Draw a line with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)} (flattened to the horizontal axis, so, e.g.,
* {@link Gravity#NORTH} = {@link Gravity#SOUTH} = {@link Gravity#CENTER})
* <li>{@link #stroke(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape line();
/**
* Set the arc angle for the subsequent draw commands
*
* <p>
* For use with {@link #arc()}
*
* @param a
* The angle, in degrees
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape angle(double a);
/**
* Set fill colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fillPaint(Paint p);
/**
* Fill the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fill();
/**
* Set the length of the following draw commands
*
* <p>
* For use with {@link #line()} and {@link #arc()}
*
* @param l
* The length
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape length(double l);
/**
* Stroke the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape stroke();
/**
* Set stroke colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape strokePaint(Paint p);
/**
* Set the width of the next draw command
*
* @param w
* The width
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape width(double w);
/**
* Set the height of the next draw command
*
* @param h
* The height
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape height(double h);
}

View File

@ -1,36 +1,63 @@
package inf101.v18.gfx.gfxmode;
import javafx.scene.canvas.GraphicsContext;
public interface ITurtle extends IPainter {
<T> T as(Class<T> class1);
/**
* Move to the given position while drawing a curve
*
* <p>
* The resulting curve is a cubic Bézier curve with the control points located
* at <code>getPos().move(getDirection, startControl)</code> and
* <code>to.move(Direction.fromDegrees(endAngle+180), endControl)</code>.
* <p>
* The turtle is left at point <code>to</code>, facing <code>endAngle</code>.
* <p>
* The turtle will start out moving in its current direction, aiming for a point
* <code>startControl</code> pixels away, then smoothly turning towards its
* goal. It will approach the <code>to</code> point moving in the direction
* <code>endAngle</code> (an absolute bearing, with 0° pointing right and 90°
* pointing up).
*
* @param to
* Position to move to
* @param startControl
* Distance to the starting control point.
* @return {@code this}, for sending more draw commands
*/
ITurtle curveTo(Point to, double startControl, double endAngle, double endControl);
void debugTurtle();
/**
* Start drawing a shape at the current turtle position.
* Move forward the given distance while drawing a line
*
* <p>
* The shape's default origin and rotation will be set to the turtle's current
* position and direction, but can be modified with {@link IShape#at(Point)} and
* {@link IShape#rotation(double)}.
* <p>
* The turtle's position and attributes are unaffected by drawing the shape.
*
* @return An IDrawParams object for setting up and drawing the shape
*/
IShape shape();
/**
* Draw a line from the current position to the given position.
*
* <p>
* This method does not change the turtle position.
*
* @param to
* Other end-point of the line
* @param dist
* Distance to move
* @return {@code this}, for sending more draw commands
*/
ITurtle line(Point to);
ITurtle draw(double dist);
/**
* Move to the given position while drawing a line
*
* @param x
* X-position to move to
* @param y
* Y-position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(double x, double y);
/**
* Move to the given position while drawing a line
*
* @param to
* Position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(Point to);
/**
* @return The current angle of the turtle, with 0° pointing to the right and
@ -79,57 +106,16 @@ public interface ITurtle extends IPainter {
ITurtle jumpTo(Point to);
/**
* Move forward the given distance while drawing a line
* Draw a line from the current position to the given position.
*
* @param dist
* Distance to move
* @return {@code this}, for sending more draw commands
*/
ITurtle draw(double dist);
/**
* Move to the given position while drawing a line
*
* @param x
* X-position to move to
* @param y
* Y-position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(double x, double y);
/**
* Move to the given position while drawing a line
* <p>
* This method does not change the turtle position.
*
* @param to
* Position to move to
* Other end-point of the line
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(Point to);
/**
* Move to the given position while drawing a curve
*
* <p>
* The resulting curve is a cubic Bézier curve with the control points located
* at <code>getPos().move(getDirection, startControl)</code> and
* <code>to.move(Direction.fromDegrees(endAngle+180), endControl)</code>.
* <p>
* The turtle is left at point <code>to</code>, facing <code>endAngle</code>.
* <p>
* The turtle will start out moving in its current direction, aiming for a point
* <code>startControl</code> pixels away, then smoothly turning towards its
* goal. It will approach the <code>to</code> point moving in the direction
* <code>endAngle</code> (an absolute bearing, with 0° pointing right and 90°
* pointing up).
*
* @param to
* Position to move to
* @param startControl
* Distance to the starting control point.
* @return {@code this}, for sending more draw commands
*/
ITurtle curveTo(Point to, double startControl, double endAngle, double endControl);
ITurtle line(Point to);
/**
* Set the size of the turtle's pen
@ -141,6 +127,21 @@ public interface ITurtle extends IPainter {
*/
ITurtle setPenSize(double pixels);
/**
* Start drawing a shape at the current turtle position.
*
* <p>
* The shape's default origin and rotation will be set to the turtle's current
* position and direction, but can be modified with {@link IShape#at(Point)} and
* {@link IShape#rotation(double)}.
* <p>
* The turtle's position and attributes are unaffected by drawing the shape.
*
* @return An IDrawParams object for setting up and drawing the shape
*/
@Override
IShape shape();
/**
* Change direction the given number of degrees (relative to the current
* direction).
@ -235,6 +236,7 @@ public interface ITurtle extends IPainter {
*/
ITurtle turnTowards(double degrees, double percent);
<T> T as(Class<T> class1);
double getWidth();
double getHeight();
}

View File

@ -88,6 +88,7 @@ public class Point {
return new Point(newX, newY);
}
@Override
public String toString() {
return String.format("(%.2f,%.2f)", x, y);
}

View File

@ -7,158 +7,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.Shape;
public class ShapePainter implements IShape {
private double x = 0, y = 0, w = 0, h = 0, rot = 0, strokeWidth = 0;
private List<Double> lineSegments = null;
private Paint fill = null;
private Paint stroke = null;
private Gravity gravity = Gravity.CENTER;
private DrawCommand cmd = null;
private boolean closed = false;
private final GraphicsContext context;
public ShapePainter(GraphicsContext context) {
super();
this.context = context;
}
public ShapePainter at(Point p) {
if (p != null) {
this.x = p.getX();
this.y = p.getY();
} else {
this.x = 0;
this.y = 0;
}
return this;
}
public ShapePainter x(double x) {
this.x = x;
return this;
}
public ShapePainter y(double y) {
this.y = y;
return this;
}
public ShapePainter width(double w) {
this.w = w;
return this;
}
public ShapePainter height(double h) {
this.h = h;
return this;
}
public IShape ellipse() {
cmd = new DrawEllipse();
return this;
}
public IShape rectangle() {
cmd = new DrawRectangle();
return this;
}
public IShape arc() {
// TODO Auto-generated method stub
return this;
}
public IShape line() {
cmd = new DrawLine();
return this;
}
public ShapePainter length(double l) {
w = l;
h = l;
return this;
}
public ShapePainter angle(double a) {
return this;
}
public ShapePainter fill() {
if (cmd != null)
cmd.fill(context, this);
return this;
}
public ShapePainter stroke() {
if (cmd != null)
cmd.stroke(context, this);
return this;
}
public ShapePainter fillPaint(Paint p) {
fill = p;
return this;
}
public ShapePainter strokePaint(Paint p) {
stroke = p;
return this;
}
public ShapePainter gravity(Gravity g) {
gravity = g;
return this;
}
public ShapePainter rotation(double angle) {
rot = angle;
return this;
}
public void draw() {
draw(context);
}
public Shape toFXShape() {
// TODO Auto-generated method stub
return null;
}
public String toSvg() {
// TODO Auto-generated method stub
return null;
}
private abstract static class DrawCommand {
public void stroke(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setStroke(p.stroke);
if (p.strokeWidth != 0)
ctx.setLineWidth(p.strokeWidth);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
strokeIt(ctx, p);
ctx.restore();
}
public void fill(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setFill(p.fill);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
fillIt(ctx, p);
ctx.restore();
}
protected abstract void strokeIt(GraphicsContext ctx, ShapePainter p);
protected abstract void fillIt(GraphicsContext ctx, ShapePainter p);
// public abstract Shape toFXShape(DrawParams p);
//
// public abstract String toSvg(DrawParams p);
protected double calcX(Gravity g, double w) {
switch (g) {
default:
@ -206,32 +55,70 @@ public class ShapePainter implements IShape {
return h / 2;
}
}
}
private static class DrawRectangle extends DrawCommand {
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
public void fill(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setFill(p.fill);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
fillIt(ctx, p);
ctx.restore();
}
public void fillIt(GraphicsContext ctx, ShapePainter p) {
ctx.fillRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
protected abstract void fillIt(GraphicsContext ctx, ShapePainter p);
// public abstract Shape toFXShape(DrawParams p);
//
// public abstract String toSvg(DrawParams p);
public void stroke(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setStroke(p.stroke);
if (p.strokeWidth != 0)
ctx.setLineWidth(p.strokeWidth);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
strokeIt(ctx, p);
ctx.restore();
}
protected abstract void strokeIt(GraphicsContext ctx, ShapePainter p);
}
private static class DrawEllipse extends DrawCommand {
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
@Override
public void fillIt(GraphicsContext ctx, ShapePainter p) {
ctx.fillOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
@Override
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
}
private static class DrawLine extends DrawCommand {
@Override
public void fillIt(GraphicsContext ctx, ShapePainter p) {
if (p.lineSegments != null) {
int nPoints = (p.lineSegments.size() / 2) + 1;
double xs[] = new double[nPoints];
double ys[] = new double[nPoints];
xs[0] = -calcX(p.gravity, p.w);
ys[0] = -calcY(p.gravity, p.h);
for (int i = 0; i < p.lineSegments.size(); i++) {
xs[i] = p.lineSegments.get(i * 2) - p.x;
ys[i] = p.lineSegments.get(i * 2 + 1) - p.y;
}
ctx.fillPolygon(xs, ys, nPoints);
}
}
@Override
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
if (p.lineSegments == null) {
double x = -calcX(p.gravity, p.w);
@ -253,37 +140,37 @@ public class ShapePainter implements IShape {
ctx.strokePolyline(xs, ys, nPoints);
}
}
}
private static class DrawRectangle extends DrawCommand {
@Override
public void fillIt(GraphicsContext ctx, ShapePainter p) {
if (p.lineSegments != null) {
int nPoints = (p.lineSegments.size() / 2) + 1;
double xs[] = new double[nPoints];
double ys[] = new double[nPoints];
xs[0] = -calcX(p.gravity, p.w);
ys[0] = -calcY(p.gravity, p.h);
for (int i = 0; i < p.lineSegments.size(); i++) {
xs[i] = p.lineSegments.get(i * 2) - p.x;
ys[i] = p.lineSegments.get(i * 2 + 1) - p.y;
}
ctx.fillPolygon(xs, ys, nPoints);
}
ctx.fillRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
@Override
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
}
public void draw(GraphicsContext context) {
if (cmd != null) {
if (fill != null)
cmd.fill(context, this);
if (stroke != null)
cmd.stroke(context, this);
}
}
private double x = 0, y = 0, w = 0, h = 0, rot = 0, strokeWidth = 0;
private List<Double> lineSegments = null;
private Paint fill = null;
private Paint stroke = null;
@Override
public IShape addPoint(Point xy) {
lineSegments.add(xy.getX());
lineSegments.add(xy.getY());
return this;
private Gravity gravity = Gravity.CENTER;
private DrawCommand cmd = null;
private boolean closed = false;
private final GraphicsContext context;
public ShapePainter(GraphicsContext context) {
super();
this.context = context;
}
@Override
@ -293,9 +180,150 @@ public class ShapePainter implements IShape {
return this;
}
@Override
public IShape addPoint(Point xy) {
lineSegments.add(xy.getX());
lineSegments.add(xy.getY());
return this;
}
@Override
public ShapePainter angle(double a) {
return this;
}
@Override
public IShape arc() {
throw new UnsupportedOperationException();
}
@Override
public ShapePainter at(Point p) {
if (p != null) {
this.x = p.getX();
this.y = p.getY();
} else {
this.x = 0;
this.y = 0;
}
return this;
}
@Override
public IShape close() {
closed = true;
return this;
}
@Override
public void draw() {
draw(context);
}
@Override
public void draw(GraphicsContext context) {
if (cmd != null) {
if (fill != null)
cmd.fill(context, this);
if (stroke != null)
cmd.stroke(context, this);
}
}
@Override
public IShape ellipse() {
cmd = new DrawEllipse();
return this;
}
@Override
public ShapePainter fill() {
if (cmd != null)
cmd.fill(context, this);
return this;
}
@Override
public ShapePainter fillPaint(Paint p) {
fill = p;
return this;
}
@Override
public ShapePainter gravity(Gravity g) {
gravity = g;
return this;
}
@Override
public ShapePainter height(double h) {
this.h = h;
return this;
}
@Override
public ShapePainter length(double l) {
w = l;
h = l;
return this;
}
@Override
public IShape line() {
cmd = new DrawLine();
return this;
}
@Override
public IShape rectangle() {
cmd = new DrawRectangle();
return this;
}
@Override
public ShapePainter rotation(double angle) {
rot = angle;
return this;
}
@Override
public ShapePainter stroke() {
if (cmd != null)
cmd.stroke(context, this);
return this;
}
@Override
public ShapePainter strokePaint(Paint p) {
stroke = p;
return this;
}
@Override
public Shape toFXShape() {
throw new UnsupportedOperationException();
}
@Override
public String toSvg() {
throw new UnsupportedOperationException();
}
@Override
public ShapePainter width(double w) {
this.w = w;
return this;
}
@Override
public ShapePainter x(double x) {
this.x = x;
return this;
}
@Override
public ShapePainter y(double y) {
this.y = y;
return this;
}
}

View File

@ -32,6 +32,8 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
}
private final Screen screen;
private final double width;
private final double height;
private final GraphicsContext context;
private final List<TurtleState> stateStack = new ArrayList<>();
@ -39,68 +41,45 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
private final Canvas canvas;
private boolean path = false;
public TurtlePainter(double width, double height, Canvas canvas) {
screen = null;
if (canvas == null)
canvas = new Canvas(width, height);
this.canvas = canvas;
this.context = canvas.getGraphicsContext2D();
this.width = width;
this.height = height;
stateStack.add(new TurtleState());
state.dir = new Direction(1.0, 0.0);
state.pos = new Point(width / 2, height / 2);
}
public TurtlePainter(Screen screen, Canvas canvas) {
this.screen = screen;
this.canvas = canvas;
this.context = canvas.getGraphicsContext2D();
this.width = screen.getWidth();
this.height = screen.getHeight();
stateStack.add(new TurtleState());
state.dir = new Direction(1.0, 0.0);
state.pos = new Point(screen.getWidth() / 2, screen.getHeight() / 2);
}
@Override
@SuppressWarnings("unchecked")
public <T> T as(Class<T> clazz) {
if (clazz == GraphicsContext.class)
return (T) context;
else
return null;
}
@Override
public void clear() {
context.clearRect(0, 0, getWidth(), getHeight());
}
@Override
public void debugTurtle() {
System.err.println("[" + state.pos + " " + state.dir + "]");
}
@Override
public IShape shape() {
ShapePainter s = new ShapePainter(context);
return s.at(getPos()).rotation(getAngle()).strokePaint(state.ink);
}
@Override
public ITurtle line(Point to) {
// context.save();
context.setStroke(state.ink);
context.setLineWidth(state.penSize);
context.strokeLine(state.pos.getX(), state.pos.getY(), to.getX(), to.getY());
// context.restore();
return this;
}
@Override
public double getAngle() {
return state.dir.toDegrees();
}
@Override
public Direction getDirection() {
return state.dir;
}
public double getHeight() {
return screen.getHeight();
}
public Screen getScreen() {
return screen;
}
@Override
public Point getPos() {
return state.pos;
}
public double getWidth() {
return screen.getWidth();
}
public ITurtle curveTo(Point to, double startControl, double endAngle, double endControl) {
Point c1 = state.pos.move(state.dir, startControl);
Point c2 = to.move(Direction.fromDegrees(endAngle + 180), endControl);
@ -123,6 +102,64 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
return this;
}
@Override
public void debugTurtle() {
System.err.println("[" + state.pos + " " + state.dir + "]");
}
@Override
public ITurtle draw(double dist) {
Point to = state.pos.move(state.dir, dist);
return drawTo(to);
}
@Override
public ITurtle drawTo(double x, double y) {
Point to = new Point(x, y);
return drawTo(to);
}
@Override
public ITurtle drawTo(Point to) {
if (path) {
context.lineTo(to.getX(), to.getY());
} else {
line(to);
}
state.inDir = state.dir;
state.pos = to;
return this;
}
@Override
public double getAngle() {
return state.dir.toDegrees();
}
@Override
public Direction getDirection() {
return state.dir;
}
@Override
public double getHeight() {
return height;
}
@Override
public Point getPos() {
return state.pos;
}
public Screen getScreen() {
return screen;
}
@Override
public double getWidth() {
return width;
}
@Override
public ITurtle jump(double dist) {
state.inDir = state.dir;
@ -145,26 +182,24 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
}
@Override
public ITurtle draw(double dist) {
Point to = state.pos.move(state.dir, dist);
return drawTo(to);
public void layerToBack() {
if (screen != null)
screen.moveToBack(this);
}
@Override
public ITurtle drawTo(double x, double y) {
Point to = new Point(x, y);
return drawTo(to);
public void layerToFront() {
if (screen != null)
screen.moveToFront(this);
}
@Override
public ITurtle drawTo(Point to) {
if (path) {
context.lineTo(to.getX(), to.getY());
} else {
line(to);
}
state.inDir = state.dir;
state.pos = to;
public ITurtle line(Point to) {
// context.save();
context.setStroke(state.ink);
context.setLineWidth(state.penSize);
context.strokeLine(state.pos.getX(), state.pos.getY(), to.getX(), to.getY());
// context.restore();
return this;
}
@ -196,6 +231,12 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
return this;
}
@Override
public IShape shape() {
ShapePainter s = new ShapePainter(context);
return s.at(getPos()).rotation(getAngle()).strokePaint(state.ink);
}
@Override
public ITurtle turn(double degrees) {
state.dir = state.dir.turn(degrees);
@ -245,29 +286,12 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
return this;
}
@Override
public void layerToFront() {
screen.moveToFront(this);
}
@Override
public void layerToBack() {
screen.moveToBack(this);
}
@Override
public ITurtle turtle() {
TurtlePainter painter = new TurtlePainter(screen, canvas);
TurtlePainter painter = screen != null ? new TurtlePainter(screen, canvas)
: new TurtlePainter(width, height, canvas);
painter.stateStack.set(0, new TurtleState(state));
return painter;
}
@SuppressWarnings("unchecked")
public <T> T as(Class<T> clazz) {
if (clazz == GraphicsContext.class)
return (T) context;
else
return null;
}
}

View File

@ -6,10 +6,26 @@ import java.util.List;
import java.util.function.BiFunction;
public class BlocksAndBoxes {
public enum PixelOrder implements Iterable<Integer> {
LEFT_TO_RIGHT(8, 4, 2, 1), RIGHT_TO_LEFT(4, 8, 1, 2), LEFT_TO_RIGHT_UPWARDS(2, 1, 8,
4), RIGHT_TO_LEFT_UPWARDS(1, 2, 4, 8);
private List<Integer> order;
private PixelOrder(int a, int b, int c, int d) {
order = Arrays.asList(a, b, c, d);
}
@Override
public Iterator<Integer> iterator() {
return order.iterator();
}
}
public static final String[] unicodeBlocks = { " ", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "" };
public static final int[] unicodeBlocks_NumPixels = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 2 };
public static final int[] unicodeBlocks_NumPixels = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 2 };
public static final String unicodeBlocksString = String.join("", unicodeBlocks);
public static final String BLOCK_EMPTY = " ";
public static final String BLOCK_BOTTOM_RIGHT = "";
@ -27,7 +43,19 @@ public class BlocksAndBoxes {
public static final String BLOCK_REVERSE_BOTTOM_LEFT = "";
public static final String BLOCK_REVERSE_BOTTOM_RIGHT = "";
public static final String BLOCK_FULL = "";
public static final String BLOCK_HALF = "";
public static final String BLOCK_HALF = "";;
public static String blockAddOne(String s, PixelOrder order) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i >= 0) {
for (int bit : order) {
if ((i & bit) == 0)
return unicodeBlocks[i | bit];
}
}
return s;
}
/**
* Convert a string into a Unicode block graphics character.
@ -122,7 +150,20 @@ public class BlocksAndBoxes {
}
throw new IllegalArgumentException(
"Expected length 4 string of \" \" and \"*\", or \"++++\", got \"" + s + "\"");
};
}
public static String blockCompact(String s) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i > 0) {
int lower = i & 3;
int upper = (i >> 2) & 3;
i = (lower | upper) | ((lower & upper) << 2);
// System.out.println("Compact: " + s + " -> " + BlocksAndBoxes.unicodeBlocks[i]
// + "\n");
return BlocksAndBoxes.unicodeBlocks[i];
}
return s;
}
public static String blockCompose(String b1, String b2, BiFunction<Integer, Integer, Integer> op) {
int i1 = unicodeBlocksString.indexOf(b1);
@ -146,33 +187,6 @@ public class BlocksAndBoxes {
return unicodeBlocks[op.apply(i1, i2)];
}
public enum PixelOrder implements Iterable<Integer> {
LEFT_TO_RIGHT(8, 4, 2, 1), RIGHT_TO_LEFT(4, 8, 1, 2), LEFT_TO_RIGHT_UPWARDS(2, 1, 8,
4), RIGHT_TO_LEFT_UPWARDS(1, 2, 4, 8);
private List<Integer> order;
private PixelOrder(int a, int b, int c, int d) {
order = Arrays.asList(a, b, c, d);
}
@Override
public Iterator<Integer> iterator() {
return order.iterator();
}
}
public static String blockAddOne(String s, PixelOrder order) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i >= 0) {
for (int bit : order) {
if ((i & bit) == 0)
return unicodeBlocks[i | bit];
}
}
return s;
}
public static String blockRemoveOne(String s, PixelOrder order) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i >= 0) {
@ -184,17 +198,4 @@ public class BlocksAndBoxes {
return s;
}
public static String blockCompact(String s) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i > 0) {
int lower = i & 3;
int upper = (i >> 2) & 3;
i = (lower | upper) | ((lower & upper) << 2);
// System.out.println("Compact: " + s + " -> " + BlocksAndBoxes.unicodeBlocks[i]
// + "\n");
return BlocksAndBoxes.unicodeBlocks[i];
}
return s;
}
}

View File

@ -19,7 +19,6 @@ public class DemoPages {
printer.println(s);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

View File

@ -36,6 +36,16 @@ public class Printer implements IPaintLayer {
4.0000, -8.5000, 1.5000, 1.0000, true);
public static final TextFont FONT_ZXSPECTRUM7 = new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE,
3.1000, -3.8000, 1.0000, 1.0000, true);
/**
* TTF file can be found here: http://users.teilar.gr/~g1951d/ in this ZIP file:
* http://users.teilar.gr/~g1951d/Symbola.zip
* <p>
* (Put the extracted Symbola.ttf in src/inf101/v18/gfx/fonts/)
*/
public static final TextFont FONT_SYMBOLA = new TextFont("Symbola.ttf", 26.70, TextMode.CHAR_BOX_SIZE, -0.4000,
-7.6000, 1.35000, 1.0000, true);
/**
* TTF file can be found here:
* http://www.kreativekorp.com/software/fonts/c64.shtml
@ -49,8 +59,6 @@ public class Printer implements IPaintLayer {
*/
public static final TextFont FONT_C64 = new TextFont("PetMe64.ttf", 31.50, TextMode.CHAR_BOX_SIZE, 0.0000, -4.000,
1.0000, 1.0000, true);
Color DEFAULT_FILL = Color.BLACK;
Color DEFAULT_STROKE = Color.TRANSPARENT;
private static final Paint DEFAULT_BACKGROUND = Color.TRANSPARENT;
private static final TextMode DEFAULT_MODE = TextMode.MODE_40X22;
@ -67,6 +75,10 @@ public class Printer implements IPaintLayer {
return r;
}
Color DEFAULT_FILL = Color.BLACK;
Color DEFAULT_STROKE = Color.TRANSPARENT;
private TextMode textMode;
private Color fill;
private Color stroke;
@ -88,6 +100,10 @@ public class Printer implements IPaintLayer {
private int csiMode = 0;
public Printer(double width, double height) {
this(null, new Canvas(width, height));
}
public Printer(Screen screen, Canvas page) {
this.screen = screen;
this.textPage = page;
@ -142,17 +158,6 @@ public class Printer implements IPaintLayer {
});
}
private void drawChar(int x, int y, Char c) {
if (c != null) {
GraphicsContext context = textPage.getGraphicsContext2D();
context.setFill(c.fill);
context.setStroke(c.stroke);
font.drawTextAt(context, (x - 1) * getCharWidth(), y * getCharHeight(), c.s,
textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode, c.bg);
}
}
private String addToCsiBuffer(String s) {
if (csiMode == 1) {
switch (s) {
@ -207,6 +212,7 @@ public class Printer implements IPaintLayer {
y = topMargin;
}
@Override
public void clear() {
print("\f");
}
@ -215,6 +221,31 @@ public class Printer implements IPaintLayer {
printAt(x, y, " ");
}
public void clearLine(int y) {
y = constrainY(y);
if (y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
Arrays.fill(lineBuffer.get(y - 1), null);
redrawTextPage();
}
}
public void clearRegion(int x, int y, int width, int height) {
if (x > getLineWidth() || y > getPageHeight())
return;
int x2 = Math.min(x + width - 1, getLineWidth());
int y2 = Math.min(y + height - 1, getPageHeight());
if (x2 < 1 || y2 < 1)
return;
int x1 = Math.max(1, x);
int y1 = Math.max(1, y);
// Char fillWith = new Char("*", Color.BLACK, Color.GREEN, Color.TRANSPARENT,
// 0);
for (int i = y1; i <= y2; i++) {
Arrays.fill(lineBuffer.get(i - 1), x1 - 1, x2, null);
}
redrawTextPage();
}
private int constrainX(int x) {
return x; // Math.min(LINE_WIDTH_HIRES, Math.max(1, x));
}
@ -249,26 +280,52 @@ public class Printer implements IPaintLayer {
public void cycleMode(boolean adjustDisplayAspect) {
textMode = textMode.nextMode();
if (adjustDisplayAspect)
if (adjustDisplayAspect && screen != null)
screen.setAspect(textMode.getAspect());
redrawTextPage();
}
public void drawCharCells() {
GraphicsContext context = screen.getBackgroundContext();
screen.clearBackground();
double w = getCharWidth();
double h = getCharHeight();
context.save();
context.setGlobalBlendMode(BlendMode.EXCLUSION);
context.setFill(Color.WHITE.deriveColor(0.0, 1.0, 1.0, 0.3));
for (int x = 0; x < getLineWidth(); x++) {
for (int y = 0; y < getPageHeight(); y++) {
if ((x + y) % 2 == 0)
context.fillRect(x * w, y * h, w, h);
}
private void drawChar(int x, int y, Char c) {
if (c != null) {
GraphicsContext context = textPage.getGraphicsContext2D();
context.setFill(c.fill);
context.setStroke(c.stroke);
font.drawTextAt(context, (x - 1) * getCharWidth(), y * getCharHeight(), c.s,
textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode, c.bg);
}
context.restore();
}
public void drawCharCells() {
if (screen != null) {
GraphicsContext context = screen.getBackgroundContext();
screen.clearBackground();
double w = getCharWidth();
double h = getCharHeight();
context.save();
context.setGlobalBlendMode(BlendMode.EXCLUSION);
context.setFill(Color.WHITE.deriveColor(0.0, 1.0, 1.0, 0.1));
for (int x = 0; x < getLineWidth(); x++) {
for (int y = 0; y < getPageHeight(); y++) {
if ((x + y) % 2 == 0)
context.fillRect(x * w, y * h, w, h);
}
}
context.restore();
}
}
public Color getBackground(int x, int y) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
Color bg = Color.TRANSPARENT;
if (c != null && c.bg instanceof Color)
bg = (Color) c.bg;
else if (background instanceof Color)
bg = (Color) background;
return bg;
}
public boolean getBold() {
@ -305,41 +362,6 @@ public class Printer implements IPaintLayer {
return fill;
}
public Color getBackground(int x, int y) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
Color bg = Color.TRANSPARENT;
if (c != null && c.bg instanceof Color)
bg = (Color) c.bg;
else if (background instanceof Color)
bg = (Color) background;
return bg;
}
public void setBackground(int x, int y, Paint bg) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.bg = bg;
drawChar(x, y, c);
}
}
public void setColor(int x, int y, Color fill) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.fill = fill;
drawChar(x, y, c);
}
}
public TextFont getFont() {
return font;
}
@ -367,6 +389,10 @@ public class Printer implements IPaintLayer {
return (videoAttrs & TextFont.ATTR_INVERSE) != 0;
}
public TextMode getTextMode() {
return textMode;
}
/**
* @return the topMargin
*/
@ -390,6 +416,20 @@ public class Printer implements IPaintLayer {
return !getChar(x, y).equals(" ");
}
@Override
public void layerToBack() {
if (screen != null) {
screen.moveToBack(this);
}
}
@Override
public void layerToFront() {
if (screen != null) {
screen.moveToFront(this);
}
}
public void move(int deltaX, int deltaY) {
x = constrainX(x + deltaX);
y = constrainYOrScroll(y + deltaY);
@ -550,6 +590,17 @@ public class Printer implements IPaintLayer {
return old;
}
public void setBackground(int x, int y, Paint bg) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.bg = bg;
drawChar(x, y, c);
}
}
public void setBackground(Paint bgColor) {
this.background = bgColor != null ? bgColor : DEFAULT_BACKGROUND;
}
@ -570,6 +621,17 @@ public class Printer implements IPaintLayer {
return null;
}
public void setColor(int x, int y, Color fill) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.fill = fill;
drawChar(x, y, c);
}
}
public void setFill(Color fill) {
this.fill = fill != null ? fill : DEFAULT_FILL;
}
@ -615,6 +677,19 @@ public class Printer implements IPaintLayer {
this.stroke = stroke != null ? stroke : DEFAULT_STROKE;
}
public void setTextMode(TextMode mode) {
setTextMode(mode, false);
}
public void setTextMode(TextMode mode, boolean adjustDisplayAspect) {
if (mode == null)
throw new IllegalArgumentException();
textMode = mode;
if (adjustDisplayAspect && screen != null)
screen.setAspect(textMode.getAspect());
redrawTextPage();
}
public void setTopMargin() {
this.topMargin = y;
}
@ -627,10 +702,6 @@ public class Printer implements IPaintLayer {
this.topMargin = constrainY(topMargin);
}
public void setVideoAttrs(int attr) {
videoAttrs = attr;
}
public void setVideoAttrDisabled(int attr) {
videoAttrs &= ~attr;
}
@ -639,42 +710,12 @@ public class Printer implements IPaintLayer {
videoAttrs |= attr;
}
public void setTextMode(TextMode mode) {
setTextMode(mode, false);
}
public void setTextMode(TextMode mode, boolean adjustDisplayAspect) {
if (mode == null)
throw new IllegalArgumentException();
textMode = mode;
if (adjustDisplayAspect)
screen.setAspect(textMode.getAspect());
redrawTextPage();
}
public TextMode getTextMode() {
return textMode;
public void setVideoAttrs(int attr) {
videoAttrs = attr;
}
public void unplot(int x, int y) {
plot(x, y, (a, b) -> a & ~b);
}
@Override
public void layerToFront() {
screen.moveToFront(this);
}
@Override
public void layerToBack() {
screen.moveToBack(this);
}
public void clearLine(int y) {
y = constrainY(y);
if (y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
Arrays.fill(lineBuffer.get(y - 1), null);
redrawTextPage();
}
}
}

View File

@ -19,6 +19,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
/**
* TextFont for grid-based text / character graphics
@ -935,7 +936,7 @@ public class TextFont {
target.save(); // save 3
if ((mode & ATTR_ITALIC) != 0) {
target.translate(-0.2, 0);
target.transform(new Affine(Affine.shear(-0.2, 0)));
target.transform(new Affine(Transform.shear(-0.2, 0)));
}
setGraphicsContext(target, xScaleFactor);
if (fill != null)

View File

@ -9,7 +9,7 @@ import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class TextFontAdjuster extends Application {
private static final String FONT_NAME = "PetMe64.ttf";
// private static final String FONT_NAME = "PetMe64.ttf";
// new TextFont(FONT_NAME, 22.2, TextModes.CHAR_BOX_SIZE, 0.0, 0.0, 1.0, 1.0);
private static TextFontAdjuster demo;
@ -21,8 +21,9 @@ public class TextFontAdjuster extends Application {
launch(args);
}
private TextFont textFont = new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, 3.1000, -3.8000, 1.0000,
1.0000, true);
private TextFont textFont = Printer.FONT_SYMBOLA;//
// new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, 3.1000,
// -3.8000, 1.0000, 1.0000, true);
private Screen screen;
private boolean paused;
@ -107,11 +108,9 @@ public class TextFontAdjuster extends Application {
textFont.getFont().getSize()));
printer.println(String.format(" xTr=%-1.1f yTr=%-1.1f xSc=%-1.1f ySc=%-1.1f ", textFont.getxTranslate(),
textFont.getyTranslate(), textFont.getxScale(), textFont.getyScale()));
// System.out.printf("new TextFont(\"%s\", %1.2f, Printer.CHAR_HEIGHT, %1.4f,
// %1.4f, %1.4f, %1.4f)%n", FONT_NAME,
// textFont.getSize(), textFont.getxTranslate(), textFont.getyTranslate(),
// textFont.getxScale(),
// textFont.getyScale());
System.out.printf("new TextFont(\"%s\", %1.2f, Printer.CHAR_HEIGHT, %1.4f, %1.4f, %1.4f, %1.4f)%n",
textFont.getFont().getName(), textFont.getSize(), textFont.getxTranslate(), textFont.getyTranslate(),
textFont.getxScale(), textFont.getyScale());
printer.moveTo(1, 15);
}

View File

@ -43,17 +43,15 @@ public interface IArea extends Iterable<ILocation> {
boolean equals(Object other);
/**
* Get a location object corresponding to (x,y)
* Convert a 1D coordinate to a location
* <p>
* Returns a location <code>l = fromIndex(i)</code> such that
* <code>toIndex(l.getX(), l.getY()) == i</code>.
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return The location object associated with (x,y)
* @throws IndexOutOfBoundsException
* if {@link #contains(int, int)} returns false for (x,y)
* @param i
* @return A location
*/
ILocation location(int x, int y);
ILocation fromIndex(int i);
/** @return Height of the area */
int getHeight();
@ -71,6 +69,32 @@ public interface IArea extends Iterable<ILocation> {
@Override
int hashCode();
/**
* Get a location object corresponding to (x,y)
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return The location object associated with (x,y)
* @throws IndexOutOfBoundsException
* if {@link #contains(int, int)} returns false for (x,y)
*/
ILocation location(int x, int y);
/**
* Get all locations in area
* <p>
* Since IArea is @{@link Iterable}, you can also use directly in a for-loop to
* iterate over the locations.
* <p>
* All locations in the list are guaranteed to be valid according to
* {@link #isValid(ILocation)}. The returned list cannot be modified.
*
* @return An unmodifiable list with all the locations in the area
*/
List<ILocation> locations();
/**
* Return an object for iterating over all the neighbours of the given position,
* suitable for use in a new-style for-loop.
@ -91,6 +115,23 @@ public interface IArea extends Iterable<ILocation> {
*/
Iterable<ILocation> neighboursOf(ILocation pos);
/** @return A (possibly) parallel {@link Stream} of all locations in the area */
Stream<ILocation> parallelStream();
/** @return A {@link Stream} of all locations in the area */
Stream<ILocation> stream();
/**
* Convert a 2D coordinate to 1D
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return x + y*getWidth()
*/
int toIndex(int x, int y);
@Override
String toString();
@ -117,45 +158,4 @@ public interface IArea extends Iterable<ILocation> {
* @return True if the area wraps around vertically
*/
boolean wrapsVertically();
/** @return A {@link Stream} of all locations in the area */
Stream<ILocation> stream();
/** @return A (possibly) parallel {@link Stream} of all locations in the area */
Stream<ILocation> parallelStream();
/**
* Convert a 2D coordinate to 1D
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return x + y*getWidth()
*/
int toIndex(int x, int y);
/**
* Convert a 1D coordinate to a location
* <p>
* Returns a location <code>l = fromIndex(i)</code> such that
* <code>toIndex(l.getX(), l.getY()) == i</code>.
*
* @param i
* @return A location
*/
ILocation fromIndex(int i);
/**
* Get all locations in area
* <p>
* Since IArea is @{@link Iterable}, you can also use directly in a for-loop to
* iterate over the locations.
* <p>
* All locations in the list are guaranteed to be valid
* according to {@link #isValid(ILocation)}. The returned list cannot be modified.
*
* @return An unmodifiable list with all the locations in the area
*/
List<ILocation> locations();
}

View File

@ -13,123 +13,19 @@ public interface IGrid<T> extends Iterable<T> {
IGrid<T> copy();
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
T get(int x, int y);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
T get(ILocation pos);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(int x, int y, T defaultResult);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(ILocation pos, T defaultResult);
/** @return The height of the grid. */
int getHeight();
/** @return The width of the grid. */
int getWidth();
/**
* Check if coordinates are valid.
* Create a parallel {@link Stream} with all the elements in this grid.
*
* Valid coordinates are 0 <= pos.getX() < getWidth(), 0 <= pos.getY() <
* getHeight().
*
* @param pos
* A position
* @return true if the position is within the grid
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
*/
boolean isValid(ILocation pos);
Stream<T> elementParallelStream();
/**
* Check if coordinates are valid.
* Create a {@link Stream} with all the elements in this grid.
*
* Valid coordinates are 0 <= x < getWidth(), 0 <= y < getHeight().
*
* @param x
* an x coordinate
* @param y
* an y coordinate
* @return true if the (x,y) position is within the grid
* @return A stream
*/
boolean isValid(int x, int y);
/**
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
void set(int x, int y, T element);
/**
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
void set(ILocation pos, T element);
Stream<T> elementStream();
/**
* Initialise the contents of all cells using an initialiser function.
@ -161,6 +57,108 @@ public interface IGrid<T> extends Iterable<T> {
*/
void fill(T element);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
T get(ILocation pos);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
T get(int x, int y);
IArea getArea();
/** @return The height of the grid. */
int getHeight();
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(ILocation pos, T defaultResult);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(int x, int y, T defaultResult);
/** @return The width of the grid. */
int getWidth();
/**
* Check if coordinates are valid.
*
* Valid coordinates are 0 <= pos.getX() < getWidth(), 0 <= pos.getY() <
* getHeight().
*
* @param pos
* A position
* @return true if the position is within the grid
*/
boolean isValid(ILocation pos);
/**
* Check if coordinates are valid.
*
* Valid coordinates are 0 <= x < getWidth(), 0 <= y < getHeight().
*
* @param x
* an x coordinate
* @param y
* an y coordinate
* @return true if the (x,y) position is within the grid
*/
boolean isValid(int x, int y);
/**
* Create a parallel {@link Stream} with all the locations in this grid.
* <p>
* All locations obtained through the stream are guaranteed to be valid
* according to {@link #isValid(ILocation)}.
*
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
*/
Stream<ILocation> locationParallelStream();
/**
* Iterate over all grid locations
* <p>
@ -185,31 +183,33 @@ public interface IGrid<T> extends Iterable<T> {
Stream<ILocation> locationStream();
/**
* Create a parallel {@link Stream} with all the locations in this grid.
* <p>
* All locations obtained through the stream are guaranteed to be valid
* according to {@link #isValid(ILocation)}.
*
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
Stream<ILocation> locationParallelStream();
void set(ILocation pos, T element);
/**
* Create a {@link Stream} with all the elements in this grid.
*
* @return A stream
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
Stream<T> elementStream();
/**
* Create a parallel {@link Stream} with all the elements in this grid.
*
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
*/
Stream<T> elementParallelStream();
IArea getArea();
void set(int x, int y, T element);
}

View File

@ -32,6 +32,21 @@ import java.util.List;
*/
public interface ILocation extends IPosition {
/**
* Iterate over neighbours in eight directions.
* <p>
* (The iterator may yield fewer than eight locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the eight cardinal and intercardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST},
* {@link GridDirection#NORTHEAST}, @link
* GridDirection#NORTHWEST}, @link GridDirection#SOUTHEAST}, @link
* GridDirection#SOUTHWEST}, )
*/
Collection<ILocation> allNeighbours();
/**
* Checks whether you can go towards direction dir without going outside the
* area bounds
@ -41,6 +56,22 @@ public interface ILocation extends IPosition {
*/
boolean canGo(GridDirection dir);
/**
* Iterate over north/south/east/west neighbours.
* <p>
* (The iterator may yield fewer than four locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the four cardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST}
*/
Collection<ILocation> cardinalNeighbours();
IArea getArea();
int getIndex();
/**
* Return the next location in direction dir.
* <p>
@ -56,37 +87,6 @@ public interface ILocation extends IPosition {
*/
ILocation go(GridDirection dir);
/**
* Iterate over north/south/east/west neighbours.
* <p>
* (The iterator may yield fewer than four locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the four cardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST}
*/
Collection<ILocation> cardinalNeighbours();
/**
* Iterate over neighbours in eight directions.
* <p>
* (The iterator may yield fewer than eight locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the eight cardinal and intercardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST},
* {@link GridDirection#NORTHEAST}, @link
* GridDirection#NORTHWEST}, @link GridDirection#SOUTHEAST}, @link
* GridDirection#SOUTHWEST}, )
*/
Collection<ILocation> allNeighbours();
IArea getArea();
int getIndex();
/**
* Find the grid cells between this location (exclusive) and another location
* (inclusive).

View File

@ -5,6 +5,20 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
public interface IMultiGrid<T> extends IGrid<List<T>> {
/**
* Add to the cell at the given location.
*
* @param loc
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to be added to the cell.
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default void add(ILocation loc, T element) {
get(loc).add(element);
}
/**
* Add to the cell at the given x,y location.
*
@ -23,35 +37,23 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
}
/**
* Add to the cell at the given location.
* Check if a cell contains an element.
*
*
* @param loc
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to be added to the cell.
* The (x,y) position of the grid cell
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default void add(ILocation loc, T element) {
get(loc).add(element);
}
/**
* Check if a cell contains an element.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to search for.
* @return true if element is at the given location
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default boolean contains(int x, int y, T element) {
return get(x, y).contains(element);
default boolean contains(ILocation loc, Predicate<T> predicate) {
for (T t : get(loc)) {
if (predicate.test(t))
return true;
}
return false;
}
/**
@ -91,6 +93,24 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
/**
* Check if a cell contains an element.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to search for.
* @return true if element is at the given location
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default boolean contains(int x, int y, T element) {
return get(x, y).contains(element);
}
/**
* Get all elements in a cell that match the predicate
*
*
* @param loc
* The (x,y) position of the grid cell
@ -100,30 +120,44 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default boolean contains(ILocation loc, Predicate<T> predicate) {
for (T t : get(loc)) {
if (predicate.test(t))
return true;
}
return false;
default List<T> get(ILocation loc, Predicate<T> predicate) {
return get(loc).stream().filter(predicate).collect(Collectors.toList());
}
/**
* Remove an element from the cell at the given x,y location.
* Check if a cell contains an element.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell
* @param element
* An element to be removed from the cell
* @return Number of elements removed
* The (x,y) position of the grid cell to get the contents of.
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default int remove(int x, int y, T element) {
return get(x, y).remove(element) ? 1 : 0;
default List<T> get(int x, int y, Predicate<T> predicate) {
return get(this.getArea().location(x, y), predicate);
}
/**
* Remove an element from the cell at the given location.
*
* @param loc
* The location of the grid cell
* @param predicate
* Predicate which should be true for elements to be removed
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default int remove(ILocation loc, Predicate<T> predicate) {
List<T> list = get(loc);
int s = list.size();
get(loc).removeIf(predicate);
return s - list.size();
}
/**
@ -160,55 +194,21 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
}
/**
* Remove an element from the cell at the given location.
*
* @param loc
* The location of the grid cell
* @param predicate
* Predicate which should be true for elements to be removed
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default int remove(ILocation loc, Predicate<T> predicate) {
List<T> list = get(loc);
int s = list.size();
get(loc).removeIf(predicate);
return s - list.size();
}
/**
* Check if a cell contains an element.
* Remove an element from the cell at the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* The (x,y) position of the grid cell
* @param element
* An element to be removed from the cell
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default List<T> get(int x, int y, Predicate<T> predicate) {
return get(this.getArea().location(x, y), predicate);
}
/**
* Get all elements in a cell that match the predicate
*
*
* @param loc
* The (x,y) position of the grid cell
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default List<T> get(ILocation loc, Predicate<T> predicate) {
return get(loc).stream().filter(predicate).collect(Collectors.toList());
default int remove(int x, int y, T element) {
return get(x, y).remove(element) ? 1 : 0;
}
}

View File

@ -2,6 +2,42 @@ package inf101.v18.grid;
public interface IPosition {
/**
* @param obj
* Another object
* @return true if obj is also an IPosition, and the x and y coordinates are
* equal
*/
@Override
boolean equals(Object obj);
/**
* Find the Euclidian distance between the midpoint of this position and another
* position.
*
* The distance is computed with the Pythagorean formula, with the assumption
* that the grid cells are square shaped with <em>width</em> = <em>height</em> =
* 1. For example, the distance from (0,0) to (3,5) is √(3²+5²) = 5.83.
*
* @param other
* @return Euclidian distance between this and other's midpoints
*/
double geometricDistanceTo(IPosition other);
/**
* Gets the x-coordinate
*
* @return
*/
int getX();
/**
* Gets the y-coordinate
*
* @return
*/
int getY();
/**
* Find the distance in grid cells to another location.
*
@ -19,6 +55,9 @@ public interface IPosition {
*/
int gridDistanceTo(IPosition other);
@Override
int hashCode();
/**
* Find the number of non-diagonal steps needed to go from this location the
* other location.
@ -37,44 +76,8 @@ public interface IPosition {
*/
int stepDistanceTo(IPosition other);
/**
* Find the Euclidian distance between the midpoint of this position and another
* position.
*
* The distance is computed with the Pythagorean formula, with the assumption
* that the grid cells are square shaped with <em>width</em> = <em>height</em> =
* 1. For example, the distance from (0,0) to (3,5) is √(3²+5²) = 5.83.
*
* @param other
* @return Euclidian distance between this and other's midpoints
*/
double geometricDistanceTo(IPosition other);
/**
* @param obj
* Another object
* @return true if obj is also an IPosition, and the x and y coordinates are
* equal
*/
boolean equals(Object obj);
/**
* Gets the x-coordinate
*
* @return
*/
int getX();
/**
* Gets the y-coordinate
*
* @return
*/
int getY();
int hashCode();
/** @return Position as a string, "(x,y)" */
@Override
String toString();
}

View File

@ -11,18 +11,6 @@ public class MyGrid<T> implements IGrid<T> {
private final IArea area;
private final List<T> cells;
/**
* Construct a grid with the given dimensions.
*
* @param width
* @param height
* @param initElement
* What the cells should initially hold (possibly null)
*/
public MyGrid(int width, int height, T initElement) {
this(new RectArea(width, height), initElement);
}
/**
* Construct a grid with the given dimensions.
*
@ -40,8 +28,16 @@ public class MyGrid<T> implements IGrid<T> {
* @param initialiser
* The initialiser function
*/
public MyGrid(int width, int height, Function<ILocation, T> initialiser) {
this(new RectArea(width, height), initialiser);
public MyGrid(IArea area, Function<ILocation, T> initialiser) {
if (area == null || initialiser == null) {
throw new IllegalArgumentException();
}
this.area = area;
this.cells = new ArrayList<T>(area.getSize());
for (ILocation loc : area) {
cells.add(initialiser.apply(loc));
}
}
/**
@ -81,16 +77,20 @@ public class MyGrid<T> implements IGrid<T> {
* @param initialiser
* The initialiser function
*/
public MyGrid(IArea area, Function<ILocation, T> initialiser) {
if (area == null || initialiser == null) {
throw new IllegalArgumentException();
}
public MyGrid(int width, int height, Function<ILocation, T> initialiser) {
this(new RectArea(width, height), initialiser);
}
this.area = area;
this.cells = new ArrayList<T>(area.getSize());
for (ILocation loc : area) {
cells.add(initialiser.apply(loc));
}
/**
* Construct a grid with the given dimensions.
*
* @param width
* @param height
* @param initElement
* What the cells should initially hold (possibly null)
*/
public MyGrid(int width, int height, T initElement) {
this(new RectArea(width, height), initElement);
}
@Override
@ -101,79 +101,13 @@ public class MyGrid<T> implements IGrid<T> {
}
@Override
public T get(int x, int y) {
return cells.get(area.toIndex(x, y));
public Stream<T> elementParallelStream() {
return cells.parallelStream();
}
@Override
public int getHeight() {
return area.getHeight();
}
@Override
public int getWidth() {
return area.getWidth();
}
@Override
public void set(int x, int y, T elem) {
cells.set(area.toIndex(x, y), elem);
}
@Override
public Iterator<T> iterator() {
return cells.iterator();
}
@Override
public T get(ILocation loc) {
if (loc.getArea() == area)
return cells.get(loc.getIndex());
else
return cells.get(area.toIndex(loc.getX(), loc.getY()));
}
@Override
public T getOrDefault(int x, int y, T defaultResult) {
T r = null;
if (isValid(x, y))
r = get(x, y);
if (r != null)
return r;
else
return defaultResult;
}
@Override
public T getOrDefault(ILocation loc, T defaultResult) {
if (loc.getArea() == area) {
T r = cells.get(loc.getIndex());
if (r != null)
return r;
else
return defaultResult;
} else {
return getOrDefault(loc.getX(), loc.getY(), defaultResult);
}
}
@Override
public boolean isValid(ILocation loc) {
return loc.getArea() == area || area.contains(loc.getX(), loc.getY());
}
@Override
public boolean isValid(int x, int y) {
return area.contains(x, y);
}
@Override
public void set(ILocation loc, T element) {
if (loc.getArea() == area) {
cells.set(loc.getIndex(), element);
} else {
set(loc.getX(), loc.getY(), element);
}
public Stream<T> elementStream() {
return cells.stream();
}
@Override
@ -193,6 +127,78 @@ public class MyGrid<T> implements IGrid<T> {
}
}
@Override
public T get(ILocation loc) {
if (loc.getArea() == area)
return cells.get(loc.getIndex());
else
return cells.get(area.toIndex(loc.getX(), loc.getY()));
}
@Override
public T get(int x, int y) {
return cells.get(area.toIndex(x, y));
}
@Override
public IArea getArea() {
return area;
}
@Override
public int getHeight() {
return area.getHeight();
}
@Override
public T getOrDefault(ILocation loc, T defaultResult) {
if (loc.getArea() == area) {
T r = cells.get(loc.getIndex());
if (r != null)
return r;
else
return defaultResult;
} else {
return getOrDefault(loc.getX(), loc.getY(), defaultResult);
}
}
@Override
public T getOrDefault(int x, int y, T defaultResult) {
T r = null;
if (isValid(x, y))
r = get(x, y);
if (r != null)
return r;
else
return defaultResult;
}
@Override
public int getWidth() {
return area.getWidth();
}
@Override
public boolean isValid(ILocation loc) {
return loc.getArea() == area || area.contains(loc.getX(), loc.getY());
}
@Override
public boolean isValid(int x, int y) {
return area.contains(x, y);
}
@Override
public Iterator<T> iterator() {
return cells.iterator();
}
@Override
public Stream<ILocation> locationParallelStream() {
return area.parallelStream();
}
@Override
public Iterable<ILocation> locations() {
return area;
@ -204,22 +210,16 @@ public class MyGrid<T> implements IGrid<T> {
}
@Override
public Stream<T> elementStream() {
return cells.stream();
public void set(ILocation loc, T element) {
if (loc.getArea() == area) {
cells.set(loc.getIndex(), element);
} else {
set(loc.getX(), loc.getY(), element);
}
}
@Override
public IArea getArea() {
return area;
}
@Override
public Stream<ILocation> locationParallelStream() {
return area.parallelStream();
}
@Override
public Stream<T> elementParallelStream() {
return cells.parallelStream();
public void set(int x, int y, T elem) {
cells.set(area.toIndex(x, y), elem);
}
}

View File

@ -8,10 +8,164 @@ import java.util.List;
import java.util.stream.Stream;
public class RectArea implements IArea {
/** A class to represent an (x, y)-location on a grid. */
class Location implements ILocation {
/** value of the x-coordinate */
protected final int x;
/** value of the y-coordinate */
protected final int y;
protected final int idx;
protected final int edgeMask;
/**
* Main constructor. Initializes a new {@link #Location} objects with the
* corresponding values of x and y.
*
* @param x
* X coordinate
* @param y
* Y coordinate
* @param idx
* 1-dimensional index
* @param edgeMask
* mask with bits {@link RectArea#N}, {@link RectArea#S},
* {@link RectArea#E}, {@link RectArea#W} set if we're on the
* corresponding edge of the area
*/
Location(int x, int y, int idx, int edgeMask) {
this.x = x;
this.y = y;
this.idx = idx;
this.edgeMask = edgeMask;
}
@Override
public Collection<ILocation> allNeighbours() {
Collection<ILocation> ns = new ArrayList<>(8);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public boolean canGo(GridDirection dir) {
return (edgeMask & dir.getMask()) == 0;
}
@Override
public Collection<ILocation> cardinalNeighbours() {
Collection<ILocation> ns = new ArrayList<>(4);
for (GridDirection d : GridDirection.FOUR_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof IPosition)) {
return false;
}
IPosition other = (IPosition) obj;
if (x != other.getX()) {
return false;
}
if (y != other.getY()) {
return false;
}
return true;
}
@Override
public double geometricDistanceTo(IPosition other) {
return Math.sqrt(Math.pow(this.x - other.getX(), 2) + Math.pow(this.y - other.getY(), 2));
}
@Override
public IArea getArea() {
return RectArea.this;
}
@Override
public int getIndex() {
return idx;
}
@Override
public int getX() {
return x;
}
@Override
public int getY() {
return y;
}
@Override
public ILocation go(GridDirection dir) {
return location(x + dir.getDx(), y + dir.getDy());
}
@Override
public int gridDistanceTo(IPosition other) {
return Math.max(Math.abs(this.x - other.getX()), Math.abs(this.y - other.getY()));
}
@Override
public List<ILocation> gridLineTo(ILocation other) {
if (!contains(other))
throw new IllegalArgumentException();
int distX = other.getX() - x;
int distY = other.getY() - y;
int length = Math.max(Math.abs(distX), Math.abs(distY));
List<ILocation> line = new ArrayList<>(length);
if (length == 0)
return line;
double dx = (double) distX / (double) length;
double dy = (double) distY / (double) length;
// System.out.printf("dx=%g, dy=%g, length=%d%n", dx, dy, length);
for (int i = 1; i <= length; i++) {
line.add(location(x + (int) Math.round(dx * i), y + (int) Math.round(dy * i)));
}
return line;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public int stepDistanceTo(IPosition other) {
return Math.abs(this.x - other.getX()) + Math.abs(this.y - other.getY());
}
@Override
public String toString() {
return "(x=" + x + ",y=" + y + ")";
}
}
protected final int width;
protected final int height;
protected final int size;
protected final List<ILocation> locs;
protected final boolean hWrap, vWrap;
public RectArea(int width, int height) {
@ -86,15 +240,16 @@ public class RectArea implements IArea {
}
@Override
public int getHeight() {
return height;
public ILocation fromIndex(int i) {
if (i >= 0 && i < size)
return locs.get(i);
else
throw new IndexOutOfBoundsException("" + i);
}
@Override
public int toIndex(int x, int y) {
x = checkX(x);
y = checkY(y);
return y * width + x;
public int getHeight() {
return height;
}
@Override
@ -112,6 +267,41 @@ public class RectArea implements IArea {
return locs.iterator();
}
@Override
public ILocation location(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height)
throw new IndexOutOfBoundsException("(" + x + "," + y + ")");
int i = x + y * width;
return locs.get(i);
}
@Override
public List<ILocation> locations() {
return locs; // (OK since locs has been through Collections.unmodifiableList())
}
@Override
public Iterable<ILocation> neighboursOf(ILocation pos) {
return pos.allNeighbours();
}
@Override
public Stream<ILocation> parallelStream() {
return locs.parallelStream();
}
@Override
public Stream<ILocation> stream() {
return locs.stream();
}
@Override
public int toIndex(int x, int y) {
x = checkX(x);
y = checkY(y);
return y * width + x;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
@ -154,193 +344,4 @@ public class RectArea implements IArea {
}
}
/** A class to represent an (x, y)-location on a grid. */
class Location implements ILocation {
/** value of the x-coordinate */
protected final int x;
/** value of the y-coordinate */
protected final int y;
protected final int idx;
protected final int edgeMask;
/**
* Main constructor. Initializes a new {@link #Location} objects with the
* corresponding values of x and y.
*
* @param x
* X coordinate
* @param y
* Y coordinate
* @param idx
* 1-dimensional index
* @param edgeMask
* mask with bits {@link RectArea#N}, {@link RectArea#S},
* {@link RectArea#E}, {@link RectArea#W} set if we're on the
* corresponding edge of the area
*/
Location(int x, int y, int idx, int edgeMask) {
this.x = x;
this.y = y;
this.idx = idx;
this.edgeMask = edgeMask;
}
@Override
public int gridDistanceTo(IPosition other) {
return Math.max(Math.abs(this.x - other.getX()), Math.abs(this.y - other.getY()));
}
@Override
public int stepDistanceTo(IPosition other) {
return Math.abs(this.x - other.getX()) + Math.abs(this.y - other.getY());
}
@Override
public double geometricDistanceTo(IPosition other) {
return Math.sqrt(Math.pow(this.x - other.getX(), 2) + Math.pow(this.y - other.getY(), 2));
}
@Override
public List<ILocation> gridLineTo(ILocation other) {
if (!contains(other))
throw new IllegalArgumentException();
int distX = other.getX() - x;
int distY = other.getY() - y;
int length = Math.max(Math.abs(distX), Math.abs(distY));
List<ILocation> line = new ArrayList<>(length);
if (length == 0)
return line;
double dx = (double) distX / (double) length;
double dy = (double) distY / (double) length;
// System.out.printf("dx=%g, dy=%g, length=%d%n", dx, dy, length);
for (int i = 1; i <= length; i++) {
line.add(location(x + (int) Math.round(dx * i), y + (int) Math.round(dy * i)));
}
return line;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof IPosition)) {
return false;
}
IPosition other = (IPosition) obj;
if (x != other.getX()) {
return false;
}
if (y != other.getY()) {
return false;
}
return true;
}
@Override
public int getX() {
return x;
}
@Override
public int getY() {
return y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean canGo(GridDirection dir) {
return (edgeMask & dir.getMask()) == 0;
}
@Override
public ILocation go(GridDirection dir) {
return location(x + dir.getDx(), y + dir.getDy());
}
@Override
public String toString() {
return "(x=" + x + ",y=" + y + ")";
}
@Override
public Collection<ILocation> cardinalNeighbours() {
Collection<ILocation> ns = new ArrayList<>(4);
for (GridDirection d : GridDirection.FOUR_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public Collection<ILocation> allNeighbours() {
Collection<ILocation> ns = new ArrayList<>(8);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public IArea getArea() {
return RectArea.this;
}
@Override
public int getIndex() {
return idx;
}
}
@Override
public ILocation location(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height)
throw new IndexOutOfBoundsException("(" + x + "," + y + ")");
int i = x + y * width;
return locs.get(i);
}
@Override
public Stream<ILocation> stream() {
return locs.stream();
}
@Override
public Stream<ILocation> parallelStream() {
return locs.parallelStream();
}
@Override
public Iterable<ILocation> neighboursOf(ILocation pos) {
return pos.allNeighbours();
}
@Override
public ILocation fromIndex(int i) {
if (i >= 0 && i < size)
return locs.get(i);
else
throw new IndexOutOfBoundsException("" + i);
}
@Override
public List<ILocation> locations() {
return locs; // (OK since locs has been through Collections.unmodifiableList())
}
}

View File

@ -23,32 +23,6 @@ public class AreaRetting {
}
public void uniqueLocationsProperty(IArea area) {
Set<ILocation> set = new HashSet<>();
for (ILocation l : area) {
assertTrue("Location should be unique: " + l, set.add(l));
}
}
public void validLocationsProperty(IArea area) {
for (ILocation l : area) {
assertTrue("Location should be in area: " + l, area.contains(l));
assertTrue("Location should be in area: " + l, area.contains(l.getX(), l.getY()));
}
}
public void neighboursDistProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertEquals(1, loc.gridDistanceTo(l));
}
}
public void neighboursSymmetryProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertTrue("My neighbour should have me as a neighbour", l.allNeighbours().contains(loc));
}
}
public void canGoProperty(ILocation l, GridDirection dir) {
int x = l.getX() + dir.getDx();
int y = l.getY() + dir.getDy();
@ -61,33 +35,23 @@ public class AreaRetting {
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
uniqueLocationsProperty(area);
}
public void distanceProperty(ILocation l1, ILocation l2) {
assertEquals(l1.gridDistanceTo(l2), l2.gridDistanceTo(l1));
assertEquals(l1.stepDistanceTo(l2), l2.stepDistanceTo(l1));
assertTrue(l1.gridDistanceTo(l2) <= l1.stepDistanceTo(l2));
assertTrue(l1.gridDistanceTo(l2) <= l1.geometricDistanceTo(l2));
}
@Test
public void validLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
validLocationsProperty(area);
}
}
@Test
public void locationsTest() {
for (int i = 0; i < 10; i++) {
IArea area = areaGen.generate();
for (ILocation l : area) {
neighboursDistProperty(l);
neighboursSymmetryProperty(l);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS)
canGoProperty(l, d);
}
public void gridLineProperty(ILocation l1, ILocation l2) {
// System.out.println(l1.toString() + " .. " + l2.toString());
List<ILocation> line = l1.gridLineTo(l2);
assertEquals(l1.gridDistanceTo(l2), line.size());
ILocation last = l1;
for (ILocation l : line) {
assertEquals(1, last.gridDistanceTo(l));
last = l;
}
assertEquals(l2, last);
}
@Test
@ -104,22 +68,58 @@ public class AreaRetting {
}
}
public void gridLineProperty(ILocation l1, ILocation l2) {
// System.out.println(l1.toString() + " .. " + l2.toString());
List<ILocation> line = l1.gridLineTo(l2);
assertEquals(l1.gridDistanceTo(l2), line.size());
ILocation last = l1;
for (ILocation l : line) {
assertEquals(1, last.gridDistanceTo(l));
last = l;
@Test
public void locationsTest() {
for (int i = 0; i < 10; i++) {
IArea area = areaGen.generate();
for (ILocation l : area) {
neighboursDistProperty(l);
neighboursSymmetryProperty(l);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS)
canGoProperty(l, d);
}
}
assertEquals(l2, last);
}
public void distanceProperty(ILocation l1, ILocation l2) {
assertEquals(l1.gridDistanceTo(l2), l2.gridDistanceTo(l1));
assertEquals(l1.stepDistanceTo(l2), l2.stepDistanceTo(l1));
assertTrue(l1.gridDistanceTo(l2) <= l1.stepDistanceTo(l2));
assertTrue(l1.gridDistanceTo(l2) <= l1.geometricDistanceTo(l2));
public void neighboursDistProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertEquals(1, loc.gridDistanceTo(l));
}
}
public void neighboursSymmetryProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertTrue("My neighbour should have me as a neighbour", l.allNeighbours().contains(loc));
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
uniqueLocationsProperty(area);
}
}
public void uniqueLocationsProperty(IArea area) {
Set<ILocation> set = new HashSet<>();
for (ILocation l : area) {
assertTrue("Location should be unique: " + l, set.add(l));
}
}
@Test
public void validLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
validLocationsProperty(area);
}
}
public void validLocationsProperty(IArea area) {
for (ILocation l : area) {
assertTrue("Location should be in area: " + l, area.contains(l));
assertTrue("Location should be in area: " + l, area.contains(l.getX(), l.getY()));
}
}
}

View File

@ -19,37 +19,6 @@ public class GridRetting {
private IGenerator<String> strGen = new StringGenerator();
private IGenerator<IGrid<String>> gridGen = new GridGenerator<String>(strGen);
/** A set on (x1,y1) doesn't affect a get on a different (x2,y2) */
public <T> void setGetIndependentProperty(IGrid<T> grid, ILocation l1, ILocation l2, T val) {
if (!l1.equals(l2)) {
T s = grid.get(l2);
grid.set(l1, val);
assertEquals(s, grid.get(l2));
}
}
@Test
public void setGetIndependentTest() {
for (int j = 0; j < 10; j++) {
IGrid<String> grid = gridGen.generate();
IGenerator<ILocation> lGen = new LocationGenerator(grid.getArea());
for (int i = 0; i < N; i++) {
ILocation l1 = lGen.generate();
ILocation l2 = lGen.generate();
String s = strGen.generate();
setGetIndependentProperty(grid, l1, l2, s);
}
}
}
/** get(x,y) is val after set(x, y, val) */
public <T> void setGetProperty(IGrid<T> grid, ILocation l, T val) {
grid.set(l, val);
assertEquals(val, grid.get(l));
}
public <T> void fillProperty1(IGrid<T> grid, T val) {
grid.fill(val);
for (ILocation l : grid.locations()) {
@ -83,12 +52,37 @@ public class GridRetting {
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
/** A set on (x1,y1) doesn't affect a get on a different (x2,y2) */
public <T> void setGetIndependentProperty(IGrid<T> grid, ILocation l1, ILocation l2, T val) {
if (!l1.equals(l2)) {
T s = grid.get(l2);
grid.set(l1, val);
assertEquals(s, grid.get(l2));
}
}
@Test
public void setGetIndependentTest() {
for (int j = 0; j < 10; j++) {
IGrid<String> grid = gridGen.generate();
IGenerator<ILocation> lGen = new LocationGenerator(grid.getArea());
for (int i = 0; i < N; i++) {
ILocation l1 = lGen.generate();
ILocation l2 = lGen.generate();
String s = strGen.generate();
setGetIndependentProperty(grid, l1, l2, s);
}
}
}
/** get(x,y) is val after set(x, y, val) */
public <T> void setGetProperty(IGrid<T> grid, ILocation l, T val) {
grid.set(l, val);
assertEquals(val, grid.get(l));
}
/** Test that get gives back the same value after set. */
@Test
public void setGetTest() {
@ -105,4 +99,10 @@ public class GridRetting {
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
}
}
}

View File

@ -7,14 +7,26 @@ import inf101.v18.gfx.Screen;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.gfx.textmode.TextMode;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.game.Game;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
// you might want to tune these options
public static final boolean MAIN_USE_BACKGROUND_GRID = true;
public static final boolean MAP_AUTO_SCALE_ITEM_DRAW = true;
public static final boolean MAP_DRAW_ONLY_DIRTY_CELLS = false;
public static final TextMode MAIN_TEXT_MODE = TextMode.MODE_80X25;
public static final boolean DEBUG_TIME = false;
public static final int LINE_MAP_BOTTOM = 20;
public static final int LINE_STATUS = 21;
public static final int LINE_MSG1 = 22;
@ -23,23 +35,61 @@ public class Main extends Application {
public static final int LINE_DEBUG = 25;
public static final int COLUMN_MAP_END = 40;
public static final int COLUMN_RIGHTSIDE_START = 41;
private Screen screen;
private ITurtle painter;
private Printer printer;
private IGame game;
private boolean grid = true;
public static void main(String[] args) {
launch(args);
}
private Screen screen;
private ITurtle painter;
private Printer printer;
private Game game;
private boolean grid = MAIN_USE_BACKGROUND_GRID;
private boolean autoNextTurn = false;
private Timeline bigStepTimeline;
private Timeline smallStepTimeline;
private void setup() {
//
game = new Game(screen, painter, printer);
game.draw();
//
bigStepTimeline = new Timeline();
bigStepTimeline.setCycleCount(Timeline.INDEFINITE);
KeyFrame kf = new KeyFrame(Duration.millis(1000), (ActionEvent event) -> {
if (autoNextTurn) {
doTurn();
}
});
bigStepTimeline.getKeyFrames().add(kf);
// bigStepTimeline.playFromStart();
//
smallStepTimeline = new Timeline();
smallStepTimeline.setCycleCount(1);
kf = new KeyFrame(Duration.millis(1), (ActionEvent event) -> {
doTurn();
});
smallStepTimeline.getKeyFrames().add(kf);
// finally, start game
doTurn();
}
@Override
public void start(Stage primaryStage) throws Exception {
screen = Screen.startPaintScene(primaryStage, Screen.CONFIG_PIXELS_STEP_SCALED); // Screen.CONFIG_SCREEN_FULLSCREEN_NO_HINT);
printer = screen.createPrinter();
painter = screen.createPainter();
printer.setTextMode(TextMode.MODE_80X25, true);
printer.setTextMode(MAIN_TEXT_MODE, true);
// Font with emojis need separate download
// printer.setFont(Printer.FONT_SYMBOLA);
if (grid)
printer.drawCharCells();
printer.setAutoScroll(false);
@ -52,7 +102,7 @@ public class Main extends Application {
printer.cycleMode(true);
if (grid)
printer.drawCharCells();
// game.draw();
game.draw();
return true;
} else if (code == KeyCode.A) {
screen.cycleAspect();
@ -75,8 +125,7 @@ public class Main extends Application {
}
} else if (code == KeyCode.ENTER) {
try {
//game.doTurn();
// game.draw();
doTurn();
} catch (Exception e) {
printer.printAt(1, 25, "Exception: " + e.getMessage(), Color.RED);
e.printStackTrace();
@ -84,8 +133,8 @@ public class Main extends Application {
return true;
} else {
try {
// game.keyPressed(code);
// game.draw();
game.keyPressed(code);
doTurn();
} catch (Exception e) {
e.printStackTrace();
try {
@ -112,13 +161,24 @@ public class Main extends Application {
*/
setup();
// game = new Game(screen, painter, printer);
// game.draw();
primaryStage.show();
}
private void setup() {
public void doTurn() {
long t = System.currentTimeMillis();
boolean waitForPlayer = game.doTurn();
if (DEBUG_TIME)
System.out.println("doTurn() took " + (System.currentTimeMillis() - t) + "ms");
long t2 = System.currentTimeMillis();
game.draw();
if (DEBUG_TIME) {
System.out.println("draw() took " + (System.currentTimeMillis() - t2) + "ms");
System.out.println("doTurn()+draw() took " + (System.currentTimeMillis() - t) + "ms");
System.out.println("waiting for player? " + waitForPlayer);
}
if (!waitForPlayer)
smallStepTimeline.playFromStart(); // this will kickstart a new turn in a few milliseconds
}
public static String BUILTIN_MAP = "40 20\n" //

View File

@ -1,32 +1,132 @@
package inf101.v18.rogue101.events;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
/**
* Example implementation of events could be used to have more complex
* behaviour than just attack/defend/get damaged/pickup/drop.
*
* @author anya
*
* @param <T>
* Relevant extra data for this particular event
*/
public class GameEvent<T> implements IEvent<T> {
public static final String ATTACK_FAILURE = "attackFailure";
public static final String ATTACK_SUCCESS = "attackSuccess";
public static final String DEFEND_FAILURE = "defendFailure";
public static final String DEFEND_SUCCESS = "defendSuccess";
public static final String EATEN = "eaten";
private String name;
private IItem source;
private IItem target;
/**
* Create and send events for an attack.
* <p>
* Both attacker and defender will receive appropriate events (ATTACK_* or
* DEFEND_*), depending on who “won”. The amount of damage is available through
* {@link #getData()}.
* <p>
* Attacker will be sent the "damage" that was actually done (as returned by
* defender's event handler)
* <p>
* Methods such as these could sensible be placed in IGame/Game.
*
* @param success
* True if attack succeeded (attacker "won")
* @param attacker
* @param defender
* @param damage
*/
public static void triggerAttack(boolean success, IItem attacker, IItem defender, int damage) {
if (success) {
Integer result = defender
.handleEvent(new GameEvent<Integer>(DEFEND_FAILURE, null, attacker, defender, damage));
if (result != null)
damage = result;
attacker.handleEvent(new GameEvent<Integer>(ATTACK_SUCCESS, null, attacker, defender, damage));
} else {
attacker.handleEvent(new GameEvent<Integer>(ATTACK_FAILURE, null, attacker, defender, 0));
defender.handleEvent(new GameEvent<Integer>(DEFEND_SUCCESS, null, attacker, defender, 0));
}
}
private final String name;
private final IItem source;
private final IItem target;
private T value;
private final IGame game;
/**
* Create a new game event
* @param name The name is used when checking which event this is / determine its “meaning”
* @param source The item that caused the event, or <code>null</code> if unknown/not relevant
* @param target The item that receives the event, or <code>null</code> if unknown/not relevant
* @param value Arbitrary extra data
*
* @param name
* The name is used when checking which event this is / determine its
* “meaning”
* @param game
* The game, or <code>null</code> if unknown/not relevant
* @param source
* The item that caused the event, or <code>null</code> if
* unknown/not relevant
* @param target
* The item that receives the event, or <code>null</code> if
* unknown/not relevant
* @param value
* Arbitrary extra data
*/
public GameEvent(String name, IItem source, IItem target, T value) {
public GameEvent(String name, IGame game, IItem source, IItem target, T value) {
this.name = name;
this.game = game;
this.source = source;
this.target = target;
this.value = value;
}
/**
* Create a new game event
*
* @param name
* The name is used when checking which event this is / determine its
* “meaning”
* @param source
* The item that caused the event, or <code>null</code> if
* unknown/not relevant
*/
public GameEvent(String name, IItem source) {
this(name, null, source, null, null);
}
/**
* Create a new game event
*
* @param name
* The name is used when checking which event this is / determine its
* “meaning”
* @param source
* The item that caused the event, or <code>null</code> if
* unknown/not relevant
* @param value
* Arbitrary extra data
*/
public GameEvent(String name, IItem source, T value) {
this(name, null, source, null, value);
}
@Override
public T getData() {
return value;
}
@Override
public String getEventName() {
return name;
}
@Override
public IGame getGame() {
return game;
}
@Override
public IItem getSource() {
return source;
@ -37,16 +137,6 @@ public class GameEvent<T> implements IEvent<T> {
return target;
}
@Override
public String getEventName() {
return name;
}
@Override
public T getData() {
return value;
}
@Override
public void setData(T value) {
this.value = value;

View File

@ -1,7 +1,30 @@
package inf101.v18.rogue101.events;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
/**
* An “event” is something that happens in the game, typically due to an actor
* taking some action (nut could also be used to transmit user input)
* <p>
* Storing the particular action and it's associated data in an object means
* that we can extend our system with many different kinds of actions and have
* many different reactions to those actions without having to all of the
* "game rule specific" stuff to all our interfaces and classes.
* <p>
* Our event objects let you store an extra (arbitrary) piece of data, giving
* more information about what happened. An event handler can also update this
* information, which is a possible way to report back to whomever caused the
* event in the first place. The event objects also contain an event name, and
* source/targets of the event (where relevant).
* <p>
* This system is fairly simplistic, and you're not expected to make use of it.
*
* @author anya
*
* @param <T>
* Type of the extra data
*/
public interface IEvent<T> {
/**
* @return Extra data stored in this event
@ -13,6 +36,16 @@ public interface IEvent<T> {
*/
String getEventName();
/**
* Not all events need to be connected to the game, but you can use this if you
* need it (e.g., for showing a message, or adding something to the map).
* <p>
* The result might be null.
*
* @return The game associated with this event, or null.
*/
IGame getGame();
/**
* The source is the item that “caused” the event
* <p>
@ -32,7 +65,8 @@ public interface IEvent<T> {
IItem getTarget();
/**
* @param value Extra data to store in this event
* @param value
* Extra data to store in this event
*/
void setData(T value);
}

View File

@ -1,5 +0,0 @@
package inf101.v18.rogue101.events;
public interface IPickedUpEvent extends IEvent {
}

View File

@ -9,21 +9,53 @@ public class Carrot implements IItem {
private int hp = getMaxHealth();
public void doTurn(IGame game) {
hp = Math.min(hp+1, getMaxHealth());
hp = Math.min(hp + 1, getMaxHealth());
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
painter.save();
painter.turn(75);
double size = ((double) hp + getMaxHealth()) / (2.0 * getMaxHealth());
double carrotLength = size * h * .6;
double carrotWidth = size * h * .4;
painter.jump(-carrotLength / 6);
painter.shape().ellipse().width(carrotLength).height(carrotWidth).fillPaint(Color.ORANGE).fill();
painter.jump(carrotLength / 2);
painter.setInk(Color.FORESTGREEN);
for (int i = -1; i < 2; i++) {
painter.save();
painter.turn(20 * i);
painter.draw(carrotLength / 3);
painter.restore();
}
painter.restore();
return true;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDefence() {
return 0;
}
@Override
public double getHealthStatus() {
return getCurrentHealth() / getMaxHealth();
}
@Override
public int getMaxHealth() {
return 10;
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "carrot";
}
@Override
@ -36,45 +68,15 @@ public class Carrot implements IItem {
return "C";
}
@Override
public double getHealthStatus() {
return getCurrentHealth() / getMaxHealth();
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
hp -= amount;
if(hp < 0) {
if (hp < 0) {
// we're all eaten!
hp = -1;
}
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
painter.save();
painter.turn(75);
double size = ((double)hp+getMaxHealth())/(2.0*getMaxHealth());
double carrotLength = size*h*.6;
double carrotWidth = size*h*.2;
painter.jump(-carrotLength/6);
painter.shape().ellipse().width(carrotLength).height(carrotWidth).fillPaint(Color.ORANGE).fill();
painter.jump(carrotLength/2);
painter.setInk(Color.FORESTGREEN);
for(int i = -1; i < 2; i++) {
painter.save();
painter.turn(20*i);
painter.draw(carrotLength/3);
painter.restore();
}
painter.restore();
return true;
}
@Override
public String getName() {
return "carrot";
}
}

View File

@ -1,14 +1,22 @@
package inf101.v18.rogue101.examples;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.rogue101.events.GameEvent;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
public class ExampleItem implements IItem {
private int hp = getMaxHealth();
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDefence() {
return 0;
@ -20,8 +28,8 @@ public class ExampleItem implements IItem {
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "strange model of an item";
}
@Override
@ -33,22 +41,11 @@ public class ExampleItem implements IItem {
public String getSymbol() {
return "X";
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
hp -= amount;
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "strange model of an item";
}
}

View File

@ -6,8 +6,6 @@ import java.util.List;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.grid.GridDirection;
import inf101.v18.rogue101.events.GameEvent;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.INonPlayer;
@ -32,13 +30,13 @@ public class Rabbit implements INonPlayer {
if (eaten > 0) {
System.out.println("ate carrot worth " + eaten + "!");
food += eaten;
game.displayMessage("You hear a faint crunching (" + getName() + " eats " + item.getArticle() + " " + item.getName() + ")");
game.displayMessage("You hear a faint crunching (" + getName() + " eats " + item.getArticle() + " "
+ item.getName() + ")");
return;
}
}
}
// TODO:
// TODO: prøv forskjellige varianter her
List<GridDirection> possibleMoves = new ArrayList<>();
for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) {
if (game.canGo(dir))
@ -50,11 +48,21 @@ public class Rabbit implements INonPlayer {
}
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getAttack() {
return 1000;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDamage() {
return 1000;
@ -71,8 +79,8 @@ public class Rabbit implements INonPlayer {
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "rabbit";
}
@Override
@ -91,14 +99,4 @@ public class Rabbit implements INonPlayer {
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "rabbit";
}
}

View File

@ -0,0 +1,489 @@
package inf101.v18.rogue101.game;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Supplier;
import inf101.v18.gfx.Screen;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.gfxmode.TurtlePainter;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IGrid;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.Main;
import inf101.v18.rogue101.examples.Carrot;
import inf101.v18.rogue101.examples.Rabbit;
import inf101.v18.rogue101.map.GameMap;
import inf101.v18.rogue101.map.IGameMap;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.map.MapReader;
import inf101.v18.rogue101.objects.Dust;
import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.INonPlayer;
import inf101.v18.rogue101.objects.IPlayer;
import inf101.v18.rogue101.objects.Wall;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
public class Game implements IGame {
/**
* All the IActors that have things left to do this turn
*/
private List<IActor> actors = Collections.synchronizedList(new ArrayList<>());
/**
* For fancy solution to factory problem
*/
private Map<String, Supplier<IItem>> itemFactories = new HashMap<>();
/**
* Useful random generator
*/
private Random random = new Random();
/**
* The game map. {@link IGameMap} gives us a few more details than
* {@link IMapView} (write access to item lists); the game needs this but
* individual items don't.
*/
private IGameMap map;
private IActor currentActor;
private ILocation currentLocation;
private int movePoints = 0;
private final ITurtle painter;
private final Printer printer;
private int numPlayers = 0;
public Game(Screen screen, ITurtle painter, Printer printer) {
this.painter = painter;
this.printer = printer;
// TODO: (*very* optional) for advanced factory technique, use
// something like "itemFactories.put("R", () -> new Rabbit());"
// must be done *before* you read the map
// NOTE: in a more realistic situation, we will have multiple levels (one map
// per level), and (at least for a Roguelike game) the levels should be
// generated
//
// inputGrid will be filled with single-character strings indicating what (if
// anything)
// should be placed at that map square
IGrid<String> inputGrid = MapReader.readFile("maps/level1.txt");
if (inputGrid == null) {
System.err.println("Map not found falling back to builtin map");
inputGrid = MapReader.readString(Main.BUILTIN_MAP);
}
this.map = new GameMap(inputGrid.getArea());
for (ILocation loc : inputGrid.locations()) {
IItem item = createItem(inputGrid.get(loc));
if (item != null) {
map.add(loc, item);
}
}
}
public Game(String mapString) {
printer = new Printer(1280, 720);
painter = new TurtlePainter(1280, 720, null);
IGrid<String> inputGrid = MapReader.readString(mapString);
this.map = new GameMap(inputGrid.getArea());
for (ILocation loc : inputGrid.locations()) {
IItem item = createItem(inputGrid.get(loc));
if (item != null) {
map.add(loc, item);
}
}
}
@Override
public void addItem(IItem item) {
map.add(currentLocation, item);
}
@Override
public void addItem(String sym) {
IItem item = createItem(sym);
if (item != null)
map.add(currentLocation, item);
}
@Override
public ILocation attack(GridDirection dir, IItem target) {
ILocation loc = map.go(currentLocation, dir);
if (map.has(loc, target))
throw new IllegalMoveException("Target isn't there!");
// TODO: implement attack
map.clean(loc);
if (target.isDestroyed()) {
return move(dir);
} else {
movePoints--;
return currentLocation;
}
}
/**
* Begin a new game turn, or continue to the previous turn
*
* @return True if the game should wait for more user input
*/
public boolean doTurn() {
do {
if (actors.isEmpty()) {
// System.err.println("new turn!");
// no one in the queue, we're starting a new turn!
// first collect all the actors:
beginTurn();
}
// process actors one by one; for the IPlayer, we return and wait for keypresses
// Possible TODO: for INonPlayer, we could also return early (returning
// *false*), and then insert a little timer delay between each non-player move
// (the timer
// is already set up in Main)
while (!actors.isEmpty()) {
// get the next player or non-player in the queue
currentActor = actors.remove(0);
if (currentActor.isDestroyed()) // skip if it's dead
continue;
currentLocation = map.getLocation(currentActor);
if (currentLocation == null) {
displayDebug("doTurn(): Whoops! Actor has disappeared from the map: " + currentActor);
}
movePoints = 1; // everyone gets to do one thing
if (currentActor instanceof INonPlayer) {
// computer-controlled players do their stuff right away
((INonPlayer) currentActor).doTurn(this);
// remove any dead items from current location
map.clean(currentLocation);
} else if (currentActor instanceof IPlayer) {
if (currentActor.isDestroyed()) {
// a dead human player gets removed from the game
// TODO: you might want to be more clever here
displayMessage("YOU DIE!!!");
map.remove(currentLocation, currentActor);
currentActor = null;
currentLocation = null;
} else {
// For the human player, we need to wait for input, so we just return.
// Further keypresses will cause keyPressed() to be called, and once the human
// makes a move, it'll lose its movement point and doTurn() will be called again
//
// NOTE: currentActor and currentLocation are set to the IPlayer (above),
// so the game remembers who the player is whenever new keypresses occur. This
// is also how e.g., getLocalItems() work the game always keeps track of
// whose turn it is.
return true;
}
} else {
displayDebug("doTurn(): Hmm, this is a very strange actor: " + currentActor);
}
}
} while (numPlayers > 0); // we can safely repeat if we have players, since we'll return (and break out of
// the loop) once we hit the player
return true;
}
/**
* Go through the map and collect all the actors.
*/
private void beginTurn() {
numPlayers = 0;
// this extra fancy iteration over each map location runs *in parallel* on
// multicore systems!
// that makes some things more tricky, hence the "synchronized" block and
// "Collections.synchronizedList()" in the initialization of "actors".
// NOTE: If you want to modify this yourself, it might be a good idea to replace
// "parallelStream()" by "stream()", because weird things can happen when many
// things happen
// at the same time! (or do INF214 or DAT103 to learn about locks and threading)
map.getArea().parallelStream().forEach((loc) -> { // will do this for each location in map
List<IItem> list = map.getAllModifiable(loc); // all items at loc
Iterator<IItem> li = list.iterator(); // manual iterator lets us remove() items
while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside
IItem item = li.next();
if (item.getCurrentHealth() < 0) {
// normally, we expect these things to be removed when they are destroyed, so
// this shouldn't happen
synchronized (this) {
formatDebug("beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n",
item.getName(), item.getSymbol(), loc);
}
li.remove();
map.remove(loc, item); // need to do this too, to update item map
} else if (item instanceof IPlayer) {
actors.add(0, (IActor) item); // we let the human player go first
synchronized (this) {
numPlayers++;
}
} else if (item instanceof IActor) {
actors.add((IActor) item); // add other actors to the end of the list
}
}
});
}
@Override
public boolean canGo(GridDirection dir) {
return map.canGo(currentLocation, dir);
}
@Override
public IItem createItem(String sym) {
switch (sym) {
case "#":
return new Wall();
case ".":
// TODO: add Dust
return null;
case "R":
return new Rabbit();
case "C":
return new Carrot();
case "@":
// TODO: add Player
case " ":
return null;
default:
// alternative/advanced method
Supplier<IItem> factory = itemFactories.get(sym);
if (factory != null) {
return factory.get();
} else {
System.err.println("createItem: Don't know how to create a '" + sym + "'");
return null;
}
}
}
@Override
public void displayDebug(String s) {
printer.clearLine(Main.LINE_DEBUG);
printer.printAt(1, Main.LINE_DEBUG, s, Color.DARKRED);
System.err.println(s);
}
@Override
public void displayMessage(String s) {
// it should be safe to print to lines Main.LINE_MSG1, Main.LINE_MSG2,
// Main.LINE_MSG3
// TODO: you can save the last three lines, and display/scroll them
printer.clearLine(Main.LINE_MSG1);
printer.printAt(1, Main.LINE_MSG1, s);
System.out.println("Message: «" + s + "»");
}
@Override
public void displayStatus(String s) {
printer.clearLine(Main.LINE_STATUS);
printer.printAt(1, Main.LINE_STATUS, s);
System.out.println("Status: «" + s + "»");
}
public void draw() {
map.draw(painter, printer);
}
@Override
public boolean drop(IItem item) {
if (item != null) {
map.add(currentLocation, item);
return true;
} else
return false;
}
@Override
public void formatDebug(String s, Object... args) {
displayDebug(String.format(s, args));
}
@Override
public void formatMessage(String s, Object... args) {
displayMessage(String.format(s, args));
}
@Override
public void formatStatus(String s, Object... args) {
displayStatus(String.format(s, args));
}
@Override
public int getHeight() {
return map.getHeight();
}
@Override
public List<IItem> getLocalItems() {
return map.getItems(currentLocation);
}
@Override
public ILocation getLocation() {
return currentLocation;
}
@Override
public ILocation getLocation(GridDirection dir) {
if (currentLocation.canGo(dir))
return currentLocation.go(dir);
else
return null;
}
/**
* Return the game map. {@link IGameMap} gives us a few more details than
* {@link IMapView} (write access to item lists); the game needs this but
* individual items don't.
*/
@Override
public IMapView getMap() {
return map;
}
@Override
public List<GridDirection> getPossibleMoves() {
// TODO
throw new UnsupportedOperationException();
}
@Override
public List<ILocation> getVisible() {
// TODO: maybe replace 3 by some sort of visibility range obtained from
// currentActor?
return map.getNeighbourhood(currentLocation, 3);
}
@Override
public int getWidth() {
return map.getWidth();
}
public void keyPressed(KeyCode code) {
// only an IPlayer/human can handle keypresses, and only if it's the human's
// turn
if (currentActor instanceof IPlayer) {
((IPlayer) currentActor).keyPressed(this, code); // do your thing
if (movePoints <= 0)
doTurn(); // proceed with turn if we're out of moves
}
}
@Override
public ILocation move(GridDirection dir) {
if (movePoints < 1)
throw new IllegalMoveException("You're out of moves!");
ILocation newLoc = map.go(currentLocation, dir);
map.remove(currentLocation, currentActor);
map.add(newLoc, currentActor);
currentLocation = newLoc;
movePoints--;
return currentLocation;
}
@Override
public IItem pickUp(IItem item) {
if (item != null && map.has(currentLocation, item)) {
// TODO: bruk getAttack()/getDefence() til å avgjøre om man får til å plukke opp
// tingen
// evt.: en IActor kan bare plukkes opp hvis den har få/ingen helsepoeng igjen
map.remove(currentLocation, item);
return item;
} else {
return null;
}
}
@Override
public ILocation rangedAttack(GridDirection dir, IItem target) {
return currentLocation;
}
@Override
public ITurtle getPainter() {
return painter;
}
@Override
public Printer getPrinter() {
return printer;
}
@Override
public int[] getFreeTextAreaBounds() {
int[] area = new int[4];
area[0] = getWidth() + 1;
area[1] = 1;
area[2] = printer.getLineWidth();
area[3] = printer.getPageHeight() - 5;
return area;
}
@Override
public void clearFreeTextArea() {
printer.clearRegion(getWidth() + 1, 1, printer.getLineWidth() - getWidth(), printer.getPageHeight() - 5);
}
@Override
public void clearFreeGraphicsArea() {
painter.as(GraphicsContext.class).clearRect(getWidth() * printer.getCharWidth(), 0,
painter.getWidth() - getWidth() * printer.getCharWidth(),
(printer.getPageHeight() - 5) * printer.getCharHeight());
}
@Override
public double[] getFreeGraphicsAreaBounds() {
double[] area = new double[4];
area[0] = getWidth() * printer.getCharWidth();
area[1] = 0;
area[2] = painter.getWidth();
area[3] = getHeight() * printer.getCharHeight();
return area;
}
@Override
public IActor getActor() {
return currentActor;
}
public ILocation setCurrent(IActor actor) {
currentLocation = map.getLocation(actor);
if (currentLocation != null) {
currentActor = actor;
movePoints = 1;
}
return currentLocation;
}
public IActor setCurrent(ILocation loc) {
List<IActor> list = map.getActors(loc);
if (!list.isEmpty()) {
currentActor = list.get(0);
currentLocation = loc;
movePoints = 1;
}
return currentActor;
}
public IActor setCurrent(int x, int y) {
return setCurrent(map.getLocation(x, y));
}
@Override
public Random getRandom() {
return random;
}
}

View File

@ -1,23 +1,235 @@
package inf101.v18.rogue101.game;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.INonPlayer;
import inf101.v18.rogue101.objects.IPlayer;
/**
* Game interface
* <p>
* The game has a map and a current {@link IActor} (the player or non-player
* whose turn it currently is). The game also knows the current location of the
* actor. Most methods that deal with the map will use this current location
* they are meant to be used by the current actor for exploring or interacting
* with its surroundings.
* <p>
* In other words, you should avoid calling most of these methods if you're not
* the current actor. You know you're the current actor when you're inside your
* {@link IPlayer#keyPressed()} or {@link INonPlayer#doTurn()} method.
*
* @author anya
*
*/
public interface IGame {
IMapView getMap();
/**
* Add an item to the current location
* <p>
* If the item is an actor, it won't move until the next turn.
*
* @param item
*/
void addItem(IItem item);
ILocation move(GridDirection dir);
/**
* Add a new item (identified by symbol) to the current location
* <p>
* If the item is an actor, it won't move until the next turn.
*
* @param sym
*/
void addItem(String sym);
/**
* Perform an attack on the target.
* <p>
* Will use your {@link IActor#getAttack()} against the target's
* {@link IItem#getDefence()}, and then possiblyu find the damage you're dealing
* with {@link IActor#getDamage()} and inform the target using
* {@link IItem#handleDamage(IGame, IItem, int)}
*
* @param dir
* Direction
* @param target
* A target item, which should be in the neighbouring square in the
* given direction
* @return Your new location if the attack resulted in you moving
*/
ILocation attack(GridDirection dir, IItem target);
ILocation rangedAttack(GridDirection dir, IItem target);
/**
* @param dir
* @return True if it's possible to move in the given direction
*/
boolean canGo(GridDirection dir);
Collection<IItem> getLocalItems();
/**
* Create a new item based on a text symbol
* <p>
* The item won't be added to the map unless you call {@link #addItem(IItem)}.
*
* @param symbol
* @return The new item
*/
IItem createItem(String symbol);
/**
* Displays a message in the debug area on the screen (bottom line)
*
* @param s
* A message
*/
void displayDebug(String s);
/**
* Displays a message in the message area on the screen (below the map and the
* status line)
*
* @param s
* A message
*/
void displayMessage(String s);
/**
* Displays a status message in the status area on the screen (right below the
* map)
*
* @param s
* A message
*/
void displayStatus(String s);
/**
* Displays a message in the message area on the screen (below the map and the
* status line)
*
* @param s
* A message
* @see String#format(String, Object...)
*/
void formatDebug(String s, Object... args);
/**
* Displays a formatted message in the message area on the screen (below the map
* and the status line)
*
* @param s
* A message
* @see String#format(String, Object...)
*/
void formatMessage(String s, Object... args);
/**
* Displays a formatted status message in the status area on the screen (right
* below the map)
*
* @param s
* A message
* @see String#format(String, Object...)
*/
void formatStatus(String s, Object... args);
/**
* Pick up an item
* <p>
* This should be used by IActors who want to pick up an item and carry it. The
* item will be returned if picking it succeeded (the actor <em>might</em> also
* make a mistake and pick up the wrong item!).
*
* @param item
* An item, should be in the current map location
* @return The item that was picked up (normally <code>item</code>), or
* <code>null</code> if it failed
*/
IItem pickUp(IItem item);
/**
* Drop an item
* <p>
* This should be used by IActors who are carrying an item and want to put it on
* the ground. Check the return value to see if it succeeded.
*
* @param item
* An item, should be carried by the current actor and not already be
* on the map
* @return True if the item was placed on the map, false means you're still
* holding it
*/
boolean drop(IItem item);
/**
* Clear the unused graphics area (you can fill it with whatever you want!)
*/
void clearFreeGraphicsArea();
/**
* Clear the unused text area (you can fill it with whatever you want!)
*/
void clearFreeTextArea();
/**
* Get the bounds of the free graphics area.
* <p>
* You can fill this with whatever you want, using {@link #getPainter()} and
* {@link #clearFreeGraphicsArea()}.
*
* @return Array of coordinates; ([0],[1]) are the top-left corner, and
* ([2],[3]) are the bottom-right corner (exclusive).
*/
double[] getFreeGraphicsAreaBounds();
/**
* Get the bounds of the free texxt area.
* <p>
* You can fill this with whatever you want, using {@link #getPrinter()} and
* {@link #clearFreeTextArea()}.
* <p>
* You'll probably want to use something like:
*
* <pre>
* int[] bounds = getFreeTextArea();
* int x = bounds[0];
* int y = bounds[1];
* game.getPrinter().printAt(x, y++, "Hello");
* game.getPrinter().printAt(x, y++, "Do you have any carrot cake?", Color.ORANGE);
* </pre>
*
* @return Array of column/line numbers; ([0],[1]) is the top-left corner, and
* ([2],[3]) is the bottom-right corner (inclusive).
*/
int[] getFreeTextAreaBounds();
/**
* See {@link #getFreeGraphicsAreaBounds()}, {@link #clearFreeGraphicsArea()}.
*
* @return A Turtle, for painting graphics
*/
ITurtle getPainter();
/**
* See {@link #getFreeTextAreaBounds()}, {@link #clearFreeTextArea()}.
*
* @return A printer, for printing text
*/
Printer getPrinter();
/**
* @return The height of the map
*/
int getHeight();
/**
* @return A list of the non-actor items at the current map location
*/
List<IItem> getLocalItems();
/**
* Get the current actor's location.
@ -27,22 +239,38 @@ public interface IGame {
* @return Location of the current actor
*/
ILocation getLocation();
/**
* Get the current actor
* <p>
* You can check if it's your move by doing game.getActor()==this.
*
* @return The current actor (i.e., the (IPlayer/INonPlayer) player who's turn it currently is)
*/
IActor getActor();
/**
* Get the neighbouring map location in direction <code>dir</code>
* <p>
* Same as <code>getLocation().go(dir)</code>
*
* @param dir A direction
* @return A location, or <code>null</code> if the location would be outside the map
* @param dir
* A direction
* @return A location, or <code>null</code> if the location would be outside the
* map
*/
ILocation getLocation(GridDirection dir);
IItem pickUp(IItem item);
/**
* @return The map
*/
IMapView getMap();
boolean drop(IItem item);
boolean canGo(GridDirection dir);
/**
* @return A list of directions we can move in, for use with
* {@link #move(GridDirection)}
*/
List<GridDirection> getPossibleMoves();
/**
* Get a list of all locations that are visible from the current location.
@ -54,26 +282,39 @@ public interface IGame {
* @return A list of grid cells visible from the {@link #getLocation()}
*/
List<ILocation> getVisible();
/**
* @return Width of the map
*/
int getWidth();
int getHeight();
IItem createItem(String symbol);
/**
* Move the current actor in the given direction.
* <p>
* The new location will be returned.
*
* @param dir
* @return A new location
* @throws IllegalMoveException
* if moving in that direction is illegal
*/
ILocation move(GridDirection dir);
void addItem(IItem item);
/**
* Perform a ranged attack on the target.
* <p>
* Rules for this are up to you!
*
* @param dir
* Direction
* @param target
* A target item, which should in some square in the given direction
* @return Your new location if the attack resulted in you moving (unlikely)
*/
ILocation rangedAttack(GridDirection dir, IItem target);
void addItem(String sym);
void displayMessage(String s);
void formatMessage(String s, Object... args);
void displayStatus(String s);
void formatStatus(String s, Object...args);
void displayDebug(String s);
void formatDebug(String s, Object... args);
/**
* @return A random generator
*/
Random getRandom();
}

View File

@ -0,0 +1,254 @@
package inf101.v18.rogue101.map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
import inf101.v18.grid.IMultiGrid;
import inf101.v18.grid.MultiGrid;
import inf101.v18.rogue101.Main;
import inf101.v18.rogue101.game.IllegalMoveException;
import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.Wall;
import javafx.scene.canvas.GraphicsContext;
public class GameMap implements IGameMap {
/**
* The grid that makes up our map
*/
private final IMultiGrid<IItem> grid;
/**
* These locations have changed, and need to be redrawn
*/
private final Set<ILocation> dirtyLocs = new HashSet<>();
/**
* An index of all the items in the map and their locations.
*/
// an IdentityHashMap uses object identity as a lookup key, so items are
// considered equal if they are the same object (a == b)
private final Map<IItem, ILocation> items = new IdentityHashMap<>();
public GameMap(IArea area) {
grid = new MultiGrid<>(area);
}
public GameMap(int width, int height) {
grid = new MultiGrid<>(width, height);
}
@Override
public void add(ILocation loc, IItem item) {
// keep track of location of all items
items.put(item, loc);
// also keep track of whether we need to redraw this cell
dirtyLocs.add(loc);
// do the actual adding
List<IItem> list = grid.get(loc);
list.add(item);
// TODO: should be sorted!
}
@Override
public boolean canGo(ILocation to) {
return !grid.contains(to, (i) -> (i instanceof Wall || i instanceof IActor));
}
@Override
public boolean hasNeighbour(ILocation from, GridDirection dir) {
return from.canGo(dir);
}
@Override
public boolean canGo(ILocation from, GridDirection dir) {
if (!from.canGo(dir))
return false;
ILocation loc = from.go(dir);
return canGo(loc);
}
@Override
public void draw(ITurtle painter, Printer printer) {
Iterable<ILocation> cells;
if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) {
if (dirtyLocs.isEmpty())
return;
else
cells = dirtyLocs;
} else {
cells = grid.locations();
painter.as(GraphicsContext.class).clearRect(0, 0, getWidth() * printer.getCharWidth(),
getHeight() * printer.getCharHeight());
printer.clearRegion(1, 1, getWidth(), getHeight());
}
GraphicsContext ctx = painter.as(GraphicsContext.class);
double h = printer.getCharHeight();
double w = printer.getCharWidth();
if (Main.MAP_AUTO_SCALE_ITEM_DRAW) {
ctx.save();
ctx.scale(w / h, 1.0);
w = h;
}
try {
for (ILocation loc : cells) {
List<IItem> list = grid.get(loc);
String sym = " ";
if (!list.isEmpty()) {
if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) {
ctx.clearRect(loc.getX() * w, loc.getY() * h, w, h);
// ctx.fillRect(loc.getX() * w, loc.getY() * h, w, h);
}
painter.save();
painter.jumpTo((loc.getX() + 0.5) * w, (loc.getY() + 0.5) * h);
boolean dontPrint = list.get(0).draw(painter, w, h);
painter.restore();
if (!dontPrint) {
sym = list.get(0).getPrintSymbol();
}
}
printer.printAt(loc.getX() + 1, loc.getY() + 1, sym);
}
} finally {
if (Main.MAP_AUTO_SCALE_ITEM_DRAW) {
ctx.restore();
}
}
dirtyLocs.clear();
}
@Override
public List<IActor> getActors(ILocation loc) {
List<IActor> items = new ArrayList<>();
for (IItem item : grid.get(loc)) {
if (item instanceof IActor)
items.add((IActor) item);
}
return items;
}
@Override
public List<IItem> getAll(ILocation loc) {
return Collections.unmodifiableList(grid.get(loc));
}
@Override
public List<IItem> getAllModifiable(ILocation loc) {
dirtyLocs.add(loc);
return grid.get(loc);
}
@Override
public void clean(ILocation loc) {
// remove any items that have health < 0:
if (grid.get(loc).removeIf((item) -> {
if (item.isDestroyed()) {
items.remove(item);
return true;
} else {
return false;
}
})) {
dirtyLocs.add(loc);
}
}
@Override
public IArea getArea() {
return grid.getArea();
}
@Override
public int getHeight() {
return grid.getHeight();
}
@Override
public List<IItem> getItems(ILocation loc) {
List<IItem> items = new ArrayList<>(grid.get(loc));
items.removeIf((i) -> i instanceof IActor);
return items;
}
@Override
public ILocation getLocation(IItem item) {
return items.get(item);
}
@Override
public ILocation getLocation(int x, int y) {
return grid.getArea().location(x, y);
}
@Override
public ILocation getNeighbour(ILocation from, GridDirection dir) {
if (!hasNeighbour(from, dir))
return null;
else
return from.go(dir);
}
@Override
public int getWidth() {
return grid.getWidth();
}
@Override
public ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException {
if (!from.canGo(dir))
throw new IllegalMoveException("Cannot move outside map!");
ILocation loc = from.go(dir);
if (!canGo(loc))
throw new IllegalMoveException("Occupied!");
return loc;
}
@Override
public boolean has(ILocation loc, IItem target) {
return grid.contains(loc, target);
}
@Override
public boolean hasActors(ILocation loc) {
return grid.contains(loc, (i) -> i instanceof IActor);
}
@Override
public boolean hasItems(ILocation loc) {
// true if grid cell contains an item which is not an IActor
return grid.contains(loc, (i) -> !(i instanceof IActor));
}
@Override
public boolean hasWall(ILocation loc) {
return grid.contains(loc, (i) -> i instanceof Wall);
}
@Override
public void remove(ILocation loc, IItem item) {
grid.remove(loc, item);
items.remove(item);
dirtyLocs.add(loc);
}
@Override
public List<ILocation> getNeighbourhood(ILocation loc, int dist) {
if (dist < 0 || loc == null)
throw new IllegalArgumentException();
else if (dist == 0)
return new ArrayList<>(); // empty!
// TODO: implement this!
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,48 @@
package inf101.v18.rogue101.map;
import java.util.List;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.objects.IItem;
/**
* Extra map methods that are for the game class only!
*
* @author anya
*
*/
public interface IGameMap extends IMapView {
/**
* Draw the map
*
* @param painter
* @param printer
*/
void draw(ITurtle painter, Printer printer);
/**
* Get a modifiable list of items
*
* @param loc
* @return
*/
List<IItem> getAllModifiable(ILocation loc);
/**
* Remove any destroyed items at the given location (items where {@link IItem#isDestroyed()} is true)
*
* @param loc
*/
void clean(ILocation loc);
/**
* Remove an item
* @param loc
* @param item
*/
void remove(ILocation loc, IItem item);
}

View File

@ -2,8 +2,6 @@ package inf101.v18.rogue101.map;
import java.util.List;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
@ -12,37 +10,181 @@ import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.IItem;
public interface IMapView {
boolean canGo(ILocation from, GridDirection dir);
boolean canGo(ILocation to);
ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException;
boolean hasActors(ILocation loc);
List<IActor> getActors(ILocation loc);
boolean hasItems(ILocation loc);
List<IItem> getItems(ILocation loc);
List<IItem> getAll(ILocation loc);
boolean hasWall(ILocation loc);
void remove(ILocation loc, IItem item);
/**
* Add an item to the map.
*
* @param loc
* A location
* @param item
* the item
*/
void add(ILocation loc, IItem item);
boolean has(ILocation loc, IItem target);
ILocation getLocation(int x, int y);
ILocation getLocation(IItem item);
/**
* Check if it's legal for an IActor to go into the given location
*
* @param to
* A location
* @return True if the location isn't already occupied
*/
boolean canGo(ILocation to);
/**
* Check if it's legal for an IActor to go in the given direction from the given
* location
*
* @param from
* Current location
* @param dir
* Direction we want to move in
* @return True if the next location exists and isn't occupied
*/
boolean canGo(ILocation from, GridDirection dir);
/**
* Get all IActors at the given location
* <p>
* The returned list either can't be modified, or modifying it won't affect the
* map.
*
* @param loc
* @return A list of actors
*/
List<IActor> getActors(ILocation loc);
/**
* Get all items (both IActors and other IItems) at the given location
* <p>
* The returned list either can't be modified, or modifying it won't affect the
* map.
*
* @param loc
* @return A list of items
*/
List<IItem> getAll(ILocation loc);
/**
* Get all non-IActor items at the given location
* <p>
* The returned list either can't be modified, or modifying it won't affect the
* map.
*
* @param loc
* @return A list of items, non of which are instanceof IActor
*/
List<IItem> getItems(ILocation loc);
/**
* @return A 2D-area defining the legal map locations
*/
IArea getArea();
int getWidth();
/**
* @return Height of the map, same as
* {@link #getArea()}.{@link IArea#getHeight()}
*/
int getHeight();
/**
* @return Width of the map, same as {@link #getArea()}.{@link IArea#getWidth()}
*/
int getWidth();
/**
* Find location of an item
*
* @param item
* The item
* @return It's location, or <code>null</code> if it's not on the map
*/
ILocation getLocation(IItem item);
/**
* Translate (x,y)-coordinates to ILocation
*
* @param x
* @param y
* @return an ILocation
* @throws IndexOutOfBoundsException
* if (x,y) is outside {@link #getArea()}
*/
ILocation getLocation(int x, int y);
/**
* Get the neighbouring location in the given direction
*
* @param from
* A location
* @param dir
* the Direction
* @return from's neighbour in direction dir, or null, if this would be outside
* the map
*/
ILocation getNeighbour(ILocation from, GridDirection dir);
/**
* Compute new location of an IActor moving the given direction
*
* @param from
* Original location
* @param dir
* Direction we're moving in
* @return The new location
* @throws IllegalMoveException
* if !{@link #canGo(ILocation, GridDirection)}
*/
ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException;
/**
* Check if an item exists at a location
*
* @param loc
* The location
* @param target
* The item we're interested in
* @return True if target would appear in {@link #getAll(loc)}
*/
boolean has(ILocation loc, IItem target);
/**
* Check for actors.
*
* @param loc
* @return True if {@link #getActors(loc)} would be non-empty
*/
boolean hasActors(ILocation loc);
/**
* Check for non-actors
*
* @param loc
* @return True if {@link #getItem(loc)} would be non-empty
*/
boolean hasItems(ILocation loc);
/**
* Check for walls
*
* @param loc
* @return True if there is a wall at the given location ({@link #getAll(loc)}
* would contain an instance of {@link Wall})
*/
boolean hasWall(ILocation loc);
/**
* Check if a neighbour exists on the map
*
* @param from A location
* @param dir A direction
* @return True if {@link #getNeighbour(from, dir)} would return non-null
*/
boolean hasNeighbour(ILocation from, GridDirection dir);
/**
* Get all locations within i steps from the given centre
* @param centre
* @param dist
* @return A list of locations, all at most i grid cells away from centre
*/
List<ILocation> getNeighbourhood(ILocation centre, int dist);
}

View File

@ -1,7 +1,5 @@
package inf101.v18.rogue101.map;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
@ -36,50 +34,6 @@ import inf101.v18.grid.MyGrid;
* @author anya (Rogue101 update, 2018)
*/
public class MapReader {
/**
* Load map from file.
* <p>
* Files are search for relative to the folder containing the MapReader class.
*
* @return the dungeon map as a grid of characters read from the file, or null
* if it failed
*/
public static IGrid<String> readFile(String path) {
IGrid<String> symbolMap = null;
InputStream stream = MapReader.class.getResourceAsStream(path);
if(stream == null)
return null;
try (Scanner in = new Scanner(stream, "UTF-8")) {
int width = in.nextInt();
int height = in.nextInt();
// System.out.println(width + " " + height);
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
try {
stream.close();
} catch (IOException e) {
}
return symbolMap;
}
/**
* @return the dungeon map as a grid of characters read from the input string, or null
* if it failed
*/
public static IGrid<String> readString(String input) {
IGrid<String> symbolMap = null;
try (Scanner in = new Scanner(input)) {
int width = in.nextInt();
int height = in.nextInt();
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
return symbolMap;
}
/**
* This method fills the previously initialized {@link #symbolMap} with the
* characters read from the file.
@ -98,4 +52,48 @@ public class MapReader {
xy[1]++;
}
}
/**
* Load map from file.
* <p>
* Files are search for relative to the folder containing the MapReader class.
*
* @return the dungeon map as a grid of characters read from the file, or null
* if it failed
*/
public static IGrid<String> readFile(String path) {
IGrid<String> symbolMap = null;
InputStream stream = MapReader.class.getResourceAsStream(path);
if (stream == null)
return null;
try (Scanner in = new Scanner(stream, "UTF-8")) {
int width = in.nextInt();
int height = in.nextInt();
// System.out.println(width + " " + height);
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
try {
stream.close();
} catch (IOException e) {
}
return symbolMap;
}
/**
* @return the dungeon map as a grid of characters read from the input string,
* or null if it failed
*/
public static IGrid<String> readString(String input) {
IGrid<String> symbolMap = null;
try (Scanner in = new Scanner(input)) {
int width = in.nextInt();
int height = in.nextInt();
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
return symbolMap;
}
}

View File

@ -2,11 +2,20 @@ package inf101.v18.rogue101.objects;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.BlocksAndBoxes;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
public class Dust implements IItem {
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getCurrentHealth() {
return 0;
}
@Override
public int getDefence() {
return 0;
@ -18,8 +27,8 @@ public class Dust implements IItem {
}
@Override
public int getCurrentHealth() {
return 0;
public String getName() {
return "thick layer of dust";
}
@Override
@ -32,21 +41,9 @@ public class Dust implements IItem {
return BlocksAndBoxes.BLOCK_HALF;
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
return 0;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "thick layer of dust";
}
}

View File

@ -1,7 +1,24 @@
package inf101.v18.rogue101.objects;
/**
* An actor is an IItem that can also do something, either controlled by the
* computer (INonPlayer) or the user (IPlayer).
*
* @author anya
*
*/
public interface IActor extends IItem {
/**
* @return This actor's attack score (used against an item's
* {@link #getDefence()} score to see if an attack is successful)
*/
int getAttack();
/**
* @return The damage this actor deals on a successful attack (used together
* with
* {@link #handleDamage(inf101.v18.rogue101.game.IGame, IItem, int)} on
* the target)
*/
int getDamage();
}

View File

@ -18,131 +18,6 @@ import inf101.v18.rogue101.game.IGame;
* @author anya
*/
public interface IItem extends Comparable<IItem> {
/**
* The defence score determines how hard an object/actor is to hit or grab.
*
* @return Defence score of this object
*/
int getDefence();
/**
* Get maximum health points.
*
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Max health points for this item
*/
int getMaxHealth();
/**
* Get current remaining health points.
* <p>
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Current health points for this item
*/
int getCurrentHealth();
/**
* Get the size of the object.
* <p>
* The size determines how much space an item will use if put into a container.
*
* @return Size of the item
*/
int getSize();
/**
* Get the map symbol of this item.
* <p>
* The symbol can be used on a text-only map, or when loading a map from text.
* <p>
* The symbol should be a single Unicode codepoint (i.e.,
* <code>getSymbol().codePointCount(0, getSymbol().length()) == 1</code>). In
* most cases this means that the symbol should be a single character (i.e.,
* getSymbol().length() == 1); but there are a few Unicode characters (such as
* many emojis and special symbols) that require two Java <code>char</code>s.
*
* @return A single-codepoint string with the item's symbol
*/
String getSymbol();
/**
* Get a (user-friendly) name for the item
* <p>
* Used for things like <code>"You see " + getArticle() + " " + getName()</code>
*
* @return Item's name
*/
String getName();
/**
* @return "a" or "an", depending on the name
*/
default String getArticle() {
return "a";
}
/**
* Get a map symbol used for printing this item on the screen.
* <p>
* This is usually the same as {@link #getSymbol()}, but could also include
* special control characters for changing the text colour, for example.
*
*
* @return A string to be displayed for this item on the screen (should be only
* one column wide when printed)
* @see <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI
* escape code (on Wikipedia)</a>
*/
default String getPrintSymbol() {
return getSymbol();
}
/**
* Get item health as a 0.0..1.0 proportion.
*
* <li><code>getHealth() >= 1.0</code> means perfect condition
* <li><code>getHealth() <= 0.0</code> means broken or dead
* <li><code>0.0 < getHealth() < 1.0</code> means partially damaged
*
* @return Health, in the range 0.0 to 1.0
*/
default double getHealthStatus() {
return getMaxHealth() > 0 ? getCurrentHealth() / getMaxHealth() : 0;
}
/**
* Inform the item that it has been damaged
*
* @param game
* The game
* @param source
* The item (usually an IActor) that caused the damage
* @param amount
* How much damage the item should take
* @return Amount of damage actually taken (could be less than
* <code>amount</code> due to armour/protection effects)
*/
int handleDamage(IGame game, IItem source, int amount);
/**
* Inform the item that something has happened.
*
* @param event
* An object describing the event.
* @return
*/
default <T> T handleEvent(IEvent<T> event) {
return event.getData();
}
default boolean isDestroyed() {
return getCurrentHealth() < 0;
}
@Override
default int compareTo(IItem other) {
return Integer.compare(getSize(), other.getSize());
@ -175,4 +50,132 @@ public interface IItem extends Comparable<IItem> {
default boolean draw(ITurtle painter, double w, double h) {
return false;
}
/**
* @return "a" or "an", depending on the name
*/
default String getArticle() {
return "a";
}
/**
* Get current remaining health points.
* <p>
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Current health points for this item
*/
int getCurrentHealth();
/**
* The defence score determines how hard an object/actor is to hit or grab.
*
* @return Defence score of this object
*/
int getDefence();
/**
* Get item health as a 0.0..1.0 proportion.
*
* <li><code>getHealth() >= 1.0</code> means perfect condition
* <li><code>getHealth() <= 0.0</code> means broken or dead
* <li><code>0.0 < getHealth() < 1.0</code> means partially damaged
*
* @return Health, in the range 0.0 to 1.0
*/
default double getHealthStatus() {
return getMaxHealth() > 0 ? getCurrentHealth() / getMaxHealth() : 0;
}
/**
* Get maximum health points.
*
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Max health points for this item
*/
int getMaxHealth();
/**
* Get a (user-friendly) name for the item
* <p>
* Used for things like <code>"You see " + getArticle() + " " + getName()</code>
*
* @return Item's name
*/
String getName();
/**
* Get a map symbol used for printing this item on the screen.
* <p>
* This is usually the same as {@link #getSymbol()}, but could also include
* special control characters for changing the text colour, for example.
*
*
* @return A string to be displayed for this item on the screen (should be only
* one column wide when printed)
* @see <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI
* escape code (on Wikipedia)</a>
*/
default String getPrintSymbol() {
return getSymbol();
}
/**
* Get the size of the object.
* <p>
* The size determines how much space an item will use if put into a container.
*
* @return Size of the item
*/
int getSize();
/**
* Get the map symbol of this item.
* <p>
* The symbol can be used on a text-only map, or when loading a map from text.
* <p>
* The symbol should be a single Unicode codepoint (i.e.,
* <code>getSymbol().codePointCount(0, getSymbol().length()) == 1</code>). In
* most cases this means that the symbol should be a single character (i.e.,
* getSymbol().length() == 1); but there are a few Unicode characters (such as
* many emojis and special symbols) that require two Java <code>char</code>s.
*
* @return A single-codepoint string with the item's symbol
*/
String getSymbol();
/**
* Inform the item that it has been damaged
*
* @param game
* The game
* @param source
* The item (usually an IActor) that caused the damage
* @param amount
* How much damage the item should take
* @return Amount of damage actually taken (could be less than
* <code>amount</code> due to armour/protection effects)
*/
int handleDamage(IGame game, IItem source, int amount);
/**
* Inform the item that something has happened.
*
* @param event
* An object describing the event.
* @return
*/
default <T> T handleEvent(IEvent<T> event) {
return event.getData();
}
/**
* @return True if this item has been destroyed, and should be removed from the map
*/
default boolean isDestroyed() {
return getCurrentHealth() < 0;
}
}

View File

@ -2,6 +2,21 @@ package inf101.v18.rogue101.objects;
import inf101.v18.rogue101.game.IGame;
/**
* An actor controlled by the computer
*
* @author anya
*
*/
public interface INonPlayer extends IActor {
/**
* Do one turn for this non-player
* <p>
* This INonPlayer will be the game's current actor ({@link IGame#getActor()})
* for the duration of this method call.
*
* @param game
* Game, for interacting with the world
*/
void doTurn(IGame game);
}

View File

@ -4,5 +4,22 @@ import inf101.v18.rogue101.game.IGame;
import javafx.scene.input.KeyCode;
public interface IPlayer extends IActor {
/**
* Send key presses from the human player to the player object.
* <p>
* The player object should interpret the key presses, and then perform its
* moves or whatever, according to the game's rules and the player's
* instructions.
* <p>
* This IPlayer will be the game's current actor ({@link IGame#getActor()}) and
* be at {@link IGame#getLocation()}, when this method is called.
* <p>
* This method may be called many times in a single turn; the turn ends
* {@link #keyPressed(IGame, KeyCode)} returns and the player has used its
* movement points (e.g., by calling {@link IGame#move(inf101.v18.grid.GridDirection)}).
*
* @param game
* Game, for interacting with the world
*/
void keyPressed(IGame game, KeyCode key);
}

View File

@ -2,13 +2,21 @@ package inf101.v18.rogue101.objects;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.BlocksAndBoxes;
import inf101.v18.rogue101.events.GameEvent;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
public class Wall implements IItem {
private int hp = getMaxHealth();
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDefence() {
return 10;
@ -20,8 +28,8 @@ public class Wall implements IItem {
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "wall";
}
@Override
@ -33,20 +41,10 @@ public class Wall implements IItem {
public String getSymbol() {
return BlocksAndBoxes.BLOCK_FULL;
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
hp -= amount;
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "wall";
}
}

View File

@ -1,15 +1,19 @@
package inf101.v18.rogue101.tests;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.Assert.*;
import org.junit.jupiter.api.Test;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.map.GameMap;
class GameMapTest {
@Test
void testSortedAdd() {
// TODO
GameMap gameMap = new GameMap(20, 20);
ILocation location = gameMap.getLocation(10, 10);
// TODO:
fail("Not yet implemented");
}

View File

@ -0,0 +1,42 @@
package inf101.v18.rogue101.tests;
import static org.junit.Assert.*;
import org.junit.jupiter.api.Test;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.game.Game;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.map.GameMap;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.IPlayer;
import javafx.scene.input.KeyCode;
class PlayerTest {
public static String TEST_MAP = "40 5\n" //
+ "########################################\n" //
+ "#...... ..C.R ......R.R......... ..R...#\n" //
+ "#.R@R...... ..........RC..R...... ... .#\n" //
+ "#... ..R........R......R. R........R.RR#\n" //
+ "########################################\n" //
;
@Test
void testPlayer1() {
// new game with our test map
Game game = new Game(TEST_MAP);
// pick (3,2) as the "current" position; this is where the player is on the
// test map, so it'll set up the player and return it
IPlayer player = (IPlayer) game.setCurrent(3, 2);
// find players location
ILocation loc = game.getLocation();
// press "UP" key
player.keyPressed(game, KeyCode.UP);
// see that we moved north
assertEquals(loc.go(GridDirection.NORTH), game.getLocation());
}
}

View File

@ -8,17 +8,6 @@ import java.util.Random;
public class ElementGenerator<T> extends AbstractGenerator<T> {
private List<T> elts;
/**
* New ElementGenerator, will pick a random element from a list.
*
* @requires list must not be empty
*/
public ElementGenerator(List<T> elts) {
if (elts.size() == 0)
throw new IllegalArgumentException();
this.elts = elts;
}
/**
* New ElementGenerator, will pick a random element from a collection.
*
@ -30,6 +19,17 @@ public class ElementGenerator<T> extends AbstractGenerator<T> {
this.elts = new ArrayList<>(elts);
}
/**
* New ElementGenerator, will pick a random element from a list.
*
* @requires list must not be empty
*/
public ElementGenerator(List<T> elts) {
if (elts.size() == 0)
throw new IllegalArgumentException();
this.elts = elts;
}
@Override
public T generate(Random r) {
return elts.get(r.nextInt(elts.size()));

View File

@ -4,7 +4,6 @@ import java.util.Random;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
import inf101.v18.grid.IPosition;
public class LocationGenerator extends AbstractGenerator<ILocation> {
private final IArea area;