- BORLAND/: Borland C++ 4.52 (chosen over 4.5 by byte-match: CODE/RP/CW32.LIB
is identical to 4.52's install lib). BCC32/TLINK32/TLIB/MAKE run natively on
Win11; CODE/BT/OPT.MAK is the shipped BTL4OPT.EXE's exact flag recipe
(extender = Borland PowerPack DPMI32, not Phar Lap TNT).
- restoration/source410/: the literal 1995-form reconstruction of the missing
BT game source (never mixed into CODE/). Round 1-3 state:
* 6 of 10 surviving original TUs COMPILE CLEAN under the period toolchain
(BTMSSN, BTCNSL, BTSCNRL, BTTEAM, BTL4MODE, BTL4ARND) - first builds
since 1996.
* BT_L4/BTL4APP.CPP pilot reconstruction: 12/12 functions, Fail() lands on
its binary-recorded line 400 exactly.
* BT/BTCNSL.HPP: console wire IDs recovered from the binary's ctors
(Killed=9, Damaged=10, ScoreUpdate=13, DeathWithoutHonor=15 [T1];
TeamScore=12 flagged [T4]).
* MUNGA/: 8 engine-header backfills back-dated from the BT412 WinTesla tree
(VDATA numbering decomp-verified; AUDREND's OpenAL-era virtual removed -
the period compiler is the drift detector).
* Tooling: backdate.py (WinTesla->1995 header transform), compile410.sh
(per-TU verification sweep under authentic OPT.MAK flags).
* README: corrected roadmap - MECH.HPP is the capstone grown with the mech
TU reconstructions; BTREG.CPP green = the header-family milestone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
534 lines
15 KiB
C++
534 lines
15 KiB
C++
//--------------------------------------------------------------------------
|
|
// Turbo Blocks -- Copyright (c) 1995, Borland International
|
|
//--------------------------------------------------------------------------
|
|
#include <owl/applicat.h>
|
|
#include <owl/framewin.h>
|
|
#include <owl/window.h>
|
|
#include <owl/dialog.h>
|
|
//#include <mmsystem.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include "blocks.rh"
|
|
|
|
const int GAME_WIDTH = 10;
|
|
const int GAME_HEIGHT = 20;
|
|
const char IniFilename[] = "BLOCKS.INI";
|
|
|
|
// TBlock is a structure which defines the geometric game piece
|
|
//
|
|
struct TBlock {
|
|
int size; // 2x2, 3x3, or 4x4 (size of square which completely
|
|
// contains the piece)
|
|
char elements[17];
|
|
|
|
void rotate();
|
|
};
|
|
|
|
// Rotate -- rotate the block (only rotates one direction)
|
|
//
|
|
void TBlock::rotate() {
|
|
int i,j;
|
|
char tempBlock[17];
|
|
|
|
switch (size) {
|
|
case 4:
|
|
strcpy(tempBlock,elements);
|
|
for (i=0;i<4;i++)
|
|
for (j=0;j<4;j++)
|
|
tempBlock[i*4+j]=elements[j*4+i];
|
|
strcpy(elements,tempBlock);
|
|
break;
|
|
|
|
case 3:
|
|
strcpy(tempBlock,elements);
|
|
for (i=0;i<3;i++)
|
|
for (j=0;j<3;j++)
|
|
tempBlock[(2-i)*4+j]=elements[j*4+i];
|
|
strcpy(elements,tempBlock);
|
|
break;
|
|
|
|
case 2: // no 2x2 blocks can rotate, so do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Define all the game pieces.
|
|
|
|
TBlock blocks[7] = { {4, " * "
|
|
" * "
|
|
" * "
|
|
" * "},
|
|
{3, " ** "
|
|
" * "
|
|
" * "
|
|
" "},
|
|
{3, "** "
|
|
" * "
|
|
" * "
|
|
" "},
|
|
{3, " * "
|
|
"*** "
|
|
" "
|
|
" "},
|
|
{3, "** "
|
|
" ** "
|
|
" "
|
|
" "},
|
|
{3, " ** "
|
|
"** "
|
|
" "
|
|
" "},
|
|
{2, "** "
|
|
"** "
|
|
" "
|
|
" "} };
|
|
|
|
// these define the pens and brushes to be used for the various
|
|
// blocks. each block type is a different color
|
|
//
|
|
|
|
TPen pen[8] = { TPen( TColor( 0, 0, 64 ) ),
|
|
TPen( TColor( 0, 0, 255 ) ),
|
|
TPen( TColor( 0, 255, 0 ) ),
|
|
TPen( TColor( 255, 0, 0 ) ),
|
|
TPen( TColor( 255,255,0 ) ),
|
|
TPen( TColor( 255,0,255 ) ),
|
|
TPen( TColor( 0,255,255 ) ),
|
|
TPen( TColor( 255,255,255 ) ) };
|
|
|
|
TBrush brush[8] = { TBrush( TColor( 0, 0, 64 ) ),
|
|
TBrush( TColor( 0, 0, 128 ) ),
|
|
TBrush( TColor( 0, 128, 0 ) ),
|
|
TBrush( TColor( 128, 0, 0 ) ),
|
|
TBrush( TColor( 128,128,0 ) ),
|
|
TBrush( TColor( 128,0,128 ) ),
|
|
TBrush( TColor( 0,128,128 ) ),
|
|
TBrush( TColor( 128,128,128 ) ) };
|
|
|
|
|
|
// BlocksWindow is the class that actually defines the game. It is used
|
|
// as the client window inside of a TFrameWindow.
|
|
//
|
|
class TBlocksWindow: public TWindow {
|
|
|
|
enum GameState { gsGameOver, // game over, wait for newgame command
|
|
gsBlockDropping, // normal mode, blocks are dropping
|
|
gsPaused }; // game is paused
|
|
|
|
TMemoryDC *memDC;
|
|
GameState gameState; // current state of the game
|
|
TBlock currentBlock; // currently falling block
|
|
int x,y, // current block position
|
|
color; // current block color
|
|
int board[GAME_HEIGHT+1][GAME_WIDTH+2]; // the game grid
|
|
// edge to make for easier edge detection
|
|
uint Timer; // ID of game timer
|
|
uint dropCount; // countdown to next time piece falls one row
|
|
|
|
bool dropping; // is the user holding the 'drop' key down?
|
|
|
|
int gameSpeed,blockSize; // read from .INI file
|
|
public:
|
|
TBlocksWindow( TWindow* parent );
|
|
~TBlocksWindow() {
|
|
char temp[10];
|
|
wsprintf(temp,"%d",gameSpeed);
|
|
WritePrivateProfileString("blocks","gamespeed",temp,IniFilename);
|
|
wsprintf(temp,"%d",blockSize);
|
|
WritePrivateProfileString("blocks","blocksize",temp,IniFilename);
|
|
delete memDC;
|
|
}
|
|
bool EvEraseBkgnd( HDC ) { // since we paint the entire window, we
|
|
return TRUE; // don't want windows doing it for us.
|
|
} // this will reduce flicker
|
|
|
|
void SetupWindow();
|
|
void CleanupWindow() {
|
|
KillTimer( Timer );
|
|
}
|
|
void Pause();
|
|
void PauseEnabler( TCommandEnabler& tce ) {
|
|
if (gameState==gsGameOver)
|
|
tce.Enable( false );
|
|
else
|
|
if (gameState==gsPaused)
|
|
tce.SetText("Resume\tAlt+P");
|
|
else
|
|
tce.SetText("Pause\tAlt+P");
|
|
}
|
|
void NewGame();
|
|
void ClearBoard();
|
|
void NewBlock( int blockType );
|
|
void RemoveLines();
|
|
void PlaceBlock();
|
|
bool HitTest( TBlock& block, int x, int y );
|
|
void DrawBlock( TDC& dc, TBlock& block, TPoint& pos );
|
|
void Paint( TDC&, bool, TRect& );
|
|
void EvKeyDown( uint key, uint repeatCount, uint flags );
|
|
void EvKeyUp( uint, uint, uint );
|
|
void EvTimer( uint id );
|
|
|
|
|
|
DECLARE_RESPONSE_TABLE( TBlocksWindow );
|
|
};
|
|
|
|
DEFINE_RESPONSE_TABLE1( TBlocksWindow, TWindow )
|
|
EV_WM_KEYDOWN,
|
|
EV_WM_KEYUP,
|
|
EV_WM_ERASEBKGND,
|
|
EV_WM_TIMER,
|
|
EV_COMMAND( CM_GAMEPAUSE, Pause ),
|
|
EV_COMMAND( CM_GAME_NEW, NewGame ),
|
|
EV_COMMAND_ENABLE( CM_GAMEPAUSE, PauseEnabler ),
|
|
END_RESPONSE_TABLE;
|
|
|
|
// TBlocksWindow -- constructor. Read the options from the INI file,
|
|
// set our game window size, and reset the game board.
|
|
//
|
|
TBlocksWindow::TBlocksWindow( TWindow* parent ) : TWindow( parent ) {
|
|
|
|
blockSize = GetPrivateProfileInt( "blocks","blocksize",20,IniFilename);
|
|
gameSpeed = GetPrivateProfileInt( "blocks","gamespeed",10,IniFilename);
|
|
|
|
Attr.W = blockSize*GAME_WIDTH;
|
|
Attr.H = blockSize*GAME_HEIGHT;
|
|
|
|
ClearBoard();
|
|
|
|
dropCount = 0;
|
|
gameState = gsGameOver;
|
|
dropping = false;
|
|
}
|
|
|
|
// SetupWindow -- after the window is created, we need to do a bit more
|
|
// work. Create a timer to drive the game, and create a memory DC that
|
|
// will be used to draw the blocks off-screen.
|
|
//
|
|
void TBlocksWindow::SetupWindow() {
|
|
TWindow::SetupWindow();
|
|
|
|
Timer = 0;
|
|
if (SetTimer( 1, 10 ))
|
|
Timer=1;
|
|
|
|
TClientDC dc( HWindow );
|
|
memDC = new TMemoryDC( dc );
|
|
memDC->SelectObject( TBitmap( dc, GAME_WIDTH*blockSize, GAME_HEIGHT*blockSize ) );
|
|
}
|
|
|
|
// RemoveLines -- checks for completed lines and removes them from
|
|
// the game board. If lines are removed, higher lines are moved down
|
|
// to fill in the space.
|
|
//
|
|
void TBlocksWindow::RemoveLines() {
|
|
int i,j,k,l;
|
|
bool lineFull,linesRemoved;
|
|
|
|
linesRemoved=false;
|
|
j=GAME_HEIGHT-1;
|
|
while (j>=0) {
|
|
lineFull=true;
|
|
for (i=1;i<=GAME_WIDTH;i++)
|
|
if (board[j][i]==0)
|
|
lineFull=false;
|
|
if (lineFull) {
|
|
linesRemoved=true;
|
|
for (k=j;k>=1;k--)
|
|
for (l=1;l<=GAME_WIDTH;l++)
|
|
board[k][l]=board[k-1][l];
|
|
for (l=1;l<=GAME_WIDTH;l++)
|
|
board[0][l]=0;
|
|
} else
|
|
j--;
|
|
}
|
|
if (linesRemoved) {
|
|
// sndPlaySound( "tada.wav", SND_ASYNC | SND_NODEFAULT );
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
// NewGame -- start a new game. First clear the board, then
|
|
// add the first block, and change the game state to gsBlockDropping
|
|
//
|
|
void TBlocksWindow::NewGame() {
|
|
ClearBoard();
|
|
NewBlock( random(7) );
|
|
gameState = gsBlockDropping;
|
|
Invalidate();
|
|
}
|
|
|
|
// ClearBoard -- resets the game board to be empty
|
|
//
|
|
void TBlocksWindow::ClearBoard() {
|
|
int i,j;
|
|
|
|
for (i=0;i<(GAME_HEIGHT+1);i++)
|
|
for (j=0;j<(GAME_WIDTH+2);j++)
|
|
board[i][j]=0;
|
|
for (i=0;i<(GAME_HEIGHT+1);i++) {
|
|
board[i][0] = -1;
|
|
board[i][GAME_WIDTH+1] = -1;
|
|
}
|
|
for (j=0;j<(GAME_WIDTH+2);j++)
|
|
board[GAME_HEIGHT][j]=-1;
|
|
}
|
|
|
|
// EvTimer -- called by the timer we created in SetupWindow. Depending
|
|
// on the game state, do various things.
|
|
//
|
|
void TBlocksWindow::EvTimer( uint ) {
|
|
|
|
switch (gameState) {
|
|
case gsGameOver: // no game in progress, do nothing
|
|
break;
|
|
|
|
case gsBlockDropping: // game in progress
|
|
dropCount++; // increment drop counter
|
|
if ((dropping) ||
|
|
(dropCount==gameSpeed)) { // if time to drop
|
|
dropCount=0; // reset counter
|
|
y++; // move block down
|
|
if (HitTest( currentBlock, x, y ) ) { // if it hit something
|
|
y--; // move it back up
|
|
PlaceBlock(); // make it permanent
|
|
RemoveLines();
|
|
NewBlock( random(7) );
|
|
}
|
|
Invalidate(); // redraw game board
|
|
}
|
|
break;
|
|
|
|
case gsPaused: // game is paused
|
|
break;
|
|
}
|
|
}
|
|
|
|
// EvKeyUp/EvKeyDown -- respond to key press/release messages. For the
|
|
// 'drop' key, we set a flag whenever the key is held down. For the
|
|
// left/right/rotate keys, we only keep track of keydown events.
|
|
//
|
|
void TBlocksWindow::EvKeyUp( uint key, uint, uint ) {
|
|
switch (key) {
|
|
case VK_NUMPAD2:
|
|
case VK_DOWN:
|
|
dropping = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void TBlocksWindow::EvKeyDown( uint key, uint /*repeat*/, uint /*flags*/ ) {
|
|
if ( gameState == gsPaused ) {
|
|
gameState = gsBlockDropping;
|
|
Invalidate();
|
|
return;
|
|
}
|
|
switch (key) {
|
|
|
|
// move block left
|
|
|
|
case VK_NUMPAD4:
|
|
case VK_LEFT:
|
|
x--;
|
|
if (HitTest( currentBlock, x, y ))
|
|
x++;
|
|
else
|
|
Invalidate();
|
|
break;
|
|
|
|
// move block right
|
|
|
|
case VK_NUMPAD6:
|
|
case VK_RIGHT:
|
|
x++;
|
|
if (HitTest( currentBlock, x, y ))
|
|
x--;
|
|
else
|
|
Invalidate();
|
|
break;
|
|
|
|
// turn on fast dropping
|
|
|
|
case VK_NUMPAD2:
|
|
case VK_DOWN:
|
|
dropping = true;
|
|
break;
|
|
|
|
// rotate block
|
|
|
|
case VK_UP:
|
|
case VK_NUMPAD5:
|
|
case VK_SPACE:
|
|
currentBlock.rotate();
|
|
if (HitTest( currentBlock, x, y )) {
|
|
currentBlock.rotate();
|
|
currentBlock.rotate();
|
|
currentBlock.rotate();
|
|
}
|
|
Invalidate();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Pause -- handler for the 'Pause' menu item
|
|
//
|
|
void TBlocksWindow::Pause() {
|
|
if (gameState==gsBlockDropping)
|
|
gameState=gsPaused;
|
|
else
|
|
if (gameState==gsPaused)
|
|
gameState=gsBlockDropping;
|
|
Invalidate();
|
|
}
|
|
|
|
// HitTest -- tests to see if a block overlaps any occupied square
|
|
// on the board
|
|
//
|
|
bool TBlocksWindow::HitTest( TBlock& block, int x, int y ) {
|
|
int i,j;
|
|
for (i=0;i<4;i++)
|
|
for (j=0;j<4;j++)
|
|
if ( ((x+i)<(GAME_WIDTH+2)) &&
|
|
((x+i+1)>=0) &&
|
|
((y+j)<(GAME_HEIGHT+1)) &&
|
|
((y+j)>=0) ) // make sure block in question is in range
|
|
if (board[y+j][x+1+i]!=0) // if the board piece is empty, skip test
|
|
if (block.elements[j*4+i]=='*')
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// NewBlock -- creates a new block at the top of the screen
|
|
//
|
|
void TBlocksWindow::NewBlock( int blockType ) {
|
|
currentBlock = blocks[ blockType ];
|
|
color = blockType+1;
|
|
x = 4;
|
|
y = 0;
|
|
// if the new block hits anything on the screen, the game is over
|
|
//
|
|
if (HitTest( currentBlock, x, y )) {
|
|
PlaceBlock();
|
|
Invalidate();
|
|
gameState = gsGameOver;
|
|
}
|
|
}
|
|
|
|
// PlaceBlock -- puts the current block permanently into the board array.
|
|
// this function is called when the block reaches the bottom
|
|
// of the game board.
|
|
//
|
|
void TBlocksWindow::PlaceBlock() {
|
|
int i,j;
|
|
for (i=0;i<4;i++)
|
|
for (j=0;j<4;j++)
|
|
if (currentBlock.elements[j*4+i]=='*')
|
|
board[y+j][x+1+i]=color;
|
|
|
|
Invalidate();
|
|
RemoveLines();
|
|
}
|
|
|
|
// DrawBlock -- draws an individual game piece.
|
|
//
|
|
void TBlocksWindow::DrawBlock( TDC& dc, TBlock& block, TPoint& pos ) {
|
|
int i,j,size=block.size;
|
|
dc.SelectObject( brush[ color ] );
|
|
dc.SelectObject( pen[ color ] );
|
|
for (i=0;i<size;i++)
|
|
for (j=0;j<size;j++)
|
|
if (block.elements[j*4+i]=='*')
|
|
dc.Rectangle( pos.x*blockSize+i*blockSize,
|
|
pos.y*blockSize+j*blockSize,
|
|
pos.x*blockSize+i*blockSize+blockSize,
|
|
pos.y*blockSize+j*blockSize+blockSize );
|
|
}
|
|
|
|
// Paint -- redraws the entire window. Currently, whenever anything
|
|
// on the game board changes, we redraw the entire board. This could
|
|
// obviously be improved, by only redrawing the area around the moving
|
|
// block.
|
|
//
|
|
void TBlocksWindow::Paint( TDC& dc, bool /*erase*/, TRect& /*rect*/ ) {
|
|
int i,j;
|
|
|
|
// if the game is paused, blank the screen (to prevent cheating!)
|
|
|
|
if (gameState==gsPaused) {
|
|
memDC->FillRect( GetClientRect(), brush[0] );
|
|
memDC->SetTextColor( TColor( 0, 255, 255 ) );
|
|
memDC->SetBkColor( TColor( 0, 0, 128 ) );
|
|
memDC->TextOut( blockSize*2,blockSize*9, " * * P A U S E D * * " );
|
|
dc.BitBlt( GetClientRect(), *memDC, TPoint(0,0) );
|
|
return;
|
|
}
|
|
|
|
// clear the memory DC
|
|
|
|
memDC->FillRect( GetClientRect(), brush[0] );
|
|
|
|
// draw the permanent blocks
|
|
|
|
for (j=0;j<GAME_HEIGHT;j++)
|
|
for (i=0;i<GAME_WIDTH;i++)
|
|
if ( board[j][i+1]!=0 ){
|
|
memDC->SelectObject( pen[ board[j][i+1] ] );
|
|
memDC->SelectObject( brush[ board[j][i+1] ] );
|
|
memDC->Rectangle( i*blockSize,j*blockSize,
|
|
i*blockSize+blockSize,j*blockSize+blockSize );
|
|
}
|
|
|
|
// display the game over message if the game has ended
|
|
|
|
if (gameState == gsGameOver) {
|
|
memDC->SetTextColor( TColor( 0, 255, 255 ) );
|
|
memDC->SetBkColor( TColor( 0, 0, 128 ) );
|
|
memDC->TextOut( blockSize*1.5,blockSize*9, "* G A M E O V E R *");
|
|
}
|
|
|
|
// if a block is dropping, draw it
|
|
|
|
if (gameState == gsBlockDropping)
|
|
DrawBlock( *memDC, currentBlock, TPoint( x, y ) );
|
|
|
|
// now copy the memory DC to the screen.
|
|
|
|
dc.BitBlt( GetClientRect(), *memDC, TPoint(0,0) );
|
|
}
|
|
|
|
// TBlocksApp -- the main application. InitMainWindow creates the
|
|
// framewindow, and inserts a TBlocksWindow as the client. The app
|
|
// also responds to the 'Game|Exit' and 'Help|About' menu items.
|
|
//
|
|
class TBlocksApp: public TApplication {
|
|
public:
|
|
void InitMainWindow() {
|
|
TFrameWindow *fw = new TFrameWindow( 0, "Turbo Blocks",
|
|
new TBlocksWindow(0), true );
|
|
fw->Attr.Style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
|
|
fw->AssignMenu( MAIN_MENU );
|
|
fw->Attr.AccelTable = ACCELERATORS_1;
|
|
fw->SetIcon( this, ICON_1 );
|
|
|
|
SetMainWindow( fw );
|
|
}
|
|
void AboutBox() {
|
|
TDialog( GetMainWindow(), ABOUT_BOX ).Execute();
|
|
}
|
|
void Exit() {
|
|
GetMainWindow()->CloseWindow();
|
|
}
|
|
DECLARE_RESPONSE_TABLE( TBlocksApp );
|
|
};
|
|
DEFINE_RESPONSE_TABLE1( TBlocksApp, TApplication )
|
|
EV_COMMAND( CM_HELP_ABOUT, AboutBox ),
|
|
EV_COMMAND( CM_GAMEEXIT, Exit ),
|
|
END_RESPONSE_TABLE;
|
|
|
|
// the main program
|
|
|
|
int OwlMain( int, char *[] ) {
|
|
randomize();
|
|
return TBlocksApp().Run();
|
|
}
|