Hades logoHades applet banner
PIC16F628 Tetris for Terminals game

applet icon

The image above shows a thumbnail of the interactive Java applet embedded into this page. Unfortunately, your browser is not Java-aware or Java is disabled in the browser preferences. To start the applet, please enable Java and reload this page. (You might have to restart the browser.)

Circuit Description

This applet demonstrates a version of the famous Tetris game running on the PIC16F628 microcontroller. The circuit consists of the microcontroller connected to a standard serial terminal.

The program running on the microcontroller (see below) first sets up the serial communication parameters (currently 9600 baud 8N2), initializes the game board, and enables timer interrupts. The main program then enters an endless loop which calls the check_handle_command() function over and over. This function first checks whether the 'game over' bit is set in the global 'state' variable, in which case only the 'restart game' command is accepted. Next, the 'timer overflow' bit in the 'state' variable is checked. If set, the current block falls down one position and the bit is cleared. The 'timer overflow' bit is set again in the interrupt handler function on the next timer0 register overflow (T0IF). Otherwise, the function checks whether a user input command has been received from the terminal, and tries to execute the command.

With the simulation running, use a mouse click (left button) to transfer the keyboard focus to the terminal window. Afterwards, type the following keys into the terminal window to control the game:

  • j: move block left
  • k: rotate block
  • l: move block right
  • space: drop block (space)

  • 'r': redraw everything
  • 's': start new game (quit current game)

  • 'f': faster
  • 'd': slower

Depending on your computer and Java virtual machine, the game might run slowly; on my Athlon-2600 (2 GHz) with JDK 1.5.0 the speed is a bit slower than the very first (easy) levels of the real Tetris game. This is limited by the simulation of the microcontroller with all its peripheral functions and the frequent repainting of the terminal window. When built as a real hardware device and connected to a terminal, the game will be pretty fast, and you might want to change the timer initialization values (or prescaler) used in vt100_initialize() and the isr() functions.

Due to severe code-size limitations - the picclite compiler only supports the PIC16F627 with its program memory of 1024 instructions - only a slightly simplified version of the original game is implemented. Most importantly, the static game board only shows whether a position is occupied, but not the type of block dropped to that position. However, the game board (24 rows of 10 columns) has the standard size, and all seven types of 4-characters block from the original game are supported.

In case you want build the circuit yourself, you can download the following files:

The program running on the microcontroller is based on the following C source code:

