Select Git revision
-
ja3-saxby authored
It sometimes takes a long time to detect game over, once I noticed it reported game over before it appeared to be the case... Other than that, the collision-detection so far appears to be working brilliantly
ja3-saxby authoredIt sometimes takes a long time to detect game over, once I noticed it reported game over before it appeared to be the case... Other than that, the collision-detection so far appears to be working brilliantly
main.cpp 12.09 KiB
/**
* @file
*
* @brief This is my submission for Coursework Challenge 1 of UWE's Internet of
* Things module.
*
* @details This a game called "Blocks", a clone of a popular and well-known
* block-stacking game which shall not be named.
* Blocks fall from the top of the screen and the player has to try and arrange
* them in the most space-efficient way possible without letting the blocks
* stack up all the way to the top of the screen.
* @details In this version, A is used to shift the current block left and B is
* used to shift it right.
*
* @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
* @date 2020
*
* @copyright Copyright (C) Joshua Saxby 2020
*
* @copyright
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "MicroBit.h"
/*
* NOTE: this global variable is our MicroBit instance.
*
* I'd much rather declare this in main() only (I don't like globals) and
* interact with it from my classes via a reference or a pointer to it, however
* this does not appear to work, for some unknown reason...
*/
MicroBit UBIT;
/**
* @brief An (X,Y) tuple type for storing 2D cartesian quantities
*/
struct Tuple {
int x;
int y;
/**
* @brief Default constructor, zero-initialises members
*/
Tuple()
: Tuple(0, 0)
{}
/**
* @brief Simple (X, Y) Constructor
*/
Tuple(int x, int y)
: x(x)
, y(y)
{}
/**
* @brief Overloaded addition operator to allow adding tuples element-wise
*/
Tuple operator+(Tuple const &other) {
Tuple result;
result.x = this->x + other.x;
result.y = this->y + other.y;
return result;
}
/**
* @brief Overloaded subtraction operator to allow subtracting tuples
* element-wise
*/
Tuple operator-(Tuple const &other) {
Tuple result;
result.x = this->x - other.x;
result.y = this->y - other.y;
return result;
}
};
/**
* @brief Points are just Tuples
*/
typedef Tuple Point;
/**
* @brief Vectors are just Tuples
*/
typedef Tuple Vector;
/**
* @brief The different kinds of shapes our Blocks can take
*/
enum class Shape {
FLAT_L, // looks like ¬ when facing SOUTH
TALL_L, // looks like L when facing NORTH
UPSIDE_DOWN_T, // looks like T when facing SOUTH
PIPE, // looks like | when facing EAST or WEST
SQUARE, // a 2x2 square
};
const size_t SHAPE_TYPES_COUNT = 5u;
/**
* @brief Describes rotation of the block based on compass direction
* @details The top of the shape is said to be pointing in the given direction.
* @note Special magic values are chosen to allow modulo-arithmetic to produce
* easy rotation calculations
*/
enum class RotationDirection : uint8_t {
NORTH = 0u,
EAST = 1u,
SOUTH = 2u,
WEST = 3u,
};
/*
* we store the sprites for our different block shapes in FLASH, i.e. read-only
* storage. This requires manual data-packing as follows
*/
const uint8_t FLAT_L_NORTH_SPRITE[] __attribute__ ((aligned (4))) = {
0xff, 0xff, // magic number requesting data to be stored in FLASH
3, 0, // image width followed by zero
2, 0, // image height followed by zero
// image data follows:
1, 0, 0,
1, 1, 1,
};
const uint8_t TALL_L_NORTH_SPRITE[] __attribute__ ((aligned (4))) = {
0xff, 0xff, // magic number requesting data to be stored in FLASH
2, 0, // image width followed by zero
3, 0, // image height followed by zero
// image data follows:
1, 0,
1, 0,
1, 1,
};
const uint8_t UPSIDE_DOWN_T_NORTH_SPRITE[] __attribute__ ((aligned (4))) = {
0xff, 0xff, // magic number requesting data to be stored in FLASH
3, 0, // image width followed by zero
2, 0, // image height followed by zero
// image data follows:
0, 1, 0,
1, 1, 1,
};
const uint8_t SQUARE_NORTH_SPRITE[] __attribute__ ((aligned (4))) = {
0xff, 0xff, // magic number requesting data to be stored in FLASH
2, 0, // image width followed by zero
2, 0, // image height followed by zero
// image data follows:
1, 1,
1, 1,
};
const uint8_t PIPE_NORTH_SPRITE[] __attribute__ ((aligned (4))) = {
0xff, 0xff, // magic number requesting data to be stored in FLASH
3, 0, // image width followed by zero
1, 0, // image height followed by zero
// image data follows:
1, 1, 1,
};
/**
* @brief Stores the shape and colour information for a Block in the game.
*/
class Block {
public:
/**
* @brief Default constructor, randomly selects attributes for this Block.
*/
Block();
/**
* @brief Returns a read-only image of what this Block looks like
*/
MicroBitImage image() const;
private:
/**
* @brief The Shape that this Block is
*/
Shape shape;
/**
* @brief The direction the top of this Block is facing
*/
RotationDirection facing;
};
Block::Block()
: shape((Shape)UBIT.random(SHAPE_TYPES_COUNT)) // choose a random shape
, facing(RotationDirection::NORTH) // for now, all shapes face North
{}
MicroBitImage Block::image() const {
// NOTE: This doesn't handle rotation direction yet
switch (this->shape) {
case Shape::FLAT_L:
return MicroBitImage((ImageData*)FLAT_L_NORTH_SPRITE);
case Shape::TALL_L:
return MicroBitImage((ImageData*)TALL_L_NORTH_SPRITE);
case Shape::UPSIDE_DOWN_T:
return MicroBitImage((ImageData*)UPSIDE_DOWN_T_NORTH_SPRITE);
case Shape::SQUARE:
return MicroBitImage((ImageData*)SQUARE_NORTH_SPRITE);
case Shape::PIPE:
return MicroBitImage((ImageData*)PIPE_NORTH_SPRITE);
}
}
// int x = 1;
// void a_button_pressed(MicroBitEvent) {
// x--;
// }
// void b_button_pressed(MicroBitEvent) {
// x++;
// }
void debug_test() {
// XXX: simple debug test to check my FLASH images were stored correctly
// for accurately-timed animations irrespective of framerate, track time
unsigned long stopwatch = UBIT.systemTime();
unsigned long move_speed = 1000; // move down one block every 350ms
// set up event handlers
// UBIT.messageBus.listen(
// MICROBIT_ID_BUTTON_A, MICROBIT_BUTTON_EVT_CLICK, a_button_pressed
// );
// UBIT.messageBus.listen(
// MICROBIT_ID_BUTTON_B, MICROBIT_BUTTON_EVT_CLICK, b_button_pressed
// );
while (true) {
// scroll random blocks down the screen
Block block; // make a new random Block
// starting positions:
int x = 1;
int y = -3;
// print image until scrolled down off the screen
while (y < 6) {
UBIT.display.clear();
UBIT.display.print(block.image(), x, y);
{
// check if enough time has elapsed to move the block
unsigned long stopwatch_now = UBIT.systemTime();
if ((stopwatch_now - stopwatch) >= move_speed) {
y++;
// update the "stopwatch"
stopwatch = stopwatch_now;
}
}
UBIT.sleep(50); // 50ms sleep for 20 FPS
}
}
}
/**
* @brief Uses collision-detection to decide whether the given Block can move
* along the provided translation Vector.
* @param shape The Block which is to be moved
* @param origin The position of this Block (X,Y of top left corner)
* @param stacked Image containing already-stacked Block pixels
* @param move The translation Vector that is being attempted to move the Block
*/
bool can_shape_move(
Block& shape, Point origin, MicroBitImage& stacked, Vector move
) {
MicroBitImage shape_image = shape.image();
// iterate over all the pixels in the image
int width = shape_image.getWidth();
int height = shape_image.getHeight();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// if this pixel is not clear
if (shape_image.getPixelValue(x, y) != 0) {
// calculate the translated position, including origin
Point destination = origin + Point(x, y) + move;
// if Y is negative, skip this pixel as a special case
if (destination.y < 0) {
continue; // we allow Blocks to be out of the top frame
}
// if out-of-bounds, of course it can't move
if (
destination.x < 0 or
destination.x > 4 or
destination.y > 4
) {
return false;
}
// check if the translated position of this pixel is not free
if (stacked.getPixelValue(destination.x, destination.y) != 0) {
return false;
}
}
}
}
// otherwise, if we found no problems then it's ok
return true;
}
// this image contains all stacked shapes --it covers the entire screen
MicroBitImage stacked_shapes(5, 5);
// game state variables
bool game_over;
// TODO: add tracking of score
void start_new_game() {
game_over = false; // we're starting a new game
// clear the stacked shapes image
stacked_shapes.clear();
// TODO: add event-handlers for button-presses
unsigned long stopwatch = UBIT.systemTime();
unsigned long move_speed = 350; // move down one block every 350ms
// this is the main game loop
do {
// XXX: for demo, keep sending random Blocks until stacked up to ceiling
Block block;
// starting position
Point origin(1, -3);
// this is the down unit-vector
Vector down(0, 1);
// if we can't send this Block down at creation, it's game over
game_over = not can_shape_move(block, origin, stacked_shapes, down);
if (not game_over) {
bool can_move = true;
do {
// draw the scene
UBIT.display.print(stacked_shapes);
// draw Block with transparency enabled on clear image pixels
UBIT.display.print(block.image(), origin.x, origin.y, 1);
{
// check if enough time has elapsed to move the block
unsigned long stopwatch_now = UBIT.systemTime();
if ((stopwatch_now - stopwatch) >= move_speed) {
// shift the shape down
origin = origin + down;
// update the "stopwatch"
stopwatch = stopwatch_now;
}
}
// delay
UBIT.sleep(50); // 50ms sleep = 20fps
} while (can_shape_move(block, origin, stacked_shapes, down));
// the Block has now collided with the stack, copy it to the stack
stacked_shapes.paste(block.image(), origin.x, origin.y, 1);
}
} while (not game_over);
// TODO: clean up game state
// TODO: remove event-handlers
UBIT.display.scroll("GAME OVER!");
// TODO: add display of score
}
int main() {
// Initialise the micro:bit runtime.
UBIT.init();
// draw first dot to indicate runtime system is initialised
UBIT.display.image.setPixelValue(0, 2, 255);
// seed the PRNG with the HWRNG
UBIT.seedRandom();
// draw second dot to indicate PRNG has been seeded
UBIT.display.image.setPixelValue(2, 2, 255);
// ensure display is set to black and white mode
UBIT.display.setDisplayMode(DISPLAY_MODE_BLACK_AND_WHITE);
// draw third and final dot to indicate screen has been set up
UBIT.display.image.setPixelValue(4, 2, 1);
// introduce the game with a greeting message
UBIT.display.scroll("BLOCKS!");
// indefinitely start new games
while (true) {
start_new_game();
}
// TODO: potentially remove this call, if clarified that it is not required.
// If main exits, there may still be other fibers running or registered event handlers etc.
// Simply release this fiber, which will mean we enter the scheduler. Worse case, we then
// sit in the idle task forever, in a power efficient sleep.
release_fiber();
}