Add an abstract base scorer (reuse code)

This commit is contained in:
Lonami Exo 2017-02-04 20:24:46 +01:00
parent 47301864eb
commit bf0fa208f5
9 changed files with 181 additions and 162 deletions

View file

@ -91,6 +91,10 @@ public class Klooni extends Game {
return prefs.getInteger("maxScore", 0); return prefs.getInteger("maxScore", 0);
} }
public static int getMaxTimeScore() {
return prefs.getInteger("maxTimeScore", 0);
}
public static void setMaxScore(int score) { public static void setMaxScore(int score) {
prefs.putInteger("maxScore", score).flush(); prefs.putInteger("maxScore", score).flush();
} }

View file

@ -11,15 +11,15 @@ import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Align;
import io.github.lonamiwebs.klooni.Klooni; import io.github.lonamiwebs.klooni.Klooni;
import io.github.lonamiwebs.klooni.game.BaseScorer;
import io.github.lonamiwebs.klooni.game.GameLayout; import io.github.lonamiwebs.klooni.game.GameLayout;
import io.github.lonamiwebs.klooni.game.Scorer;
// Horizontal band, used to show the score on the pause menu // Horizontal band, used to show the score on the pause menu
public class Band extends Actor { public class Band extends Actor {
//region Members //region Members
private final Scorer scorer; private final BaseScorer scorer;
private final Texture bandTexture; private final Texture bandTexture;
public final Rectangle scoreBounds; public final Rectangle scoreBounds;
@ -32,7 +32,7 @@ public class Band extends Actor {
//region Constructor //region Constructor
public Band(final Klooni game, final GameLayout layout, final Scorer scorer, final Color bandColor) { public Band(final Klooni game, final GameLayout layout, final BaseScorer scorer, final Color bandColor) {
this.scorer = scorer; this.scorer = scorer;
// A 1x1 pixel map will be enough since the band texture will then be expanded // A 1x1 pixel map will be enough since the band texture will then be expanded

View file

@ -0,0 +1,97 @@
package io.github.lonamiwebs.klooni.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.utils.Align;
import io.github.lonamiwebs.klooni.Klooni;
public abstract class BaseScorer {
//region Members
final Label leftLabel;
final Label highScoreLabel;
final Texture cupTexture;
final Rectangle cupArea;
private final Color cupColor;
//endregion
//region Constructor
// The board size is required when calculating the score
BaseScorer(final Klooni game, GameLayout layout, int highScore) {
cupTexture = new Texture(Gdx.files.internal("ui/cup.png"));
cupColor = Klooni.theme.currentScore.cpy();
cupArea = new Rectangle();
Label.LabelStyle labelStyle = new Label.LabelStyle();
labelStyle.font = game.skin.getFont("font");
leftLabel = new Label("0", labelStyle);
leftLabel.setColor(Klooni.theme.currentScore);
leftLabel.setAlignment(Align.right);
highScoreLabel = new Label(Integer.toString(highScore), labelStyle);
highScoreLabel.setColor(Klooni.theme.highScore);
layout.update(this);
}
//endregion
//region Private methods
protected abstract void addScore(int score);
// The original game seems to work as follows:
// If < 1 were cleared, score = 0
// If = 1 was cleared, score = cells cleared
// If > 1 were cleared, score = cells cleared + score(cleared - 1)
private int calculateClearScore(int stripsCleared, int boardSize) {
if (stripsCleared < 1) return 0;
if (stripsCleared == 1) return boardSize;
else return boardSize * stripsCleared + calculateClearScore(stripsCleared - 1, boardSize);
}
//endregion
//region Public methods
// Adds the score a given piece would give
public final void addPieceScore(int areaPut) {
addScore(areaPut);
}
// Adds the score given by the board, this is, the count of cleared strips
public final void addBoardScore(int stripsCleared, int boardSize) {
addScore(calculateClearScore(stripsCleared, boardSize));
}
abstract public boolean isGameOver();
abstract public int getCurrentScore();
abstract public void saveScore();
abstract protected boolean isNewRecord();
public void draw(SpriteBatch batch) {
// If we beat a new record, the cup color will linear interpolate to the high score color
cupColor.lerp(isNewRecord() ? Klooni.theme.highScore : Klooni.theme.currentScore, 0.05f);
batch.setColor(cupColor);
batch.draw(cupTexture, cupArea.x, cupArea.y, cupArea.width, cupArea.height);
leftLabel.draw(batch, 1f);
highScoreLabel.draw(batch, 1f);
}
//endregion
}

View file

@ -56,7 +56,7 @@ public class GameLayout {
// coordinates. Since these objects are not actors and we cannot // coordinates. Since these objects are not actors and we cannot
// add them to a table (and would probably be harder), this approach // add them to a table (and would probably be harder), this approach
// was used. Note that all these are using Y-up coordinates. // was used. Note that all these are using Y-up coordinates.
void update(Scorer scorer) { void update(BaseScorer scorer) {
float cupSize = Math.min(scoreHeight, scorer.cupTexture.getHeight()); float cupSize = Math.min(scoreHeight, scorer.cupTexture.getHeight());
final Rectangle area = new Rectangle( final Rectangle area = new Rectangle(
marginWidth, pieceHolderHeight + boardHeight, marginWidth, pieceHolderHeight + boardHeight,
@ -66,7 +66,7 @@ public class GameLayout {
area.x + area.width * 0.5f - cupSize * 0.5f, area.y, area.x + area.width * 0.5f - cupSize * 0.5f, area.y,
cupSize, cupSize); cupSize, cupSize);
scorer.currentScoreLabel.setBounds( scorer.leftLabel.setBounds(
area.x, area.y, area.x, area.y,
area.width * 0.5f - cupSize * 0.5f, area.height); area.width * 0.5f - cupSize * 0.5f, area.height);
@ -75,25 +75,6 @@ public class GameLayout {
area.width * 0.5f - cupSize * 0.5f, area.height); area.width * 0.5f - cupSize * 0.5f, area.height);
} }
void update(TimeScorer scorer) {
float cupSize = Math.min(scoreHeight, scorer.cupTexture.getHeight());
final Rectangle area = new Rectangle(
marginWidth, pieceHolderHeight + boardHeight,
availableWidth, scoreHeight);
scorer.cupArea.set(
area.x + area.width * 0.5f - cupSize * 0.5f, area.y,
cupSize, cupSize);
scorer.timeLeftLabel.setBounds(
area.x, area.y,
area.width * 0.5f - cupSize * 0.5f, area.height);
scorer.highTimeLabel.setBounds(
area.x + area.width * 0.5f + cupSize * 0.5f, area.y,
area.width * 0.5f - cupSize * 0.5f, area.height);
}
void update(Board board) { void update(Board board) {
// We can't leave our area, so pick the minimum between available // We can't leave our area, so pick the minimum between available
// height and width to determine an appropriated cell size // height and width to determine an appropriated cell size

View file

@ -1,34 +1,20 @@
package io.github.lonamiwebs.klooni.game; package io.github.lonamiwebs.klooni.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.utils.Align;
import io.github.lonamiwebs.klooni.Klooni; import io.github.lonamiwebs.klooni.Klooni;
// Used to keep track of the current and maximum // Used to keep track of the current and maximum
// score, and to also display it on the screen. // score, and to also display it on the screen.
// The maximum score is NOT saved automatically. // The maximum score is NOT saved automatically.
public class Scorer { public class Scorer extends BaseScorer {
//region Members //region Members
private int currentScore, maxScore; private int currentScore, maxScore;
final Label currentScoreLabel;
final Label highScoreLabel;
final Texture cupTexture;
final Rectangle cupArea;
private final Color cupColor;
// If the currentScore beat the maxScore, then we have a new record // If the currentScore beat the maxScore, then we have a new record
private boolean newRecord; private boolean newRecord;
@ -41,59 +27,26 @@ public class Scorer {
// The board size is required when calculating the score // The board size is required when calculating the score
public Scorer(final Klooni game, GameLayout layout) { public Scorer(final Klooni game, GameLayout layout) {
super(game, layout, Klooni.getMaxScore());
currentScore = 0; currentScore = 0;
maxScore = Klooni.getMaxScore(); maxScore = Klooni.getMaxScore();
cupTexture = new Texture(Gdx.files.internal("ui/cup.png"));
cupColor = Klooni.theme.currentScore.cpy();
cupArea = new Rectangle();
Label.LabelStyle labelStyle = new Label.LabelStyle();
labelStyle.font = game.skin.getFont("font");
currentScoreLabel = new Label("0", labelStyle);
currentScoreLabel.setColor(Klooni.theme.currentScore);
currentScoreLabel.setAlignment(Align.right);
highScoreLabel = new Label(Integer.toString(maxScore), labelStyle);
highScoreLabel.setColor(Klooni.theme.highScore);
layout.update(this);
} }
//endregion //endregion
//region Private methods //region Private methods
private void addScore(int score) { @Override
protected void addScore(int score) {
currentScore += score; currentScore += score;
newRecord = currentScore > maxScore; newRecord = currentScore > maxScore;
} }
// The original game seems to work as follows:
// If < 1 were cleared, score = 0
// If = 1 was cleared, score = cells cleared
// If > 1 were cleared, score = cells cleared + score(cleared - 1)
private int calculateClearScore(int stripsCleared, int boardSize) {
if (stripsCleared < 1) return 0;
if (stripsCleared == 1) return boardSize;
else return boardSize * stripsCleared + calculateClearScore(stripsCleared - 1, boardSize);
}
//endregion //endregion
//region Public methods //region Public methods
// Adds the score a given piece would give
public void addPieceScore(int areaPut) {
addScore(areaPut);
}
// Adds the score given by the board, this is, the count of cleared strips
public void addBoardScore(int stripsCleared, int boardSize) {
addScore(calculateClearScore(stripsCleared, boardSize));
}
public int getCurrentScore() { public int getCurrentScore() {
return currentScore; return currentScore;
} }
@ -104,20 +57,23 @@ public class Scorer {
} }
} }
@Override
protected boolean isNewRecord() {
return newRecord;
}
@Override
public boolean isGameOver() {
return false;
}
public void draw(SpriteBatch batch) { public void draw(SpriteBatch batch) {
int roundShown = MathUtils.round(shownScore); int roundShown = MathUtils.round(shownScore);
if (roundShown != currentScore) { if (roundShown != currentScore) {
shownScore = Interpolation.linear.apply(shownScore, currentScore, 0.1f); shownScore = Interpolation.linear.apply(shownScore, currentScore, 0.1f);
currentScoreLabel.setText(Integer.toString(MathUtils.round(shownScore))); leftLabel.setText(Integer.toString(MathUtils.round(shownScore)));
} }
super.draw(batch);
// If we beat a new record, the cup color will linear interpolate to the high score color
cupColor.lerp(newRecord ? Klooni.theme.highScore : Klooni.theme.currentScore, 0.05f);
batch.setColor(cupColor);
batch.draw(cupTexture, cupArea.x, cupArea.y, cupArea.width, cupArea.height);
currentScoreLabel.draw(batch, 1f);
highScoreLabel.draw(batch, 1f);
} }
//endregion //endregion

View file

@ -1,38 +1,21 @@
package io.github.lonamiwebs.klooni.game; package io.github.lonamiwebs.klooni.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.TimeUtils; import com.badlogic.gdx.utils.TimeUtils;
import io.github.lonamiwebs.klooni.Klooni; import io.github.lonamiwebs.klooni.Klooni;
public class TimeScorer { public class TimeScorer extends BaseScorer {
//region Members //region Members
private final long startTime; private final long startTime;
// Maximum time alive, in seconds
private int maxTimeScore;
// Indicates where we would die in time. Score adds to this, so we take // Indicates where we would die in time. Score adds to this, so we take
// longer to die. To get the "score" we simply calculate `deadTime - startTime` // longer to die. To get the "score" we simply calculate `deadTime - startTime`
private long deadTime; private long deadTime;
final Label timeLeftLabel;
final Label highTimeLabel;
final Texture cupTexture;
final Rectangle cupArea;
private final Color cupColor;
private static final long START_TIME = 20 * 1000000000L; private static final long START_TIME = 20 * 1000000000L;
//endregion //endregion
@ -41,40 +24,21 @@ public class TimeScorer {
// The board size is required when calculating the score // The board size is required when calculating the score
public TimeScorer(final Klooni game, GameLayout layout) { public TimeScorer(final Klooni game, GameLayout layout) {
super(game, layout, Klooni.getMaxTimeScore());
startTime = TimeUtils.nanoTime(); startTime = TimeUtils.nanoTime();
deadTime = startTime + START_TIME; deadTime = startTime + START_TIME;
cupTexture = new Texture(Gdx.files.internal("ui/cup.png"));
cupColor = Klooni.theme.currentScore.cpy();
cupArea = new Rectangle();
Label.LabelStyle labelStyle = new Label.LabelStyle();
labelStyle.font = game.skin.getFont("font");
timeLeftLabel = new Label("0", labelStyle);
timeLeftLabel.setColor(Klooni.theme.currentScore);
timeLeftLabel.setAlignment(Align.right);
highTimeLabel = new Label(Integer.toString(nanosToSeconds(maxTimeScore)), labelStyle);
highTimeLabel.setColor(Klooni.theme.highScore);
layout.update(this);
} }
//endregion //endregion
//region Private methods //region Private methods
private void addScore(int score) { @Override
protected void addScore(int score) {
deadTime += scoreToNanos(score); deadTime += scoreToNanos(score);
} }
private int calculateClearScore(int stripsCleared, int boardSize) {
if (stripsCleared < 1) return 0;
if (stripsCleared == 1) return boardSize;
else return boardSize * stripsCleared + calculateClearScore(stripsCleared - 1, boardSize);
}
private int nanosToSeconds(long nano) { private int nanosToSeconds(long nano) {
return MathUtils.ceil((float)(nano * 1e-09)); return MathUtils.ceil((float)(nano * 1e-09));
} }
@ -84,6 +48,7 @@ public class TimeScorer {
return (long)((score / 4.0) * 1e+09); return (long)((score / 4.0) * 1e+09);
} }
@Override
public boolean isGameOver() { public boolean isGameOver() {
return TimeUtils.nanoTime() > deadTime; return TimeUtils.nanoTime() > deadTime;
} }
@ -92,27 +57,28 @@ public class TimeScorer {
//region Public methods //region Public methods
// Adds the score a given piece would give @Override
public void addPieceScore(int areaPut) { public int getCurrentScore() {
addScore(areaPut); return nanosToSeconds(deadTime - startTime);
} }
// Adds the score given by the board, this is, the count of cleared strips @Override
public void addBoardScore(int stripsCleared, int boardSize) { public void saveScore() {
addScore(calculateClearScore(stripsCleared, boardSize)); // TODO Save high time score
} }
@Override
protected boolean isNewRecord() {
// TODO Return true if it is a new record
return false;
}
@Override
public void draw(SpriteBatch batch) { public void draw(SpriteBatch batch) {
int timeLeft = Math.max(nanosToSeconds(deadTime - TimeUtils.nanoTime()), 0); int timeLeft = Math.max(nanosToSeconds(deadTime - TimeUtils.nanoTime()), 0);
timeLeftLabel.setText(Integer.toString(timeLeft)); leftLabel.setText(Integer.toString(timeLeft));
// If we beat a new record, the cup color will linear interpolate to the high score color super.draw(batch);
//cupColor.lerp(newRecord ? Klooni.theme.highScore : Klooni.theme.currentScore, 0.05f);
batch.setColor(cupColor);
batch.draw(cupTexture, cupArea.x, cupArea.y, cupArea.width, cupArea.height);
timeLeftLabel.draw(batch, 1f);
highTimeLabel.draw(batch, 1f);
} }
//endregion //endregion

View file

@ -9,6 +9,7 @@ import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import io.github.lonamiwebs.klooni.Klooni; import io.github.lonamiwebs.klooni.Klooni;
import io.github.lonamiwebs.klooni.game.BaseScorer;
import io.github.lonamiwebs.klooni.game.Board; import io.github.lonamiwebs.klooni.game.Board;
import io.github.lonamiwebs.klooni.game.GameLayout; import io.github.lonamiwebs.klooni.game.GameLayout;
import io.github.lonamiwebs.klooni.game.Piece; import io.github.lonamiwebs.klooni.game.Piece;
@ -21,8 +22,7 @@ class GameScreen implements Screen, InputProcessor {
//region Members //region Members
private final Scorer scorer; private final BaseScorer scorerlol;
private final TimeScorer timeScorer;
private final Board board; private final Board board;
private final PieceHolder holder; private final PieceHolder holder;
@ -55,12 +55,20 @@ class GameScreen implements Screen, InputProcessor {
this.gameMode = gameMode; this.gameMode = gameMode;
final GameLayout layout = new GameLayout(); final GameLayout layout = new GameLayout();
scorer = new Scorer(game, layout); switch (gameMode) {
timeScorer = new TimeScorer(game, layout); case GAME_MODE_SCORE:
scorerlol = new Scorer(game, layout);
break;
case GAME_MODE_TIME:
scorerlol = new TimeScorer(game, layout);
break;
default:
throw new RuntimeException("Unknown game mode given: "+gameMode);
}
board = new Board(layout, BOARD_SIZE); board = new Board(layout, BOARD_SIZE);
holder = new PieceHolder(layout, HOLDER_PIECE_COUNT, board.cellSize); holder = new PieceHolder(layout, HOLDER_PIECE_COUNT, board.cellSize);
pauseMenu = new PauseMenuStage(layout, game, scorer); pauseMenu = new PauseMenuStage(layout, game, scorerlol);
gameOverSound = Gdx.audio.newSound(Gdx.files.internal("sound/game_over.mp3")); gameOverSound = Gdx.audio.newSound(Gdx.files.internal("sound/game_over.mp3"));
} }
@ -78,6 +86,12 @@ class GameScreen implements Screen, InputProcessor {
return true; return true;
} }
private void doGameOver() {
pauseMenu.show(true);
if (Klooni.soundsEnabled())
gameOverSound.play();
}
//endregion //endregion
//region Screen //region Screen
@ -95,17 +109,13 @@ class GameScreen implements Screen, InputProcessor {
Klooni.theme.glClearBackground(); Klooni.theme.glClearBackground();
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// With the time mode, we always need to check whether it's game over or not if (scorerlol.isGameOver() && !pauseMenu.isShown()) {
if (timeScorer.isGameOver() && !pauseMenu.isShown()) { doGameOver();
pauseMenu.show(true);
if (Klooni.soundsEnabled())
gameOverSound.play();
} }
batch.begin(); batch.begin();
//scorer.draw(batch); scorerlol.draw(batch);
timeScorer.draw(batch);
board.draw(batch); board.draw(batch);
holder.update(); holder.update();
holder.draw(batch); holder.draw(batch);
@ -148,16 +158,12 @@ class GameScreen implements Screen, InputProcessor {
return false; return false;
if (action == PieceHolder.ON_BOARD_DROP) { if (action == PieceHolder.ON_BOARD_DROP) {
//scorer.addPieceScore(area); scorerlol.addPieceScore(area);
//scorer.addBoardScore(board.clearComplete(), board.cellCount); scorerlol.addBoardScore(board.clearComplete(), board.cellCount);
timeScorer.addPieceScore(area);
timeScorer.addBoardScore(board.clearComplete(), board.cellCount);
// After the piece was put, check if it's game over // After the piece was put, check if it's game over
if (isGameOver()) { if (isGameOver()) {
pauseMenu.show(true); doGameOver();
if (Klooni.soundsEnabled())
gameOverSound.play();
} }
} }
return true; return true;

View file

@ -66,6 +66,15 @@ public class MainMenuScreen extends InputListener implements Screen {
// Stats button (high scores) // Stats button (high scores)
final SoftButton statsButton = new SoftButton(2, "stats_texture"); final SoftButton statsButton = new SoftButton(2, "stats_texture");
// TODO For testing purposes, open the time mode
statsButton.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
MainMenuScreen.this.game.setScreen(
new GameScreen(MainMenuScreen.this.game, GameScreen.GAME_MODE_TIME));
dispose();
}
});
table.add(statsButton).space(16); table.add(statsButton).space(16);
// Palette button (buy colors) // Palette button (buy colors)

View file

@ -17,8 +17,8 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import io.github.lonamiwebs.klooni.Klooni; import io.github.lonamiwebs.klooni.Klooni;
import io.github.lonamiwebs.klooni.actors.Band; import io.github.lonamiwebs.klooni.actors.Band;
import io.github.lonamiwebs.klooni.actors.SoftButton; import io.github.lonamiwebs.klooni.actors.SoftButton;
import io.github.lonamiwebs.klooni.game.BaseScorer;
import io.github.lonamiwebs.klooni.game.GameLayout; import io.github.lonamiwebs.klooni.game.GameLayout;
import io.github.lonamiwebs.klooni.game.Scorer;
// The pause stage is not a whole screen but rather a menu // The pause stage is not a whole screen but rather a menu
// which can be overlaid on top of another screen // which can be overlaid on top of another screen
@ -33,14 +33,14 @@ class PauseMenuStage extends Stage {
private final ShapeRenderer shapeRenderer; private final ShapeRenderer shapeRenderer;
private final Band band; private final Band band;
private final Scorer scorer; private final BaseScorer scorer;
//endregion //endregion
//region Constructor //region Constructor
// We need the score to save the maximum score if a new record was beaten // We need the score to save the maximum score if a new record was beaten
PauseMenuStage(final GameLayout layout, final Klooni game, final Scorer scorer) { PauseMenuStage(final GameLayout layout, final Klooni game, final BaseScorer scorer) {
this.scorer = scorer; this.scorer = scorer;
shapeRenderer = new ShapeRenderer(20); // 20 vertex seems to be enough for a rectangle shapeRenderer = new ShapeRenderer(20); // 20 vertex seems to be enough for a rectangle