/* PIC16F628 Tetris for Terminals (2nd version)
 * 
 * This program implements a (simplified) version of the famous 'Tetris'
 * game with a PIC16F628 connected to a serial terminal (VT52 or higher).
 * Just connect the terminal to the PIC RX/TX ports, select the required
 * communication settings (9600 baud 8N1) on the terminal, and you are
 * ready to go. 
 * 
 * The program assumes an input clock of 1.075 MHz (in order to generate 
 * the correct baud-clock for 9600 baud communication), but it might also
 * work with 1.00 MHz clock if your terminal tolerates deviations from the 
 * exact baud-clock. The low clock rate was chosen to make the program
 * (almost) useable in a simulator; you might want to use 4 MHz or 16 MHz
 * and 19200 or 38400 kbaud for real hardware. This requires changing the
 * RX/TX settings in vt100_initialize().
 * 
 * Use the following keys to control the game:
 * 
 * 'j'        - move current block left
 * 'k'        - rotate current block
 * 'l'        - move current block right
 * 'space'    - drop current block               
 * 'r'        - redraw the screen
 * 'q'        - quit (give up current game)
 * 'f'        - faster
 * 'd'        - slower
 * 
 * Tip: if you run the demo via the Hades simulator, please type at most
 * one command key during each repaint iteration. Otherwise, the 16F628
 * receive buffer will overrun, and the 16F628 simulation model seems to
 * misbehave afterwards - it reacts only to each second keypress afterwards,
 * which makes playing much more difficult :-)
 * 
 * Originally intended for the PIC16F84 with only 68 bytes of RAM,
 * the program uses a very compact representation of the gaming board,
 * with one bit per position (1=occupied 0=empty). All accesses to the
 * gaming board should be via the setPixel() and occupied() functions.
 * A standard gaming board of 24 rows of 10 columns each is used.
 * Note that the decision for a 1-bit representation means that we lose
 * the option to display the original type (color) of the different blocks.
 * Unfortunately, I found no way to tweak the program into the 16F84
 * via the picclite compiler, so I switched to the pincompatible 16F628.
 * The original 1-bit datastructures were kept, though the 16F628 should
 * have enough RAM to support storing the block-type in the gaming board.
 *
 * Similarly, the current block is represented via a 4x4 bitmap stored
 * in the 'current_block<0..3>' variables, where the lower nibble of each
 * byte specify the 4 bits of one row of the current block. New blocks
 * are created via the create_block() and create_random_block() functions,
 * and rotate_block() uses some bit-fiddling to rotate the block inplace
 * by 90 degrees. The current position (row, column) of the current block
 * is maintained in the current_row and current_col variables, and the
 * test_if_block_fits() functions checks whether the block fits within the
 * gaming board bounds and the already placed blocks.
 * 
 * The program enables interrupts for both the timer0 interrupts and
 * the serial-port receiver interrupts (user input via the terminal).
 * The interrupt handler just copies the received input character to the
 * 'command' variable, and sets a bit in the 'state' variable on a timer
 * overflow.
 * On each iteration of the check_handle_command() function, the 'state'
 * variable is checked for the timer overflow bit. If set, the program
 * attempts to let the current block fall down one position and handles
 * the resulting situation (collapsing completed rows, or setting the
 * game-over bit if the block is stuck at the topmost row).
 * Otherwise, the check_handle_command() bit copies the contents of the
 * command variable to a tmp variable, resets the command variable, and
 * then continues to check for and execute the given user command.
 * 
 * Despite using an 'incremental repainting' strategy of first erasing and
 * then overdrawing the current block during the game-play, the performance
 * of the program is still limited by the slow serial connection.
 * A single block movement still needs four VT52 goto commands and writes
 * four characters to erase a block, and another four gotos and writes to
 * redraw the block. This alones takes 40*(1+8+1)*(1/baudrate) seconds - 
 * about 40 msecs at 9600 baud. 
 * While the program uses the 16F628 transmitter register to send characters,
 * it also uses a wait loop in function vt100_putc() to avoid overrunning
 * the transmitter. This is far simpler than trying to maintain a send buffer,
 * but it also means that we are likely to drop user input requests, because
 * the sending is triggered from within the interrupt handler. Also, note
 * that the wait-loop limits in vt100_putc() need to be adapted when changing 
 * the terminal baud-rate or the 16F628 input clock.
 *
 * TODO:
 * The program was written for the free Picclite compiler (9.50PL1), which
 * supports the 16F627 but not the 16F628. This means that the program must
 * fit into the first 1K words of program memory, which severely limits the
 * things we can do. At the moment, there are just seven unused words left
 * free in the program memory...
 * 
 * 
 * If you have access to the full version of the compiler or another compiler
 * for the 16F628, you could try to re-instate the uncommented functions
 * and to add some functions that were omitted due to program-memory size
 * limitiations. For example:
 * - show the 'level' and 'score' info after each new block
 * - distinguish the different block-types via different characters
 * - distinguish the 'score' between block-types
 * - add highlighting of 'collapsing' rows before redrawing the game-board
 * - implement the 'pause' command
 * - make timer0 delays smaller with increasing game level
 * - add high-score list management, could use the data EEPROM
 * - program should run with watchdog timer enabled (needs extra clrwdt)
 * - etc.
 *
 * Note: see http://www.rickard.gunee.com/projects for a really amazing
 * assembly-code version of Tetris for the PIC16F84 - with direct connection
 * to a PAL/NTSC black&white television set, and even including audio!
 * I only learned about that version after writing this C program, however. 
 * Fortunately, data-structures and timing of both programs are quite different.
 * 
 * Note: make sure to set the UART initialization code to match the
 * selected clock rate. You might also have to play with the TMR0
 * reload value in the interrupt handler to optimize the game-play
 * (not too slow, but neither too fast...)
 * 
 * 
 * (C) 2005, 2006 fnh hendrich@informatik.uni-hamburg.de
 * 
 * 2006.03.23 - change vt100_putc to support 4MHz and 250 kHz clock
 * 2006.03.20 - ifdef stuff to change between 4 MHz and 250 kHz clock
 * 2006.03.06 - change 16F628 to use XT osc. mode at 4.000 MHz
 * 2006.01.04 - reorganize main-loop <-> isr interaction
 * 2005.12.29 - implemented check_remove_complete_rows()
 * 2005.12.28 - first working version (with interrupts)
 * 2005.12.27 - first code (game board drawing etc)
 */

