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;
import android.util.AttributeSet;
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 {
int color;
private int color;
private Paint fill;
private Paint stroke;
ColorButton(Context context, AttributeSet attrs) {
super(context, attrs);
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
public void onClick(View view) {
System.out.println("You clicked color " + color);
GridActivity activity = (GridActivity) getContext();
activity.onClickColor(color);
gridActivity().onClickColor(color);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
GridActivity activity = (GridActivity) getContext();
int half = canvas.getHeight()/2;
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
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);
fill.setColor(gridActivity().colorFromPalette(color));
float center = getHeight()/2f;
canvas.drawCircle(center, center, center-8, fill);
canvas.drawCircle(center, center, center-7, stroke);
}
}
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 {
int edgeSize();
int numCells(); // Should always be edgeSize squared
int numCells(); // Always equals edgeSize() squared
int getColorAt(int row, int column);
int getColorAt(int index);
void setColorAt(int row, int column, int color);
void setColorAt(int index, int color);
}
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 {
private int[][] grid;
......@@ -18,7 +22,6 @@ public class FloodGridArray2D implements FloodGrid {
@Override
public int numCells() {
return grid.length * grid.length;
}
......@@ -29,7 +32,10 @@ public class FloodGridArray2D implements FloodGrid {
@Override
public int getColorAt(int index) {
return getColorAt(index / edgeSize(), index % edgeSize());
return getColorAt(
FloodGridOps.indexToRow(this, index),
FloodGridOps.indexToColumn(this, index)
);
}
@Override
......@@ -39,6 +45,10 @@ public class FloodGridArray2D implements FloodGrid {
@Override
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;
import java.io.InputStreamReader;
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 {
/* 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) {
if(numColors < 3) {
throw new IllegalArgumentException("numColors too small");
......@@ -16,6 +41,8 @@ public class FloodGridOps {
}
}
/* Convert a grid to a string, for visualization in console mode.
*/
static String toString(FloodGrid grid) {
StringBuilder buf = new StringBuilder();
for(int row = 0; row < grid.edgeSize(); row++) {
......@@ -27,6 +54,9 @@ public class FloodGridOps {
return buf.toString();
}
/* Convert a grid to a single-dimensional array, for serialization
* into bundles, etc.
*/
static int[] toArray(FloodGrid grid) {
int[] colors = new int[grid.numCells()];
for(int i = 0; i < colors.length; i++) {
......@@ -35,12 +65,21 @@ public class FloodGridOps {
return colors;
}
/* Restore grid colors from an array. The sizes must match!
*/
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++) {
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) {
int goal = grid.getColorAt(0);
for(int i = 1; i < grid.numCells(); i++) {
......@@ -51,72 +90,91 @@ public class FloodGridOps {
return true;
}
static boolean okayToVisit(
FloodGrid grid,
boolean[][] alreadyVisited,
int row,
int column,
int color)
{
return (row >= 0
&& row < grid.edgeSize()
&& column >= 0
&& column < grid.edgeSize()
&& !alreadyVisited[row][column]
&& grid.getColorAt(row, column) == color);
/* This is the essential algorithm that changes colors of contiguous regions
* of the grid. It uses a grid of booleans to track which cells have already
* been visited, then defers to two helper methods.
*/
static void flood(FloodGrid grid, int newColor) {
boolean[][] visited = new boolean[grid.edgeSize()][grid.edgeSize()];
int prevColor = grid.getColorAt(0,0);
flood(grid, visited, 0, 0, prevColor, newColor);
}
/* 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(
FloodGrid grid,
boolean[][] alreadyVisited,
boolean[][] visited,
int row,
int column,
int previousColor,
int prevColor,
int newColor)
{
alreadyVisited[row][column] = true;
visited[row][column] = true;
grid.setColorAt(row, column, newColor);
// Down
if(okayToVisit(grid, alreadyVisited, row+1, column, previousColor)) {
flood(grid, alreadyVisited, row+1, column, previousColor, newColor);
if(okayToVisit(grid, visited, row+1, column, prevColor)) {
flood(grid, visited, row+1, column, prevColor, newColor);
}
// Right
if(okayToVisit(grid, alreadyVisited, row, column+1, previousColor)) {
flood(grid, alreadyVisited, row, column+1, previousColor, newColor);
if(okayToVisit(grid, visited, row, column+1, prevColor)) {
flood(grid, visited, row, column+1, prevColor, newColor);
}
// Up
if(okayToVisit(grid, alreadyVisited, row-1, column, previousColor)) {
flood(grid, alreadyVisited, row-1, column, previousColor, newColor);
if(okayToVisit(grid, visited, row-1, column, prevColor)) {
flood(grid, visited, row-1, column, prevColor, newColor);
}
// Left
if(okayToVisit(grid, alreadyVisited, row, column-1, previousColor)) {
flood(grid, alreadyVisited, row, column-1, previousColor, newColor);
if(okayToVisit(grid, visited, row, column-1, prevColor)) {
flood(grid, visited, row, column-1, prevColor, newColor);
}
}
static void flood(FloodGrid grid, int newColor) {
boolean[][] alreadyVisited = new boolean[grid.edgeSize()][grid.edgeSize()];
int previousColor = grid.getColorAt(0,0);
flood(grid, alreadyVisited, 0, 0, previousColor, newColor);
/* The last helper for the flood algorithm just determines if it's okay to
* visit a particular coordinate -- it's in-bounds, hasn't been visited yet,
* and matches the existing color.
*/
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 {
final int numColors = 4;
FloodGrid grid = new FloodGridArray2D(6);
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));
while(!gameOver(grid)) {
System.out.print("Your choice: ");
String line = reader.readLine();
try {
int newColor = Integer.parseInt(line);
if(newColor >= 0 && newColor < numColors) {
if (newColor < 0 || newColor >= numColors)
throw new NumberFormatException();
flood(grid, newColor);
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 {
int buttonIdx = 0;
for( ; buttonIdx < numColors; buttonIdx++) {
ColorButton button = (ColorButton) buttonBar.getChildAt(2*buttonIdx+1);
button.color = buttonIdx;
button.setColor(buttonIdx);
}
// Remove any subsequent buttons and gaps
for( ; buttonIdx < MAX_COLORS; buttonIdx++) {
......
......@@ -9,86 +9,108 @@ import android.widget.RadioButton;
import android.widget.SeekBar;
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 NUM_COLORS_KEY = "NUM_COLORS";
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 TextView boardSizeValue, numColorsValue;
private RadioButton[] paletteRadios;
private PaletteSwatchesView[] paletteSwatches;
private RadioButton[] paletteRadios; // These two arrays should be
private PaletteSwatchesView[] paletteSwatches; // same length
/* 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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boardSizeValue = findViewById(R.id.boardSizeValue);
boardSizeSlider = findViewById(R.id.boardSizeSlider);
boardSizeSlider.setOnSeekBarChangeListener(this);
setBoardSizeFromSlider(boardSizeSlider.getProgress());
numColorsValue = findViewById(R.id.numColorsValue);
numColorsSlider = findViewById(R.id.numColorsSlider);
numColorsSlider.setOnSeekBarChangeListener(this);
paletteRadios = new RadioButton[] {
paletteRadios = new RadioButton[]{
findViewById(R.id.radioPalette1),
findViewById(R.id.radioPalette2),
findViewById(R.id.radioPalette3)
};
for(RadioButton r : paletteRadios) {
for (RadioButton r : paletteRadios) {
r.setOnCheckedChangeListener(this);
}
paletteSwatches = new PaletteSwatchesView[] {
paletteSwatches = new PaletteSwatchesView[]{
findViewById(R.id.swatchesPalette1),
findViewById(R.id.swatchesPalette2),
findViewById(R.id.swatchesPalette3)
};
for(int i = 0; i < paletteSwatches.length; i++) {
paletteSwatches[i].setPaletteId(paletteRadioIdArrayId(paletteRadios[i].getId()));
for (int i = 0; i < paletteSwatches.length; i++) {
paletteSwatches[i].setPaletteId(
paletteRadioToArrayId(paletteRadios[i].getId()));
}
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.)
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
private static int sliderPositionToBoardSize(int position) {
switch(position) {
case 0: return 5;
case 1: return 10;
case 2: return 15;
case 3: return 20;
default: return 7;
}
}
private void setBoardSizeFromSlider(int position) {
boardSizeValue.setText(Integer.toString(sliderPositionToBoardSize(position)));
}
private static int sliderPositionToNumColors(int position) {
return position + 3;
}
private void setNumColorsFromSlider(int position) {
int nc = sliderPositionToNumColors(position);
numColorsValue.setText(Integer.toString(nc));
for(PaletteSwatchesView psv : paletteSwatches) {
psv.setNumColors(nc);
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
switch (seekBar.getId()) {
case R.id.boardSizeSlider:
setBoardSizeFromSlider(i);
break;
case R.id.numColorsSlider:
setNumColorsFromSlider(i);
break;
}
}
......@@ -102,22 +124,12 @@ public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBar
}
/* Listener for radio buttons selecting palette. We're not using a
* RadioGroup, so have to turn other buttons off manually.
*/
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
switch(seekBar.getId()) {
case R.id.boardSizeSlider:
setBoardSizeFromSlider(i);
break;
case R.id.numColorsSlider:
setNumColorsFromSlider(i);
break;
}
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean on) {
if(on) {
// Turn everyone else off
public void onCheckedChanged(CompoundButton compoundButton, boolean isOn) {
if (isOn) { // Turn everyone else off
for (RadioButton r : paletteRadios) {
if (r.getId() != compoundButton.getId()) {
r.setChecked(false);
......@@ -126,8 +138,10 @@ public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBar
}
}
int paletteRadioIdArrayId(int id) {
switch(id) {
/* Map a radio ID to the corresponding array ID for the palette.
*/
private int paletteRadioToArrayId(int id) {
switch (id) {
case R.id.radioPalette3:
return R.array.pastels1Palette;
case R.id.radioPalette2:
......@@ -138,21 +152,29 @@ public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBar
}
}
int getSelectedPaletteId() {
/* Determine which radio button is checked, and convert that to a palette
* array ID.
*/
private int getSelectedPaletteId() {
int id = 0;
for(RadioButton r : paletteRadios) {
if(r.isChecked()) {
for (RadioButton r : paletteRadios) {
if (r.isChecked()) {
id = r.getId();
break;
}
}
return paletteRadioIdArrayId(id);
return paletteRadioToArrayId(id);
}
/* When the start-game button is pressed, package up the settings and invoke
* the game activity.
*/
public void clickStartGame(View v) {
Intent intent = new Intent(this, GridActivity.class);
intent.putExtra(BOARD_SIZE_KEY, sliderPositionToBoardSize(boardSizeSlider.getProgress()));
intent.putExtra(NUM_COLORS_KEY, sliderPositionToNumColors(numColorsSlider.getProgress()));
intent.putExtra(BOARD_SIZE_KEY, sliderPositionToBoardSize(
boardSizeSlider.getProgress()));
intent.putExtra(NUM_COLORS_KEY, sliderPositionToNumColors(
numColorsSlider.getProgress()));
intent.putExtra(PALETTE_KEY, getSelectedPaletteId());
startActivity(intent);
}
......
......@@ -70,11 +70,7 @@
<edu.liu.floodgame.ColorButton
android:layout_width="@dimen/color_button"
android:layout_height="@dimen/color_button"
android:layout_marginStart="@dimen/default_gap"
android:layout_marginLeft="@dimen/default_gap"
android:layout_marginEnd="@dimen/default_gap"
android:layout_marginRight="@dimen/default_gap" />
android:layout_height="@dimen/color_button" />