Skip to content

Instantly share code, notes, and snippets.

@shiracamus
Last active August 31, 2020 23:45
Show Gist options
  • Save shiracamus/12f74c5271e2ae406f17dfe83c7b6707 to your computer and use it in GitHub Desktop.
Save shiracamus/12f74c5271e2ae406f17dfe83c7b6707 to your computer and use it in GitHub Desktop.
CleanArchitecture Breakout.java on QIita https://qiita.com/shiracamus/items/54b7bf1af018ce829335
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.function.Consumer;
import java.awt.Component;
import java.awt.CardLayout;
import java.awt.Graphics;
import java.awt.Font;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseMotionAdapter;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Breakout extends JFrame {
public static void main(String[] args) {
var breakout = new Breakout();
breakout.playAfterSplash(5);
breakout.setVisible(true);
}
private final BreakoutGame game = new BreakoutGame();
private final CardLayout card = new CardLayout(0, 0);
private Timer timer;
public Breakout() {
setTitle("ブロック崩し");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
var pane = getContentPane();
pane.setLayout(card);
pane.add(new Splash(), "splash");
pane.add(game.getView(), "game");
pack();
}
public void playAfterSplash(int sec) {
card.show(getContentPane(), "splash");
timer = new javax.swing.Timer(sec * 1000, e -> {
timer.stop();
timer = null;
play();
});
timer.start();
}
public void play() {
card.show(getContentPane(), "game");
game.start();
}
}
class Splash extends JPanel {
@Override
public void paint(Graphics g) {
super.paint(g);
g.setFont(new Font("TimeRoman", Font.CENTER_BASELINE, 30));
g.setColor(Color.red);
g.drawString("ブロック崩し", 300, 200);
g.drawString("五秒後に始まるよー!", 250, 300);
g.drawString("マウスを横に動かしてボールを跳ね返そう", 125, 500);
g.drawString("マウスは下の矢印の先っぽがが初期位置でつ", 100, 600);
g.drawString("↓", 425, 700);
}
}
class BreakoutGame {
private final BreakoutViewModel viewModel = new BreakoutViewModel();
private final BreakoutPresenter presenter = new BreakoutPresenter(viewModel);
private final BreakoutUseCase uc = new BreakoutUseCase(presenter);
private final GameView view = new GameView(viewModel, BreakoutStage.WIDTH, BreakoutStage.HEIGHT);
private final BreakoutController controller = new BreakoutController(uc, view);
public BreakoutGame() {
presenter.addListener(model -> view.repaint());
presenter.addListener(model -> {
if (viewModel.isGameClear() || viewModel.isGameOver()) {
stop();
}
});
}
public Component getView() {
return view;
}
public void start() {
controller.enable();
}
public void stop() {
controller.disable();
}
}
class GameView extends JPanel {
private final GameViewModel model;
public GameView(GameViewModel model, int width, int height) {
this.model = model;
setPreferredSize(new Dimension(width, height));
setBackground(Color.black);
}
@Override
public void paint(Graphics g) {
super.paint(g);
model.paint(g);
}
}
interface GameViewModel {
public void paint(Graphics g);
}
class BreakoutViewModel implements GameViewModel {
private BreakoutViewData data;
public void update(BreakoutViewData data) {
this.data = data;
}
public boolean isAvailable() {
return data != null;
}
public boolean isGameClear() {
return data.isGameClear();
}
public boolean isGameOver() {
return data.isGameOver();
}
@Override
public void paint(Graphics g) {
if (!isAvailable()) return;
paintBalls(g);
paintWalls(g);
paintBlocks(g);
paintRacket(g);
if (isGameClear()) {
paintGameClear(g);
} else if (isGameOver()) {
paintGameOver(g);
}
}
public void paintWalls(Graphics g) {
final int[] offset = {0, -16, -8};
final int blockWidth = 26, blockHeight = 10, gapX = 6, gapY = 5;
g.setColor(Color.GREEN);
data.viewWalls((wallX, wallY, wallWidth, wallHeight) -> {
for (int y = wallY, iy = 0; y < wallY + wallHeight; y += blockHeight + gapY, iy++) {
for (int blockX = wallX + offset[iy % offset.length]; blockX < wallX + wallWidth; blockX += blockWidth + gapX) {
int x = blockX;
int width = blockWidth;
int height = blockHeight;
if (x < wallX) {
x = wallX;
width -= wallX - blockX;
} else if (x + blockWidth >= wallX + wallWidth) {
width = wallX + wallWidth - x;
}
if (wallY + height >= wallY + wallHeight) {
height = wallY + wallHeight - y;
}
g.fillRect(x, y, width, height);
}
}
});
}
public void paintBlocks(Graphics g) {
data.viewBlocks((x, y, width, height, color) -> {
g.setColor(color);
g.fillRect(x, y, width, height);
});
}
public void paintRacket(Graphics g) {
g.setColor(Color.WHITE);
data.viewRacket((x, y, width, height) -> g.fillRect(x, y, width, height));
}
private void paintBalls(Graphics g) {
g.setColor(Color.RED);
data.viewBalls((x, y, size) -> g.fillOval(x, y, size, size));
}
public void paintGameClear(Graphics g) {
g.setFont(new Font("TimeRoman", Font.BOLD, 50));
g.setColor(Color.orange);
g.drawString("Game Clear!", 300, 550);
}
public void paintGameOver(Graphics g) {
g.setFont(new Font("TimeRoman", Font.BOLD, 50));
g.setColor(Color.orange);
g.drawString("Game Over!", 300, 550);
}
}
class BreakoutPresenter implements BreakoutViewer {
private final BreakoutViewModel viewModel;
private final List<Consumer<BreakoutViewModel>> listeners = new ArrayList<>();
public BreakoutPresenter(BreakoutViewModel viewModel) {
this.viewModel = viewModel;
}
public void addListener(Consumer<BreakoutViewModel> listener) {
listeners.add(listener);
}
public void removeListener(Consumer<BreakoutViewModel> listener) {
listeners.remove(listener);
}
@Override
public void view(BreakoutViewData data) {
viewModel.update(data);
listeners.forEach(listener -> listener.accept(viewModel));
}
}
class BreakoutController {
public static final int MOVE_BALLS_INTERVAL_MILLISEC = 50;
private final BreakoutOperation operation;
private final Component mouseDevice;
private final MouseMotionListener mouseController;
private final Timer ballController;
public BreakoutController(BreakoutOperation operation, Component mouseDevice) {
this.operation = operation;
this.mouseDevice = mouseDevice;
mouseController = makeMouseController();
ballController = makeBallController(MOVE_BALLS_INTERVAL_MILLISEC);
}
private MouseMotionListener makeMouseController() {
return new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent event) {
operation.moveRacket(event.getX());
}
};
}
private Timer makeBallController(int interval_millisec) {
return new javax.swing.Timer(interval_millisec, e -> {
operation.moveBalls();
});
}
public void enable() {
mouseDevice.addMouseMotionListener(mouseController);
ballController.start();
}
public void disable() {
mouseDevice.removeMouseMotionListener(mouseController);
ballController.stop();
}
}
interface BreakoutOperation {
public void moveRacket(int x);
public void moveBalls();
}
interface BreakoutViewer {
public void view(BreakoutViewData data);
}
interface BreakoutViewData {
public boolean isGameClear();
public boolean isGameOver();
public void viewWalls(WallViewer viewer);
public void viewBlocks(BlockViewer viewer);
public void viewRacket(RacketViewer viewer);
public void viewBalls(BallViewer viewer);
}
class BreakoutUseCase implements BreakoutOperation {
protected final BreakoutViewer viewer;
protected final BreakoutViewData viewData = createViewData();
protected BreakoutStage stage = createStage();
public BreakoutUseCase(BreakoutViewer viewer) {
this.viewer = viewer;
}
public BreakoutStage createStage() {
return new BreakoutStage1();
}
public BreakoutViewData createViewData() {
return new BreakoutViewData() {
@Override
public boolean isGameClear() {
return stage.isClear();
}
@Override
public boolean isGameOver() {
return stage.isOver();
}
@Override
public void viewWalls(WallViewer viewer) {
stage.viewWalls(viewer);
}
@Override
public void viewBlocks(BlockViewer viewer) {
stage.viewBlocks(viewer);
}
@Override
public void viewRacket(RacketViewer viewer) {
stage.viewRacket(viewer);
}
@Override
public void viewBalls(BallViewer viewer) {
stage.viewBalls(viewer);
}
};
}
@Override
public void moveRacket(int x) {
stage.moveRacket(x);
output();
}
@Override
public void moveBalls() {
stage.moveBalls();
output();
}
public void output() {
viewer.view(viewData);
}
}
abstract class BreakoutStage {
public static final int WIDTH = 855;
public static final int HEIGHT = 800;
public static final int WALL_SIZE = 40;
protected final Random rand = new Random();
protected final Court court = makeCourt();
protected final Racket racket = makeRacket();
protected final List<Block> blocks = makeBlocks();
protected final List<Ball> balls = makeBalls();
protected Court makeCourt() {
return new Court(WIDTH, HEIGHT, WALL_SIZE);
}
abstract protected Racket makeRacket();
abstract protected List<Block> makeBlocks();
abstract protected List<Ball> makeBalls();
public void moveRacket(int x) {
racket.move(x);
}
public void moveBalls() {
balls.forEach(ball -> {
ball.move(HEIGHT);
court.rebound(ball);
blocks.forEach(ball::bound);
ball.bound(racket);
});
}
public boolean isClear() {
return blocks.stream().allMatch(block -> block.isCleared());
}
public boolean isOver() {
return balls.stream().allMatch(ball -> ball.isDead());
}
public void viewWalls(WallViewer viewer) {
court.viewWalls(viewer);
}
public void viewBlocks(BlockViewer viewer) {
blocks.forEach(block -> block.view(viewer));
}
public void viewRacket(RacketViewer viewer) {
racket.view(viewer);
}
public void viewBalls(BallViewer viewer) {
balls.forEach(ball -> ball.view(viewer));
}
}
class BreakoutStage1 extends BreakoutStage {
@Override
protected Racket makeRacket() {
return new Racket(WIDTH / 2, HEIGHT - 110, 120, 5, WALL_SIZE, WIDTH - WALL_SIZE);
}
@Override
protected List<Block> makeBlocks() {
List<Block> blocks = new ArrayList<>();
addBlocks(blocks, 60, 40, 16, 40, 48, 8, 4, Color.YELLOW, 3);
addBlocks(blocks, 300, 40, 16, 40, 16, 8, 3, Color.GREEN, 2);
addBlocks(blocks, 450, 40, 16, 40, 16, 8, 1, Color.GRAY, Block.UNBREAKABLE);
addBlocks(blocks, 600, 40, 16, 40, 16, 9, 4, Color.CYAN, 1);
return blocks;
}
protected void addBlocks(List<Block> blocks, int topY,
int width, int height, int gapX, int gapY,
int cols, int rows, Color color, int strength) {
int topX = (WIDTH - width * cols - gapX * (cols - 1)) / 2;
int endY = topY + (height + gapY) * rows;
int endX = topX + (width + gapX) * cols;
for (int y = topY; y < endY; y += height + gapY) {
for (int x = topX; x < endX; x += width + gapX) {
blocks.add(new Block(x, y, width, height, color, strength));
}
}
}
@Override
protected List<Ball> makeBalls() {
return Arrays.asList(new Ball[] {
makeBall(250, 5, -6, 7),
makeBall(260, -5, -3, 10),
makeBall(420, 4, 6, 8),
makeBall(480, -5, 2, 10),
makeBall(590, 5, -6, 11),
makeBall(550, -5, -3, 12),
makeBall(570, 4, 6, 13),
makeBall(480, -5, 2, 14),
makeBall(490, 5, -6, 8),
makeBall(400, -5, -3, 8),
makeBall(350, 4, 6, 9),
makeBall(400, -5, 2, 10),
makeBall(390, -5, -3, 10),
makeBall(500, 4, 6, 10),
makeBall(530, -5, 2, 7),
});
}
protected Ball makeBall(int y, int vx, int vy, int size) {
return new Ball(40 + rand.nextInt(700), y, vx, vy, size);
}
}
class Bounder {
protected Rectangle rect;
public Bounder(int x, int y, int width, int height) {
this.rect = new Rectangle(x, y, width, height);
}
public boolean isHit(int x, int y) {
return this.rect.contains(x, y);
}
public void hit() {
// default: nothing to do
}
public void view(BounderViewer viewer) {
viewer.view(rect.x, rect.y, rect.width, rect.height);
}
}
interface BounderViewer {
public void view(int x, int y, int width, int height);
}
class Court {
private final Wall up, left, right;
public Court(int width, int height, int wallSize) {
up = new Wall(0, 0, width, wallSize);
left = new Wall(0, 0, wallSize, height);
right = new Wall(width - wallSize, 0, wallSize, height);
}
public boolean isHit(int x, int y) {
return up.isHit(x, y) || left.isHit(x, y) || right.isHit(x, y);
}
public void rebound(Boundee boundee) {
boundee.bound(up);
boundee.bound(left);
boundee.bound(right);
}
public void viewWalls(WallViewer viewer) {
up.view(viewer);
left.view(viewer);
right.view(viewer);
}
}
class Wall extends Bounder {
public Wall(int x, int y, int width, int height) {
super(x, y, width, height);
}
}
interface WallViewer extends BounderViewer {
}
class Racket extends Bounder {
private final int left, right;
public Racket(int centerX, int centerY, int width, int height,
int limitLeft, int limitRight) {
super(centerX - width / 2, centerY - height / 2, width, height);
left = limitLeft;
right = limitRight - width;
}
public void move(int x) {
x -= rect.width / 2;
rect.x = x < left ? left
: x > right ? right
: x;
}
}
interface RacketViewer extends BounderViewer {
}
class Block extends Bounder {
public static final int UNBREAKABLE = -1;
private static final int BROKEN = 0;
private final Color color;
private int strength;
public Block(int x, int y, int width, int height, Color color, int strength) {
super(x, y, width, height);
this.color = color;
this.strength = strength;
}
@Override
public boolean isHit(int x, int y) {
return isBroken() ? false : super.isHit(x, y);
}
@Override
public void hit() {
if (strength > 0) strength--;
}
public boolean isBroken() {
return strength == BROKEN;
}
public boolean isCleared() {
return strength <= 0;
}
public void view(BlockViewer viewer) {
if (isBroken()) return;
viewer.view(rect.x, rect.y, rect.width, rect.height, color);
}
}
interface BlockViewer {
public void view(int x, int y, int width, int height, Color color);
}
interface Boundee {
public void bound(Bounder bounder);
}
class Ball implements Boundee {
private int x, y, vx, vy;
private final int size, r;
private boolean alive = true;
public Ball(int x, int y, int vx, int vy, int size) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.size = size | 1; // always odd
this.r = this.size / 2;
}
public void move(int bottomY) {
if (alive) {
x += vx;
y += vy;
alive = y - r < bottomY;
}
}
@Override
public void bound(Bounder bounder) {
boolean up = bounder.isHit(x, y - r);
boolean down = bounder.isHit(x, y + r);
boolean left = bounder.isHit(x - r, y);
boolean right = bounder.isHit(x + r, y);
boolean up_left = bounder.isHit(x - r, y - r);
boolean up_right = bounder.isHit(x + r, y - r);
boolean down_left = bounder.isHit(x - r, y + r);
boolean down_right = bounder.isHit(x + r, y + r);
if (vy < 0 && up && !bounder.isHit(x, y - r - vy) ||
vy > 0 && down && !bounder.isHit(x, y + r - vy)) {
bounder.hit();
vy *= -1;
} else if (vx < 0 && left && !bounder.isHit(x - r - vx, y - r) ||
vx > 0 && right && !bounder.isHit(x + r - vx, y - r)) {
bounder.hit();
vx *= -1;
} else if (up_left && vx < 0 && vy < 0 ||
up_right && vx > 0 && vy < 0 ||
down_left && vx < 0 && vy > 0 ||
down_right && vx > 0 && vy > 0) {
bounder.hit();
vy *= -1;
vx *= -1;
}
}
public boolean isDead() {
return !alive;
}
public void view(BallViewer viewer) {
viewer.view(x - r, y - r, size);
}
}
interface BallViewer {
public void view(int x, int y, int size);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment