Commit 04d4637f authored by Christopher League's avatar Christopher League
Browse files

Some clean-up and documentation

parent ba85d70f
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
\ No newline at end of file
...@@ -7,36 +7,56 @@ import android.graphics.Paint; ...@@ -7,36 +7,56 @@ import android.graphics.Paint;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
/**
* This represents a button for choosing a designated color for the next move.
* It is constructed as part of the layout XML, so the color is set separately.
* Its parent context must be the GridActivity, which it notifies on each click.
* It also defers to the parent to convert the integer color code to an actual
* color, based on the current palette.
*/
public class ColorButton extends View implements View.OnClickListener { public class ColorButton extends View implements View.OnClickListener {
int color; private int color;
private Paint fill;
private Paint stroke;
ColorButton(Context context, AttributeSet attrs) { ColorButton(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
setOnClickListener(this); setOnClickListener(this);
fill = new Paint();
fill.setStyle(Paint.Style.FILL);
stroke = borderPaint();
}
void setColor(int color) {
this.color = color;
}
static Paint borderPaint() {
Paint p = new Paint();
p.setStyle(Paint.Style.STROKE);
p.setColor(Color.BLACK);
p.setStrokeWidth(5f);
p.setAntiAlias(true);
p.setAlpha(160);
return p;
}
private GridActivity gridActivity() {
return (GridActivity) getContext();
} }
@Override @Override
public void onClick(View view) { public void onClick(View view) {
System.out.println("You clicked color " + color); gridActivity().onClickColor(color);
GridActivity activity = (GridActivity) getContext();
activity.onClickColor(color);
} }
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
GridActivity activity = (GridActivity) getContext(); fill.setColor(gridActivity().colorFromPalette(color));
int half = canvas.getHeight()/2; float center = getHeight()/2f;
Paint paint = new Paint(); canvas.drawCircle(center, center, center-8, fill);
paint.setStyle(Paint.Style.FILL); canvas.drawCircle(center, center, center-7, stroke);
paint.setColor(activity.colorFromPalette(color));
canvas.drawCircle(half, half, half-1, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.BLACK);
paint.setStrokeWidth(4f);
paint.setAntiAlias(true);
paint.setAlpha(127);
canvas.drawCircle(half, half, half-3, paint);
} }
} }
package edu.liu.floodgame; package edu.liu.floodgame;
/**
* This is a generic interface to the data needed for implementing the Flood
* puzzle. The grid is always square, with edgeSize() cells along each
* dimension, and numCells() total cells. Colors are represented as small
* integers starting from zero, and you can get/set them either by (0-based) row
* and column, or by index.
*/
public interface FloodGrid { public interface FloodGrid {
int edgeSize(); int edgeSize();
int numCells(); // Should always be edgeSize squared int numCells(); // Always equals edgeSize() squared
int getColorAt(int row, int column); int getColorAt(int row, int column);
int getColorAt(int index); int getColorAt(int index);
void setColorAt(int row, int column, int color); void setColorAt(int row, int column, int color);
void setColorAt(int index, int color); void setColorAt(int index, int color);
} }
package edu.liu.floodgame; package edu.liu.floodgame;
/**
* A trivial implementation of the FloodGrid interface using a two-dimensional array,
* so we can mock up the game in console mode.
*/
public class FloodGridArray2D implements FloodGrid { public class FloodGridArray2D implements FloodGrid {
private int[][] grid; private int[][] grid;
...@@ -18,7 +22,6 @@ public class FloodGridArray2D implements FloodGrid { ...@@ -18,7 +22,6 @@ public class FloodGridArray2D implements FloodGrid {
@Override @Override
public int numCells() { public int numCells() {
return grid.length * grid.length; return grid.length * grid.length;
} }
...@@ -29,7 +32,10 @@ public class FloodGridArray2D implements FloodGrid { ...@@ -29,7 +32,10 @@ public class FloodGridArray2D implements FloodGrid {
@Override @Override
public int getColorAt(int index) { public int getColorAt(int index) {
return getColorAt(index / edgeSize(), index % edgeSize()); return getColorAt(
FloodGridOps.indexToRow(this, index),
FloodGridOps.indexToColumn(this, index)
);
} }
@Override @Override
...@@ -39,6 +45,10 @@ public class FloodGridArray2D implements FloodGrid { ...@@ -39,6 +45,10 @@ public class FloodGridArray2D implements FloodGrid {
@Override @Override
public void setColorAt(int index, int color) { public void setColorAt(int index, int color) {
setColorAt(index / edgeSize(), index % edgeSize(), color); setColorAt(
FloodGridOps.indexToRow(this, index),
FloodGridOps.indexToColumn(this, index),
color
);
} }
} }
...@@ -5,8 +5,33 @@ import java.io.IOException; ...@@ -5,8 +5,33 @@ import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.Random; import java.util.Random;
/**
* This class implements all the basic operations for the Flood Game logic,
* using the generic FloodGrid interface. It also contains a main function that
* implements a simple console version of the game.
*/
public class FloodGridOps { public class FloodGridOps {
/* Converting between single index and row/column is trivial, but it's nice
* to have these as intuitively-named operations, rather than duplicating
* the arithmetic everywhere.
*/
static int indexToRow(FloodGrid grid, int index) {
return index / grid.edgeSize();
}
static int indexToColumn(FloodGrid grid, int index) {
return index % grid.edgeSize();
}
/* The inverse conversion, from row/column to single index.
*/
static int rowColumnToIndex(FloodGrid grid, int row, int column) {
return row * grid.edgeSize() + column;
}
/* Initialize a grid with random colors.
*/
static void randomize(FloodGrid grid, Random rng, int numColors) { static void randomize(FloodGrid grid, Random rng, int numColors) {
if(numColors < 3) { if(numColors < 3) {
throw new IllegalArgumentException("numColors too small"); throw new IllegalArgumentException("numColors too small");
...@@ -16,6 +41,8 @@ public class FloodGridOps { ...@@ -16,6 +41,8 @@ public class FloodGridOps {
} }
} }
/* Convert a grid to a string, for visualization in console mode.
*/
static String toString(FloodGrid grid) { static String toString(FloodGrid grid) {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
for(int row = 0; row < grid.edgeSize(); row++) { for(int row = 0; row < grid.edgeSize(); row++) {
...@@ -27,6 +54,9 @@ public class FloodGridOps { ...@@ -27,6 +54,9 @@ public class FloodGridOps {
return buf.toString(); return buf.toString();
} }
/* Convert a grid to a single-dimensional array, for serialization
* into bundles, etc.
*/
static int[] toArray(FloodGrid grid) { static int[] toArray(FloodGrid grid) {
int[] colors = new int[grid.numCells()]; int[] colors = new int[grid.numCells()];
for(int i = 0; i < colors.length; i++) { for(int i = 0; i < colors.length; i++) {
...@@ -35,12 +65,21 @@ public class FloodGridOps { ...@@ -35,12 +65,21 @@ public class FloodGridOps {
return colors; return colors;
} }
/* Restore grid colors from an array. The sizes must match!
*/
static void fromArray(FloodGrid grid, int[] colors) { static void fromArray(FloodGrid grid, int[] colors) {
if(grid.numCells() != colors.length) {
throw new IllegalArgumentException
("Mismatched sizes of grid and colors array.");
}
for(int i = 0; i < grid.numCells(); i++) { for(int i = 0; i < grid.numCells(); i++) {
grid.setColorAt(i, colors[i]); grid.setColorAt(i, colors[i]);
} }
} }
/* Determine whether the puzzle has been solved (all the cells are the same
* color as the origin).
*/
static boolean gameOver(FloodGrid grid) { static boolean gameOver(FloodGrid grid) {
int goal = grid.getColorAt(0); int goal = grid.getColorAt(0);
for(int i = 1; i < grid.numCells(); i++) { for(int i = 1; i < grid.numCells(); i++) {
...@@ -51,72 +90,91 @@ public class FloodGridOps { ...@@ -51,72 +90,91 @@ public class FloodGridOps {
return true; return true;
} }
static boolean okayToVisit( /* This is the essential algorithm that changes colors of contiguous regions
FloodGrid grid, * of the grid. It uses a grid of booleans to track which cells have already
boolean[][] alreadyVisited, * been visited, then defers to two helper methods.
int row, */
int column, static void flood(FloodGrid grid, int newColor) {
int color) boolean[][] visited = new boolean[grid.edgeSize()][grid.edgeSize()];
{ int prevColor = grid.getColorAt(0,0);
return (row >= 0 flood(grid, visited, 0, 0, prevColor, newColor);
&& row < grid.edgeSize()
&& column >= 0
&& column < grid.edgeSize()
&& !alreadyVisited[row][column]
&& grid.getColorAt(row, column) == color);
} }
/* The first helper for the flood algorithm floods FROM a particular row and
* column, changing any contiguous cells of prevColor into newColor. It
* must consider all four directions, but only proceed if okayToVisit.
*/
static void flood( static void flood(
FloodGrid grid, FloodGrid grid,
boolean[][] alreadyVisited, boolean[][] visited,
int row, int row,
int column, int column,
int previousColor, int prevColor,
int newColor) int newColor)
{ {
alreadyVisited[row][column] = true; visited[row][column] = true;
grid.setColorAt(row, column, newColor); grid.setColorAt(row, column, newColor);
// Down // Down
if(okayToVisit(grid, alreadyVisited, row+1, column, previousColor)) { if(okayToVisit(grid, visited, row+1, column, prevColor)) {
flood(grid, alreadyVisited, row+1, column, previousColor, newColor); flood(grid, visited, row+1, column, prevColor, newColor);
} }
// Right // Right
if(okayToVisit(grid, alreadyVisited, row, column+1, previousColor)) { if(okayToVisit(grid, visited, row, column+1, prevColor)) {
flood(grid, alreadyVisited, row, column+1, previousColor, newColor); flood(grid, visited, row, column+1, prevColor, newColor);
} }
// Up // Up
if(okayToVisit(grid, alreadyVisited, row-1, column, previousColor)) { if(okayToVisit(grid, visited, row-1, column, prevColor)) {
flood(grid, alreadyVisited, row-1, column, previousColor, newColor); flood(grid, visited, row-1, column, prevColor, newColor);
} }
// Left // Left
if(okayToVisit(grid, alreadyVisited, row, column-1, previousColor)) { if(okayToVisit(grid, visited, row, column-1, prevColor)) {
flood(grid, alreadyVisited, row, column-1, previousColor, newColor); flood(grid, visited, row, column-1, prevColor, newColor);
} }
} }
static void flood(FloodGrid grid, int newColor) { /* The last helper for the flood algorithm just determines if it's okay to
boolean[][] alreadyVisited = new boolean[grid.edgeSize()][grid.edgeSize()]; * visit a particular coordinate -- it's in-bounds, hasn't been visited yet,
int previousColor = grid.getColorAt(0,0); * and matches the existing color.
flood(grid, alreadyVisited, 0, 0, previousColor, newColor); */
static boolean okayToVisit(
FloodGrid grid,
boolean[][] visited,
int row,
int column,
int color)
{
return (row >= 0
&& row < grid.edgeSize()
&& column >= 0
&& column < grid.edgeSize()
&& !visited[row][column]
&& grid.getColorAt(row, column) == color);
} }
// Console version of the game! /* A simple console version of the game!
*/
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
final int numColors = 4; final int numColors = 4;
FloodGrid grid = new FloodGridArray2D(6); FloodGrid grid = new FloodGridArray2D(6);
randomize(grid, new Random(), numColors); randomize(grid, new Random(), numColors);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); BufferedReader reader =
new BufferedReader(new InputStreamReader(System.in));
System.out.print(toString(grid)); System.out.print(toString(grid));
while(!gameOver(grid)) { while(!gameOver(grid)) {
System.out.print("Your choice: "); System.out.print("Your choice: ");
String line = reader.readLine(); String line = reader.readLine();
try {
int newColor = Integer.parseInt(line); int newColor = Integer.parseInt(line);
if(newColor >= 0 && newColor < numColors) { if (newColor < 0 || newColor >= numColors)
throw new NumberFormatException();
flood(grid, newColor); flood(grid, newColor);
System.out.print(toString(grid)); System.out.print(toString(grid));
} else { }
System.out.println("Error"); catch(NumberFormatException exn) {
System.out.printf("Error: must be integer from 0 to %d\n",
numColors-1);
} }
} }
System.out.println("Puzzle solved.");
} }
} }
...@@ -44,7 +44,7 @@ public class GridActivity extends AppCompatActivity implements FloodGrid { ...@@ -44,7 +44,7 @@ public class GridActivity extends AppCompatActivity implements FloodGrid {
int buttonIdx = 0; int buttonIdx = 0;
for( ; buttonIdx < numColors; buttonIdx++) { for( ; buttonIdx < numColors; buttonIdx++) {
ColorButton button = (ColorButton) buttonBar.getChildAt(2*buttonIdx+1); ColorButton button = (ColorButton) buttonBar.getChildAt(2*buttonIdx+1);
button.color = buttonIdx; button.setColor(buttonIdx);
} }
// Remove any subsequent buttons and gaps // Remove any subsequent buttons and gaps
for( ; buttonIdx < MAX_COLORS; buttonIdx++) { for( ; buttonIdx < MAX_COLORS; buttonIdx++) {
......
...@@ -9,86 +9,108 @@ import android.widget.RadioButton; ...@@ -9,86 +9,108 @@ import android.widget.RadioButton;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener, CompoundButton.OnCheckedChangeListener { /** This activity is the start of the app, where we choose options for the game.
* It has sliders for board size, number of colors, and a radio selection of
* the color palette. A start button transitions to the main game in
* GridActivity.
*/
public class MainActivity extends AppCompatActivity
implements SeekBar.OnSeekBarChangeListener,
CompoundButton.OnCheckedChangeListener
{
/* We'll need some string keys for passing the options to the next activity.
*/
public static final String BOARD_SIZE_KEY = "BOARD_SIZE"; public static final String BOARD_SIZE_KEY = "BOARD_SIZE";
public static final String NUM_COLORS_KEY = "NUM_COLORS"; public static final String NUM_COLORS_KEY = "NUM_COLORS";
public static final String PALETTE_KEY = "PALETTE"; public static final String PALETTE_KEY = "PALETTE";
/* Handles to the widgets on the screen. We don't need to manually
* saveInstanceState because these all manage their state for us.
*/
private SeekBar boardSizeSlider, numColorsSlider; private SeekBar boardSizeSlider, numColorsSlider;
private TextView boardSizeValue, numColorsValue; private RadioButton[] paletteRadios; // These two arrays should be
private RadioButton[] paletteRadios; private PaletteSwatchesView[] paletteSwatches; // same length
private PaletteSwatchesView[] paletteSwatches;
/* Slider positions are zero-based integers, but we're translating
* them to different ranges or preselected options.
*/
private static int sliderPositionToBoardSize(int position)
{
return (position+1) * 5; // Generates 5, 10, 15, ...
}
private static int sliderPositionToNumColors(int position) {
return position + 3; // Generates 3, 4, 5, 6, ...
}
/* We display a number in a TextView beside the sliders; these change the
* numbers.
*/
private void setBoardSizeFromSlider(int position) {
TextView boardSizeValue = findViewById(R.id.boardSizeValue);
boardSizeValue.setText(Integer.toString(sliderPositionToBoardSize(position)));
}
/* For number of colors, we additionally change the palettes swatches to
* display just the number of colors selected.
*/
private void setNumColorsFromSlider(int position) {
int nc = sliderPositionToNumColors(position);
TextView numColorsValue = findViewById(R.id.numColorsValue);
numColorsValue.setText(Integer.toString(nc));
for (PaletteSwatchesView psv : paletteSwatches) {
psv.setNumColors(nc);
}
}
/* The onCreate method initializes everything and installs listeners.
*/
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
boardSizeValue = findViewById(R.id.boardSizeValue);
boardSizeSlider = findViewById(R.id.boardSizeSlider); boardSizeSlider = findViewById(R.id.boardSizeSlider);
boardSizeSlider.setOnSeekBarChangeListener(this); boardSizeSlider.setOnSeekBarChangeListener(this);
setBoardSizeFromSlider(boardSizeSlider.getProgress()); setBoardSizeFromSlider(boardSizeSlider.getProgress());
numColorsValue = findViewById(R.id.numColorsValue);
numColorsSlider = findViewById(R.id.numColorsSlider); numColorsSlider = findViewById(R.id.numColorsSlider);
numColorsSlider.setOnSeekBarChangeListener(this); numColorsSlider.setOnSeekBarChangeListener(this);
paletteRadios = new RadioButton[] { paletteRadios = new RadioButton[]{
findViewById(R.id.radioPalette1), findViewById(R.id.radioPalette1),
findViewById(R.id.radioPalette2), findViewById(R.id.radioPalette2),
findViewById(R.id.radioPalette3) findViewById(R.id.radioPalette3)
}; };
for(RadioButton r : paletteRadios) { for (RadioButton r : paletteRadios) {
r.setOnCheckedChangeListener(this); r.setOnCheckedChangeListener(this);
} }
paletteSwatches = new PaletteSwatchesView[] { paletteSwatches = new PaletteSwatchesView[]{
findViewById(R.id.swatchesPalette1), findViewById(R.id.swatchesPalette1),
findViewById(R.id.swatchesPalette2), findViewById(R.id.swatchesPalette2),
findViewById(R.id.swatchesPalette3) findViewById(R.id.swatchesPalette3)
}; };
for(int i = 0; i < paletteSwatches.length; i++) { for (int i = 0; i < paletteSwatches.length; i++) {
paletteSwatches[i].setPaletteId(paletteRadioIdArrayId(paletteRadios[i].getId())); paletteSwatches[i].setPaletteId(
paletteRadioToArrayId(paletteRadios[i].getId()));
} }
setNumColorsFromSlider(numColorsSlider.getProgress()); setNumColorsFromSlider(numColorsSlider.getProgress());
if(savedInstanceState == null) {
System.out.println("No existing bundle");
}
else {
System.out.println("Using existing bundle");
}
} }
/* When a slider changes, update appropriately. (There are two sliders
* handled by this one listener.)
*/