Tetris game running on the Tahoe board
Two dimensional arrays in Micro Framework
Probably the biggest glitch with writing Tetris, was a fact that Micro Framework supports only one dimensional arrays. Since Tetris is about grid with falling 'grid-blocks', the two dimensional arrays are quite essential.
First step was to write ByteMatrix class which represents matrix of byte value (two dimensional byte array). Constructor of the class takes Rows and Columns argument to initiate the matrix size. Internally, values are stored in one dimensional array accessed by the GetCell(int row, int column) and SetCell(int row, int column, byte value) methods, that evaluates the appropriate index in array.
public class ByteMatrix { private byte[] _baseArray; private int _rows, _columns; public ByteMatrix(int rows, int columns) public ByteMatrix(ByteMatrix sourceMatrix) public byte GetCell(int row, int column) public void SetCell(int row, int column, byte value) public void SetCells(int row, int column, byte[] inputArray) public void Clear() #region Properties public int Rows public int Columns public int Length public byte[] BaseArray #endregion }
Separating logic and presentation layer
It's always better to separate business logic of the application and presentation layer. The code becomes better portable and maintainable as well as unit test of the logic are much easier to implement when UI is not considered. Tetris application has two namespaces GameLogic and Presentation. First one contains all classes for core game. These classes can be easily ported to .NET Compact Framework or full Framework because has no direct relation to Micro Framework. On the other hand the Presentation namespace consist of WPF controls and windows that are used to visualize the game and provides interaction with user.The base of the game is the GameUniverse class which keeps game grid, current falling block and game statistics (score, level, etc.). GameUniverse can be initialized by Init() method, that clears the game grid and game statistics. Starting game in GameUniverse is done by StartLevel(int level). Argument specifies level where game will start. StepUniverse() method runs one game step: moving block one row down a test for bottom of the grid or end of game. This method is called from presentation layer by timer.
I'am using quite lot of properties on objects - just to make code more demonstrative. However, in performance critical parts of code is better to avoid them. Properties are usually about accessing object fields by methods, so it's just another call on the stack. Especially in embedded development (Micro Framework, Compact Framework) those getter / setter properties makes not to much sense.
public class GameUniverse { // Game grid dimensions const int FIELD_COLS = 10; const int FIELD_ROWS = 20; private int blockX, blockY; private ByteMatrix currentBlock, nextBlock; private ByteMatrix field = new ByteMatrix(FIELD_ROWS, FIELD_COLS); private GameStatistics gameStats = new GameStatistics(); public void Init() public void StartLevel(int initLevel) public void StepUniverse() public void StepLeft() public void StepRight() public void Rotate() public void DropDown() private void NewBlock() private void StepDown() private bool Check(ByteMatrix block, int x, int y) private void ProcessFullLines() #region Properties public ByteMatrix CurrentBlock public ByteMatrix NextBlock public ByteMatrix Field public int BlockX public int BlockY public GameStatistics Statistics #endregion }
Tetris running in Tahoe board emulator
Persisting high score table
I've never liked those games thats forgets the high score table. Since Micro Framework has good support for persisting objects, let's save high score table to flash memory. Persistence is done by ExtendedWeakReference class, which stores serializable object passed in Target property. Prerequisite for storing object into flash memory is serializability of object itself and all it's components. Following listing shows serializable HighScoreTable class with array of serializable ScoreRecord structure./// <summary> /// Struct representing score record in high score table /// </summary> [Serializable] public struct ScoreRecord { public string Name; public int Score; } /// <summary> /// Class to keep and work with high score results /// </summary> [Serializable] public class HighScoreTable { public ScoreRecord[] Table; public HighScoreTable() public int AddRecord(ScoreRecord scoreRecord) }
Process of retrieving the stored high score table is in constructor of TetrisApp(). Static ExtendedWeakReference field is initialized by calling the CreateReference method, which takes three arguments. First two arguments are used to identify the object in perstistent storage. Third argument specifies the 'survival level'.
// Create ExtendedWeakReference for high score table highScoreEWD = ExtendedWeakReference.RecoverOrCreate( typeof(TetrisApp), 0, ExtendedWeakReference.c_SurvivePowerdown); // Set persistance priority highScoreEWD.Priority = (int)ExtendedWeakReference.PriorityLevel.Important;
Saved object is restored by reading from the Target property. In case that no object has been restored, it is necessary to create new one.
// Try to recover previously saved HighScore HighScore = (HighScoreTable)highScoreEWD.Target; // If nothing was recovered - create new if (HighScore == null) HighScore = new HighScoreTable();
PersistHighScore() method is called after every change to highscore table. By setting the Target property of ExtendedWeakReference the saving mechanism is triggered and object is saved into flash memory.
public void PersistHighScore() { // Persist HighScore by settinig the Target property // of ExtendedWeakReference highScoreEWD.Target = HighScore; }
Tetris running in Tahoe board emulator
Running the code
Game was developed and tested on Tahoe board from Embedded Fusion. Since there is no additional hardware required, it is possible to run game just in emulator. For those who don't own the Tahoe board, SDK with Tahoe emulator can be downloaded from Embedded Fusion support page.
The only one platform specific option in source code is in GPIOButtonInputProvider.cs file on lines 39-47. It is an button mapping for specific pins of the CPU. I'am using pins on Meridian CPU which are wired to Up, Down, Left, Right and Select button on Tahoe development kit.
ButtonPad[] buttons = new ButtonPad[] { // Associate the buttons to the pins as setup in the emulator/hardware new ButtonPad(this, Button.Up , Meridian.Pins.GPIO5), new ButtonPad(this, Button.Down , Meridian.Pins.GPIO9), new ButtonPad(this, Button.Left , Meridian.Pins.GPIO6), new ButtonPad(this, Button.Right , Meridian.Pins.GPIO6), new ButtonPad(this, Button.Select, Meridian.Pins.GPIO7), };