I think, most important is to be aware that your question is about an exercise that (apparently) at one time was designed to be easy and interesting.
However, that was long ago, and meanwhile both hardware and software has progressed. It is no longer easy to access physical memory. And, at least for the beginner, it is no longer interesting to do that.
Thus, the main goodness points of the exercise have been chopped off by time’s inexorable entropic action.
However, for one who absolutely want to do this exercise, that once-so-easily-accessible hardware can be simulated. Doing such a simulation transparently, so that the student code is the same as if it was for real, is rather difficult and system-dependent. However, if you (the student) just agree to explicitly call some “update” routine in order to simulate the hardware screen update, then it’s more near managable – and thus, old exercises can be reused! :-)
One can then imagine that main
does some initialization of things, and calls a routine doTheDancingDolls
with arguments that give access to simulated video memory, and the aforementioned “update” functionality:
void doTheDancingDolls(
curses::Console& console,
b8000::DisplayMemorySim& memorySim
)
{
do
{
// Whatever - this is where you Do Things to the simulated video memory.
// Then:
memorySim.update( console );
} while( !console.keyboard().keypressIsAvailable() );
console.keyboard().getKey(); // Consume the keypress.
}
… where the memorySim
object somehow provides the necessary pointer to the simulated video memory – a pointer that corresponds to the B8000000 address in the exercise.
That’s the main idea, how to get a starting point for solving this exercise that's designed for now ANTIQUATED equipment.
Fleshing it out a bit more, here’s one possible implementation of the video memory simulation (note that this is not code that addresses the things that you were meant to do in the exercise, it just addresses the things that you were never meant to do, but possibly would have to today):
File [b8000.h]
#ifndef B8000_H
#define B8000_H
// A simulation of the early PC's 25*80 color text mode video memory.
// The structure of the memory is INTENTIONALLY not modelled in the
// exposed interface. A student can learn from imposing such structure.
//
// Copyright (c) 2011 Alf P. Steinbach.
//------------------------------------------ Dependencies:
#include "curses_plus_plus.h" // curses::Console
#include <vector> // std::vector
//------------------------------------------ Interface:
namespace b8000 {
typedef unsigned char Byte;
class DisplayMemorySim
{
private:
std::vector< Byte > buf_;
public:
enum { nColumns = 80, nLines = 25 };
DisplayMemorySim(): buf_( 2*nLines*nColumns ) {}
void* bufferPointer() { return &buf_[0]; }
void const* bufferPointer() const { return &buf_[0]; }
void update( curses::Console& console ) const
{
assert( console.nLines() >= nLines );
assert( console.nColumns() >= nColumns );
assert( console.colors().nColors() >= 16 );
for( int y = 0; y < nLines; ++y )
{
for( int x = 0; x < nColumns; ++x )
{
int const iCell = 2*(y*nColumns + x);
Byte const charCode = buf_[iCell];
Byte const colors = buf_[iCell + 1];
Byte const fg = colors & 0xF;
Byte const bg = colors >> 4;
console.colors().setFgBg( fg, bg );
console.putAt( x, y, charCode );
}
}
console.updateScreen();
}
};
} // namespace b8000
#endif
And the curses::Console
class might be a simple wrapper over the curses library, e.g. like …
File [curses_plus_plus.h]
#ifndef CURSES_PLUS_PLUS_H
#define CURSES_PLUS_PLUS_H
// Sort of minimal C++ interface to the "curses" library.
// Copyright (c) 2011 Alf P. Steinbach.
//------------------------------------------ Dependencies:
#include "curses.h"
#if defined( _MSC_VER ) && defined( __PDCURSES__ )
# pragma comment( lib, "pdcurses.lib" )
#endif
#ifdef _MSC_VER
# pragma warning( disable: 4355 ) // 'this' used in member initializer
#endif
#include <assert.h> // assert
#include <stddef.h> // NULL
//------------------------------------------ Interface:
namespace curses {
class Console;
namespace detail {
template< class Number > inline Number square( Number x ) { return x*x; }
template< class Derived >
struct SingleInstance
{
static int& instanceCount()
{
static int n = 0;
return n;
}
SingleInstance( SingleInstance const& ); // No such.
SingleInstance& operator=( SingleInstance const& ); // No such.
SingleInstance()
{
++instanceCount();
assert(( "can only have one instance at a time", (instanceCount() == 1) ));
}
~SingleInstance() { --instanceCount(); }
};
} // namespace detail
namespace color {
#ifdef _WIN32 // Any Windows, 32-bit or 64-bit.
int const lightness = 0x08; // Windows only. 8
// The portable colors, expressed for Windows systems.
int const black = COLOR_BLACK; // 0
int const blue = COLOR_BLUE; // 1
int const green = COLOR_GREEN; // 2
int const cyan = COLOR_CYAN; // 3
int const red = COLOR_RED; // 4
int const magenta = COLOR_MAGENTA; // 5
int const yellow = COLOR_YELLOW | lightness; // 6 + 8
int const white = COLOR_WHITE | lightness; // 7 + 8
// Windows-specific colors.
int const gray = COLOR_BLACK | lightness;
int const lightBlue = COLOR_BLUE | lightness;
int const lightGreen = COLOR_GREEN | lightness;
int const lightCyan = COLOR_CYAN | lightness;
int const lightRed = COLOR_RED | lightness;
int const lightMagenta = COLOR_MAGENTA | lightness;
int const brown = COLOR_YELLOW & ~lightness; // A bit greenish.
int const lightGray = COLOR_WHITE & ~lightness;
#else
// The portable colors, expressed for non-Windows systems.
int const black = COLOR_BLACK;
int const blue = COLOR_BLUE;
int const green = COLOR_GREEN;
int const cyan = COLOR_CYAN;
int const red = COLOR_RED;
int const magenta = COLOR_MAGENTA;
int const yellow = COLOR_YELLOW;
int const white = COLOR_WHITE;
#endif
} // namespace color
class Colors
: private detail::SingleInstance< Colors >
{
private:
Colors( Colors const& ); // No such.
Colors& operator=( Colors const& ); // No such.
int n_;
int nPairs_;
int rawIndexOfPair0_;
int rawIndexFor( int fg, int bg ) const
{
return bg*n_ + fg;
}
public:
int nColors() const { return n_; }
int nColorPairs() const { return nPairs_; }
int indexFor( int fg, int bg ) const
{
int const rawIndex = rawIndexFor( fg, bg );
return 0?0
: (rawIndex == rawIndexOfPair0_)? 0
: (rawIndex == 0)? rawIndexOfPair0_
: rawIndex;
}
void setColorPair( int i )
{
::color_set( short( i ), NULL ); //attrset( COLOR_PAIR( i ) );
}
void setFgBg( int fg, int bg )
{
setColorPair( indexFor( fg, bg ) );
}
Colors( Console& )
{
::start_color(); // Initialize color support.
// Although these look like global constants, they're global variables
// that are initialized by the call to Curses' `start_color` (above).
n_ = ::COLORS; nPairs_ = ::COLOR_PAIRS;
assert( detail::square( n_ ) <= nPairs_ ); // Our requirement.
// Obtain curses' default colors, those are at color pair index 0.
{
short fg0 = -1;
short bg0 = -1;
::pair_content( 0, &fg0, &bg0 );
assert( fg0 != -1 );
assert( bg0 != -1 );
rawIndexOfPair0_ = rawIndexFor( fg0, bg0 );
}
// Initialize color pair table to support finding index for given colors.
// The color pair at index 0 can't be specified (according to docs),
// so trickery is required. Here like swapping index 0 to elsewhere.
for( int fg = 0; fg < n_; ++fg )
{
for( int bg = 0; bg < n_; ++bg )
{
int const i = indexFor( fg, bg );
if( i == 0 ) { continue; } // It's initialized already.
::init_pair( short( i ), short( fg ), short( bg ) );
}
}
}
};
class Keyboard
: private detail::SingleInstance< Keyboard >
{
private:
WINDOW* pCursesWin_;
bool isBlocking_;
int bufferedKeypress_;
bool hasBufferedKeypress_;
void setBlocking( bool desiredBlocking )
{
if( isBlocking_ != desiredBlocking )
{
::nodelay( pCursesWin_, !desiredBlocking );
isBlocking_ = desiredBlocking;
}
}
public:
int getKey()
{
if( hasBufferedKeypress_ )
{
hasBufferedKeypress_ = false;
return bufferedKeypress_;
}
setBlocking( true );
return ::getch();
}
bool keypressIsAvailable()
{
if( hasBufferedKeypress_ ) { return true; }
setBlocking( false );
int const key = ::getch();
if( key == ERR ) { return false; }
hasBufferedKeypress_ = true;
bufferedKeypress_ = key;
return true;
}
Keyboard( WINDOW& pCursesWin )
: pCursesWin_( &pCursesWin )
, isBlocking_( true )
, hasBufferedKeypress_( false )
{
::keypad( pCursesWin_, true ); // Assemble multi-character sequences into key symbols.
::nodelay( pCursesWin_, false );
assert( isBlocking_ == true );
}
~Keyboard()
{
setBlocking( true );
}
};
class Console
: private detail::SingleInstance< Console >
{
private:
::WINDOW* pCursesWin_;
Colors colors_;
Keyboard keyboard_;
Console( Console const& ); // No such.
Console& operator=( Console const& ); // No such.
static ::WINDOW* beginWin()
{
return ::initscr();
}
public:
// At least with pdcurses in Windows, these numbers are for the
// console window, i.e. not for its underlying text buffer.
int nColumns() const { return ::getmaxx( pCursesWin_ ); }
int nLines() const { return ::getmaxy( pCursesWin_ ); }
Colors& colors() { return colors_; }
Colors const& colors() const { return colors_; }
Keyboard& keyboard() { return keyboard_; }
Keyboard const& keyboard() const { return keyboard_; }
void putAt( int x, int y, char const s[] )
{
::mvaddstr( y, x, s );
}
void putAt( int x, int y, char c )
{
::mvaddch( y, x, c );
}
void updateScreen() { ::refresh(); }
Console()
: pCursesWin_( beginWin() )
, colors_( *this )
, keyboard_( *pCursesWin_ )
{
::cbreak(); // Immediate input (no line buffering).
::noecho(); // No input echo.
}
~Console()
{
::endwin();
}
};
} // namespace curses
#endif
To drive it all, in the main program you’d check whether the terminal window (Windows “ console window”) is sufficiently large, etc.:
File [main.cpp]
#include "string_util.h" // strUtil::S
#include "b8000.h" // b8000::DisplayMemorySim
#include "curses_plus_plus.h" // curses::Console
#include <algorithm> // std::max
#include <assert.h> // assert
#include <iostream> // std::cerr, std::endl
#include <stdexcept> // std::runtime_error, std::exception
#include <stdlib.h> // EXIT_SUCCESS, EXIT_FAILURE
void doTheDancingDolls(
curses::Console& console,
b8000::DisplayMemorySim& memorySim
)
{
do
{
// Whatever - this is where you Do Things to the simulated video memory.
// The following three lines are just to see that something's going on here.
using stringUtil::S;
static int x = 0;
console.putAt( 30, 5, S() << "I have now counted to " << ++x << "..." );
// Then:
//memorySim.update( console );
} while( !console.keyboard().keypressIsAvailable() );
console.keyboard().getKey(); // Consume the keypress.
}
bool throwX( std::string const& s ) { throw std::runtime_error( s ); }
void cppMain()
{
using std::max;
using stringUtil::S;
curses::Console console;
enum
{
w = b8000::DisplayMemorySim::nColumns,
h = b8000::DisplayMemorySim::nLines
};
(console.colors().nColors() >= 16)
|| throwX( "The console window doesn't support 16 colors." );
(console.nColumns() >= w)
|| throwX( S() << "The console window has less than " << w << " columns." );
(console.nLines() >= h)
|| throwX( S() << "The console window has less than " << h << " lines." );
namespace color = curses::color;
console.colors().setFgBg( color::lightRed, color::yellow );
console.putAt( 30, 0, " The Dancing Dolls! " );
console.putAt( 30, 1, S() << " In " << h << "x" << w << " color mode." );
console.putAt( 30, 3, S() << " Press ANY key to start... " );
console.keyboard().getKey();
console.putAt( 30, 3, S() << " Press ANY key to stop... " );
b8000::DisplayMemorySim displayMemorySim;
doTheDancingDolls( console, displayMemorySim );
}
int main()
{
using namespace std;
try
{
cppMain();
return EXIT_SUCCESS;
}
catch( exception const& x )
{
cout << "!" << x.what() << endl;
}
return EXIT_FAILURE;
}
This post got a bit long, so I’m not adding in the code for the S()
thing (it's not much code but still).
Anyway, this should provide a starting point for experimentation.
Disclaimer: I just cobbled this together. It’s not very extensively tested. I may, for example, have misunderstood things about the curses library’ keyboard handling –. And I’m hoping that it will work also in *nix-land, but I don’t know.
Cheers & hth.,