#include "init.h"	// included by C-Wiz
#include "htc.h"
#include "stdlib.h"    // we need rand()
#include "tetris.h"

// NOTE: YOU MUST SELECT AND UNCOMMENT ONE OF THOSE MACROS.
// Depending on the selected clock-rate of course - a 4 MHz clock
// is a good choice for the actual real hardware, while 250 kHz
// means that the Hades simulation will run near real-time.
// For other input clock rates, change the RX/TX baudrate stuff
// and the TMR0 and "divider"/"divider_limit" values to get an
// acceptable game speed.
//

#define CLK4MHZ   4000000
//#define CLK250KHZ  250000



#define ROWS  ((unsigned char) 24)
#define COLS  ((unsigned char) 10)

#define PAINT_FIXED  ((unsigned char) 2)
#define PAINT_ACTIVE ((unsigned char) 1)
#define ERASE        ((unsigned char) 0)

#define XOFFSET ((unsigned char) 3)
#define XLIMIT  ((unsigned char) (XOFFSET+COLS))



static unsigned char board[30];         // the main game-board

static unsigned char current_block0;    // bit-pattern of the current block,
static unsigned char current_block1;    // with one four-bit bitmap stored
static unsigned char current_block2;    // in the lower nibble of each of these
static unsigned char current_block3;    // variables

static signed char current_row;         // row of the current block
static signed char current_col;         // col of the current block

static unsigned char level;
static unsigned char score;

// the possible user commands encoded as one-byte constants
//
/*
#define CMD_LEFT     ((unsigned char) 0x01)
#define CMD_RIGHT    ((unsigned char) 0x02)
#define CMD_ROTATE   ((unsigned char) 0x04)
#define CMD_DROP     ((unsigned char) 0x08)
#define CMD_REDRAW   ((unsigned char) 0x10)

#define GAME_OVER    ((unsigned char) 0x40)
#define NEEDS_INIT   ((unsigned char) 0x80)
*/

#define STATE_IDLE   ((unsigned char) 0x1)
#define TIMEOUT      ((unsigned char) 0x2)
#define NEEDS_INIT   ((unsigned char) 0x4) 
#define GAME_OVER    ((unsigned char) 0x8) 


#define CMD_NONE     ((unsigned char) 0)
#define CMD_LEFT     ((unsigned char) 'j')
#define CMD_RIGHT    ((unsigned char) 'l')
#define CMD_ROTATE   ((unsigned char) 'k')
#define CMD_DROP     ((unsigned char) ' ')
#define CMD_REDRAW   ((unsigned char) 'r')
#define CMD_START    ((unsigned char) 's')

#define CMD_FASTER   ((unsigned char) 'f')
#define CMD_SLOWER   ((unsigned char) 'd')

// high-scores: 1 point per new block, 20 points per completed row
#define SCORE_PER_BLOCK  ((unsigned char) 1)
#define SCORE_PER_ROW    ((unsigned char) 20)



static unsigned char  state;
static unsigned char  command;

static unsigned char  divider;
static unsigned char  divider_limit;
static unsigned char  heartbeat;





/**
 * utility function to calculate (1 << nbits)
 */
unsigned char power_of_two( unsigned char nbits ) {
  unsigned char mask;
  
  mask = (1 << nbits);
  return mask;	
}


/**
 * utility function that returns the bit-patterns of the seven 
 * predefined types (shapes) of Tetris blocks via ROM lookups.
 * This method returns the block in the standard orientation only.
 * We use a 4x4 matrix packed into two unsigned chars as
 * (first-row << 4 | second-row), (third-row << 4 | fourth-row).
 */ 
