Skip to content
Snippets Groups Projects
Select Git revision
  • aa0744c5b1ad89c7b2b9a45d8b439e5bcf475ba2
  • master default protected
  • v1.0.0
  • mvp-2
  • mvp-1
5 results

main.cpp

Blame
  • 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();
    }