unsigned char getRawBlockPattern( unsigned char index ) {
  switch( index ) {
  	          // brown square 2x2
    case 0:   return  0b00000110;
    case 1:   return  0b01100000;
  	
  	          // brown square 2x2
    case 2:   return  0b00000110;
    case 3:   return  0b01100000;

              // yellow cursor-type block    
    case 4:   return  0b00001110;
    case 5:   return  0b01000000;

              // red 1x4 block
    case 6:   return  0b01000100;
    case 7:   return  0b01000100;

              // blue 2+2 shifted block
    case 8:   return  0b00001100;
    case 9:   return  0b01100000;

              // green=2+2 shifted block (inverse to blue)
    case 10:  return  0b00000110;
    case 11:  return  0b11000000;

              // cyan=3+1 L-shaped block
    case 12:  return  0b00001110;
    case 13:  return  0b00100000;

              // lilac 1+3 L-shaped block
    case 14:  return  0b00001110;
    case 15:  return  0b10000000;
    
              // we will not arrive here, but the compiler wants this
    default:  return  0b11111111; 
  }
}


/**
 * fill the current_block0..3 variables with the bit pattern of the
 * selected block type (0,1..7).
 * 
 * Implementation note: the first version of the code used an array
 * current_block[4], which made for clean C sources but very clumsy
 * assembly code after compiling. Switching to four separate variables
 * with two utility functions reduced the code size and made the program
 * fit into the 16F627...
 */
void create_block( unsigned char index ) {
  unsigned char tmp;

  tmp = getRawBlockPattern(index+index);
  current_block0 = (tmp & 0xf0) >> 4;
  current_block1 = (tmp & 0x0f);

  tmp = getRawBlockPattern(index+index+1);
  current_block2 = (tmp & 0xf0) >> 4;
  current_block3 = (tmp & 0x0f);
}


/**
 * create a new randomly-chosen block.
 * Note that rand() doesn't seem to return zero at all,
 * so the seven types of blocks are indexed via values 1..7
 * instead of 0..6.
 */
void create_random_block( void ) {
  unsigned char x;
  x = rand() & 0x7;
  create_block( x );	
}


/**
 * helper function to access one nibble (row) of the current block.
 */
unsigned char getBlockNibble( unsigned char i ) {
  unsigned char tmp;
  switch( i ) {
  	case 0: tmp = current_block0; break;
  	case 1: tmp = current_block1; break;
  	case 2: tmp = current_block2; break;
  	case 3: tmp = current_block3; break;
  }
  return tmp;
}


/**
 * helper function for rotate_block()
 */
void updateBlockNibble( unsigned char i, unsigned char mask ) {
  switch( i ) {
    case 0: current_block0 |= mask; break;
    case 1: current_block1 |= mask; break;  	
    case 2: current_block2 |= mask; break;
    case 3: current_block3 |= mask; break;  	
  }
}


/**
 * rotate the current_block data by 90 degrees (counterclockwise).
 * We use an inplace rotation that uses the upper nibbles of the
 * current_block0..3 variables as intermediate storage.
 * 
 * 
 * (- - - -   a b c d)           (a b c d   d h l p)
 * (- - - -   e f g h)           (e f g h   c g k o)
 * (- - - -   i j k l)   =>      (i j k l   b f j n)
 * (- - - -   m n o p)           (m n o p   a e i m)
 * 
*/ void rotate_block( void ) { unsigned char i, j; // move lower nibble to upper nibbles current_block0 = current_block0 << 4; current_block1 = current_block1 << 4; current_block2 = current_block2 << 4; current_block3 = current_block3 << 4; // fill in the rotated bits for( i=0; i < 4; i++ ) { for( j=0; j < 4; j++ ) { if ((getBlockNibble(i) & power_of_two(j+4)) != 0) { updateBlockNibble( j, power_of_two(3-i) ); } } } // clear upper nibbles again. This is not strictly required, // and uncommented for now to conserve program memory... // for( i=0; i < 4; i++ ) { // current_block[i] = current_block[i] & 0x0f; // } } /** * check whether the current block has a pixel at position(row,col) */ bit getBlockPixel( unsigned char row, unsigned char col ) { //return (current_block[row] & power_of_two(col) != 0; unsigned char tmp; tmp = getBlockNibble( row ); return (tmp & power_of_two(col)) != 0; } /** * accessor function for the gaming board position at (row,col). * Use val=1 for occupied and val=0 for empty places. */ void setPixel( unsigned char row, unsigned char col, unsigned char val ) { unsigned char index; unsigned char mask; index = (row >> 3)*10 + col; mask = power_of_two(row & 0x7); if (val == 0) board[index] &= ((unsigned char) ~mask); else board[index] |= mask; } /** * check whether the gaming board position at (row,col) is occupied. */ bit occupied( unsigned char row, unsigned char col ) { unsigned char index; unsigned char mask; index = (row >> 3)*10 + col; mask = power_of_two(row & 0x7); return (board[index] & mask) != 0; } /** * clear the whole gaming board. */ void clear_board( void ) { unsigned char r, c; for( r=0; r < ROWS; r++ ) { for( c=0; c < COLS; c++ ) { setPixel( r, c, 0 ); } } } /** * create a randomly populated gaming board. */ /* Uncommented to save program memory space. void random_board( void ) { unsigned char r, c, val; for( r=0; r < ROWS; r++ ) { for( c=0; c < COLS; c++ ) { val = rand() & 0x01; setPixel( r, c, val ); } } } */ /** * check whether the current block fits at the position given by * (current_row, current_col). * Returns 1 if the block fits, and 0 if not. */ bit test_if_block_fits( void ) { unsigned char i, j; signed char ccj; for( i=0; i < 4; i++ ) { for( j=0; j < 4; j++ ) { if (getBlockPixel(i,j)) { ccj = current_col + j; if (ccj < 0) return 0; // too far left if (ccj >= 10) return 0; // too far right if (current_row+i >= ROWS) return 0; // too low if (occupied(current_row+i, ccj)) return 0; } } } return 1; // block fits } /** * copy the bits from the current block at its current position * to the gaming board. This means to fix the current 'foreground' * block into the static 'background' gaming-board pattern. */ void copy_block_to_gameboard( void ) { unsigned char i, j; unsigned char cri, ccj; for( i=0; i < 4; i++ ) { cri = current_row + i; for( j=0; j < 4; j++ ) { ccj = current_col + j; if (getBlockPixel(i,j)) { setPixel( cri, ccj, 0x01 ); } } } } /** * check whether the specified game-board row (0=top,23=bottom) * is complete (all bits set) or not. */ bit is_complete_row( unsigned char r ) { unsigned char c; for( c=0; c < COLS; c++ ) { if (!occupied(r,c)) return 0; } return 1; } /** * remove one (presumed complete) row from the game-board, * so that all rows above the specified row drop one level. */ void remove_row( unsigned char row ) { unsigned char r, c, tmp; for( c=0; c < COLS; c++ ) { for( r=row; r>0; r-- ) { tmp = occupied( r-1, c ); setPixel( r, c, tmp ); } // finally, clear topmost pixel setPixel( 0, c, 0 ); } } /** * check for completed rows and remove them from the gaming board. */ void check_remove_completed_rows( void ) { unsigned char r, flag; flag = 0; for( r=0; r < ROWS; r++ ) { if (is_complete_row(r)) { flag = 1; remove_row( r ); level++; } } if (flag) { display_board(); display_score(); } } /** * try to move the current block left. * This function updates the current_col variable, but does not * repaint the current block. */ void cmd_move_left( void ) { current_col --; if (test_if_block_fits()) return; // if we arrive here, the block doesn't fit, // and we simply undo the column change. current_col ++; } /** * try to move the current block right. * This function updates the current_col variable, but does not * repaint the current block. */ void cmd_move_right( void ) { current_col++; if (test_if_block_fits()) return; // if we arrive here, the block doesn't fit, // and we simply undo the column change. current_col --; } /** * try to rotate the current block. * This function updates the current_block(0..3) variables, * but does not repaint the current block. */ void cmd_rotate( void ) { rotate_block(); if (test_if_block_fits()) return; // if we arrive here, the block doesn't fit, // and we undo the rotation by three more rotations... rotate_block(); rotate_block(); rotate_block(); } /** * attempt to move the current block one position down. * If it doesn't fit, copy the current block to the gaming board * and check for completed rows. If any completed rows are found, * check_remove_completed_rows() also automatically repaints the * whole gaming board. * Finally, we create a new random block (and let our caller * handle repainting the new block). */ void cmd_move_down( void ) { current_row++; if (test_if_block_fits()) return; // fits if (current_row <= 2) { // already stuck right on top state |= GAME_OVER; } // if we arrive here, the block doesn't fit, // and we have to copy it to the game board. // we also need a new random block... current_row--; display_block( PAINT_FIXED ); // this is now stuck copy_block_to_gameboard(); check_remove_completed_rows(); // repaints all when necessary current_row = 0; current_col = 4; level ++; create_random_block(); score += SCORE_PER_BLOCK; display_block( PAINT_ACTIVE ); } /** * initialize the 16F628 serial communication registers. * We also put some timer initialization here. */ void vt100_initialize( void ) { TRISB1 = 1; // port B1 is RX pin TRISB2 = 0; // port B2 is TX pin RB2 = 1; // output idle for now /* 16F628 only */ SYNC = 0; // asynchronous mode #ifdef CLK4MHZ SPBRG = 25; // 9615 baud (+0.16%) at 4 MHz clock BRGH = 1; #elif CLK250KHZ SPBRG = 2; // roughly 9600 baud at 250 KHz clock BRGH = 1; #else SPBRK = XXX; // intentional error here. Make sure to #define one BRGH = 1; // of the above macros. #endif // SPBRG = 6; // 38400baud at 4MHz 9600 baud at 1MHz TX9 = 1; // select 9-bit transmission TX9D = 1; // ninth bit is one (extra stop bit) RX9 = 0; // 8N1 8-bit data RCIF = 0; // zera flag de interrupcao de recepcao na USART RCIE = 1; // habilita interrupcao de recepcao na USART SPEN = 1; // habilita USART CREN = 1; // habilita recepcao na USART PEIE = 1; // habilita interrupcao em perifericos T0IE = 1; T0CS = 0; // use clock as timer source // at 4MHz input clock, we need a timer0 prescaler, // with PSA=0, PS<2:0>=011 (ratio 1:16) PSA = 0; // option<3>, prescaler assigned to timer0 // PS2 = 0; // option<2> // PS1 = 1; // option<1> // PS0 = 1; // option<0> // THIS DOES NOT WORK: OPTION = (OPTION & 0xf0) | 0xf0; TMR0 = 1; } /** * send "ESC [ ? 2 l" to put a VT100 into VT52 mode * and "ESC f" to hide the cursor. */ void vt100_enter_vt52_mode() { // VT100: enter VT52 mode vt100_putc( 27 ); vt100_putc( '[' ); vt100_putc( '?' ); vt100_putc( '2' ); vt100_putc( 'l' ); // VT52: cursor off vt100_putc( 27 ); vt100_putc( 'f' ); } /** * transmit the given character via tx to the terminal. * We assume that we can immediately put the new data into the transmit * buffer register TXREG. Afterwards, we should wait until the TRMT * (TXSTA<1>) bit is 1 to indicate that TSR is empty again. * Unfortunately, this doesn't work reliably with the Hades 16F628 * simulation model, so that we use an explicit wait loop instead. * You will have to update the wait loop limit when chaning the * RS232 communication parameters (baudrate) and PIC input clock * frequency. */ void vt100_putc( unsigned char ch ) { unsigned char count; TXREG = ch; // the character to be transmitted TXEN = 1; // start transmission // TRMT DOESNT WORK RELIABLY ON HADES PIC16F628 YET // for( ;; ) { // if (TRMT) break; // } #ifdef CLK4MHZ for( count=0; count < 250; count++ ) { // 4 MHz 9600 8N1 RB6 = 0; RB6 = 1; } #elif CLK250KHZ for( count=0; count < 15; count++ ) { // 250 kHz 9600 8N1 RB6 = 0; RB6 = 1; } #else SPBRK = XXX; // intentional error here. Make sure to #define CLKxxx #endif } /** * move the VT100 cursor to the given position. * This is done by sending 'ESC Y l c' * NOTE: VT52 expects an offset of 32 for the l and c values. */ void vt100_goto( unsigned char row, unsigned char col ) { vt100_putc( 27 ); // ESC vt100_putc( 'Y' ); // ESC-Y vt100_putc( (row+32) ); vt100_putc( (col+32) ); } /** * request a VT100 cursor-home command on the terminal via tx. * We send the VT100/VT52 command 'ESC H'. */ void vt100_cursor_home( void ) { vt100_putc( 27 ); // ESC vt100_putc( 'H' ); } /** * helper function to convert a hex-digit into a character 0..9 a..f */ unsigned char vt100_hex( unsigned char val ) { if (val <= 9) return '0' + val; else return ('a'-10) + val; } /** * print the given integer value as two hex-digits. */ void vt100_xtoa( unsigned char val ) { unsigned char c; c = val >> 4; vt100_putc( vt100_hex( c )); c = val & 0x0f; vt100_putc( vt100_hex( c )); } /** * display the current game board position on the terminal. * This method redraws the whole gaming board including borders. * Use another call to display_block() to also draw the current block. */ void display_board( void ) { unsigned char r,c; unsigned char ch; vt100_cursor_home(); for( r=0; r < ROWS; r++ ) { vt100_goto( r, 2 ); vt100_putc( '|' ); // one row of the board: border, data, border for( c=0; c < COLS; c++ ) { ch = occupied(r,c) ? 'X' : ' '; vt100_putc( ch ); } vt100_putc( '|' ); // a real VT52 wants both linefeed (10) and carriage-return (13). // We don't want a linefeed after the last row, because the terminal // would scroll up then... anyway, we use vt100_goto() now. // // vt100_putc( 13 ); // vt100_putc( 10 ); } } /** * debugging display of the current block outside the gaming-board * area. * void display_test_block( void ) { unsigned char i, j, ch; for( i=0; i < 4; i++ ) { vt100_goto( i+11, 30 ); for ( j=0; j < 4; j++ ) { ch = (getBlockPixel(i,j)) ? 'X' : '.'; vt100_putc( ch ); } } } */ /** * display (or erase) the current block at its current position, * depending on whether the paintMode parameter is PAINT_ACTIVE, * PAINT_FIXED, or ERASE. * This method just paints the active pixels from the block, * but nothing else (no game board, no borders, no score). * To achieve the best performance, we use the VT52 cursor * positioning commands via vt100_goto() for each of the * (always four) visible pixels of the block. */ void display_block( unsigned char paintMode ) { unsigned char i, j, ch; unsigned char rr, cc; for( i=0; i < 4; i++ ) { rr = current_row + i; if (rr >= ROWS) continue; // out of range for ( j=0; j < 4; j++ ) { cc = XOFFSET + current_col + j; if (cc >= XLIMIT) continue; // out of range if (getBlockPixel(i,j)) { vt100_goto( rr, cc ); if (paintMode == PAINT_ACTIVE) vt100_putc( 'H' ); else if (paintMode == PAINT_FIXED) vt100_putc( 'X' ); else vt100_putc( ' ' ); } } // HACK: // extra goto because neither xterm/seycon nor Winxp hyperterm // understand the VT52 "cursor off" command, and the blinking // cursor at the end of a block is really annoying... // a "real" VT100/VT52 does work fine without this. vt100_goto( 1, 1 ); } } // display_block /** * display the current level and score values on the terminal. */ void display_score( void ) { vt100_goto( 20, 40 ); /* uncommented to free some program memory for more important things... vt100_putc( 'L' ); vt100_putc( 'e' ); vt100_putc( 'v' ); vt100_putc( 'e' ); vt100_putc( 'l' ); vt100_putc( ':' ); vt100_putc( ' ' ); */ vt100_xtoa( level ); vt100_putc( ' ' ); /* vt100_goto( 21, 40 ); vt100_putc( 'S' ); vt100_putc( 'c' ); vt100_putc( 'o' ); vt100_putc( 'r' ); vt100_putc( 'e' ); vt100_putc( ':' ); vt100_putc( ' ' ); */ vt100_xtoa( (score>>8) ); vt100_xtoa( (score) ); //vt100_putc( '\n' ); } /* void wait( void ) { unsigned char count; for( count=0; count < 250; count++ ) { RB5 = 1; RB5 = 0; } } */ void init_game( void ) { GIE = 0; // disable interrupts while we're initializing vt100_enter_vt52_mode(); clear_board(); display_board(); current_row = 0; current_col = 4; create_random_block(); score = SCORE_PER_BLOCK; // one block created right now level = 0; divider = 0; //divider_limit = 10; state = STATE_IDLE; command = CMD_NONE; GIE = 1; // (re)-enable interrupts now display_block( PAINT_ACTIVE ); } bit gameover( void ) { return (state & GAME_OVER) != 0; } bit timeout( void ) { return (state & TIMEOUT) != 0; } void check_handle_command( void ) { unsigned char tmp; // if the game is over, we only react to the 's' restart command. // if (gameover()) { if (command == CMD_START) { init_game(); } display_score(); // flickers somewhat :-( return; } // first check game status and a possible timeout. // (we don't want the user to block timeouts by just overflowing // us with keystrokes :-)) // if (timeout()) { display_block( ERASE ); cmd_move_down(); display_block( PAINT_ACTIVE ); state |= TIMEOUT; state ^= TIMEOUT; // reset timeout flag return; } // now, check whether we have a command. If so, handle it. // tmp = command; command = CMD_NONE; RB4 = 0; switch( tmp ) { case CMD_NONE: // idle break; case CMD_LEFT: // try to move the current block left display_block( ERASE ); cmd_move_left(); display_block( PAINT_ACTIVE ); break; case CMD_ROTATE: // try to rotate the current block display_block( ERASE ); cmd_rotate(); display_block( PAINT_ACTIVE ); break; case CMD_RIGHT: // try to move the current block right display_block( ERASE ); cmd_move_right(); display_block( PAINT_ACTIVE ); break; case CMD_DROP: // drop the current block display_block( ERASE ); for( ; test_if_block_fits(); ) { current_row++; } // now one step back, then paint the block in its // final position. current_row--; display_block( PAINT_FIXED ); // this checks for completed rows and does the // major cleanup and redraw if necessary. cmd_move_down(); break; case CMD_REDRAW: // redraw everything display_board(); display_score(); display_block( PAINT_ACTIVE ); break; case CMD_START: // quit and (re-) start the game init_game(); break; case CMD_FASTER: // lower the divider_limit value divider_limit --; if (divider_limit < 1) divider_limit = 1; break; case CMD_SLOWER: // increase the divider_limit value divider_limit ++; break; default: // do nothing break; } } void main(void) { init(); // Function call inserted by C-Wiz TRISB = 0x06; // RX/TX=inputs (managed by uart), rest outputs // initialize this only once, so that later user-changes via // 'd' (slower) or 'f' (faster) are remembered even when starting // a new game. #ifdef CLK4MHZ divider_limit = 10; #elif CLK250KHZ divider_limit = 2; #else undefined = 20; // intentional error. Define one of the above macros. #endif vt100_initialize(); // setup the rx/tx and timer parameters CLRWDT(); init_game(); // initialize the game-board and stuff for( ;; ) { CLRWDT(); check_handle_command(); } } static interrupt void isr( void ) { unsigned char dummy; if (RCIF) { // a rxtx receiver interrupt indicates that the user has // pressed one key on the terminal. We simply set the command // to the keycode and let the main loop decode and handle the // command. When multiple keys are pressed before the main loop // has processed them, all previous commands are lost and only // the last command is remembered. Maintaining a queue of commands // might be a better idea, but program memory is already quite // scarce... // RCIF = 0; RB4 = 1; // indicate RCIF on pin for debugging dummy = RCSTA; // read and ignore receiver status dummy = RCREG; // read command character if (OERR) { // re-enable receiver in case of overrun CREN = 0; CREN = 1; } command = dummy; // RB4 = 0; } else if (T0IF) { // a timer overflow indicates that the current block // should fall down one position (unless game over). // We set TIMEOUT and let the main program handle this. // RB3 = 1; T0IF = 0; // TMR0 = 100; // 10 at 1MHz // TMR0 = 1; // at 4 MHz // works, but still too fast TMR0 = 100; // 4 MHz, with divider/divider_limit divider++; if (divider >= divider_limit) { divider = 0; state |= TIMEOUT; RB3 = 0; } // some debugging output on RB7... //heartbeat ++; //if ((heartbeat & 0x20) != 0) RB7 = 1; //else RB7 = 0; } // INTCON = 0; // PIR1 = 0; // TXIE = 0; // GIE = 0; }

Print version | Run this demo in the Hades editor (via Java WebStart)
Usage | FAQ | About | License | Feedback | Tutorial (PDF) | Referenzkarte (PDF, in German)
Impressum http://tams.informatik.uni-hamburg.de/applets/hades/webdemos/95-dpi/pic16f628-tetris/tetris.html