|
25 Years of Programming
An open source source for C, C++, OWL, BASIC, MDB, XLS, DOT, and more... |
Home Projects Up Sitemap Search Blog Forum+Chat About Us Privacy Terms of Use Feedback FAQ Images Services Payments Humor |
The Game of Life cellular automaton program, Borland C++ 4.0 ObjectWindows
WLife2d.cpp plays The Game of Life, a cellular automaton originated by John Conway. It is described in chapters about artificial life in the books Chaos Under Control, by Peak and Frame, and Complexity, by Waldrop. An initial grid is set up in which some of the squares ("cells") are "on" and some are "off". The program then cycles through successive generations. Whether a cell remains on ("survives"), or turns off ("dies"), or turns on ("is born") depends on the number of neighbors it has. Aside from whatever value it has in the study of cellular automata, the shifting patterns are fun to watch. This version of the program was developed with Borland C++ 4.0 and ObjectWindows Library (OWL) 2.0 for Windows 3.1. Besides the standard Life game rules, you can create custom rules of your own and see how they behave. The screen wraps in all directions, so the left and right borders are connected, as are the top and bottom. You can specify the height and width of the field to calculate. If you use the full window, each cell is one pixel on the screen, but if you use a smaller field, it is enlarged to fill the screen so the cells are bigger. Click on the screenshot thumbnails above to view the full size images. Much of this program's functionality is provided by SDibWindow, SDib, and other library classes you'll find on this site, so the project winds up being large. Links to the needed source code are provided in the listings below where they are referenced. Download:Click here to download wlife2d.zip (about 162 KB). The zip contains these files that are unique to this project:
This screenshot of the project nodes in the Borland C++ 4.0 IDE might help you set up the project nodes.
|
/* wlife2d.cpp 12-15-01
Copyright (C)1989-2001 Steven Whitney.
Initially published by http://25yearsofprogramming.com.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License (GPL)
Version 3 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Game of Life, originated by John Conway. Example of a cellular automaton.
Win32s target. (Originally adapted from wshowfs.cpp)
------
To Do:
give LifeRule a static ulong counter to number the random rule sets.
add an option to load a specific bitmap to start each random rule set, e.g. onedot.bmp,
so you can see how different rules affect one particular pattern.
the initial random screen of each new set isn't displayed: starts with first CALCULATED screen
is it shown for standard games? file-loaded? Is there an Invalidate() missing somewhere?
Force display by calling EvPaint()?
functions that need changes for a 1d option are marked. determine whether it can be
accomodated here, or, probably, needs a separate program. (1d appends successive
generations to the screen, while 2d uses a whole screen each generation).
When you save a screen as BMP, there is one additional cycle before you get to
do the save. You actually save the next generation. Would rather save this one. How?
use .INI file for some startup options: HxW field size, starting .MAP file to load,
mono or color mode
-----
Notes:
--all notes and all non-version-specific to do are now in Life.doc
*/
#include <owl\owlpch.h>
#include <owl\chooseco.h>
#include <owl\opensave.h>
#include <owl\radiobut.h>
#include <owl\edit.h>
#include <owl\inputdia.h>
#include <classlib\arrays.h>
#include <math.h>
#pragma hdrstop
#include "c:\bcs\my.h"
#include "c:\bcs\library\filearay.h"
#include "c:\bcs\library\stopwatc.h"
#include "c:\bcs\library\doubrect.h"
#include "c:\bcs\library\sdibwin.h"
#include "c:\bcs\mylib.cpp"
#include "c:\bcs\library\doubrect.cpp"
#include "c:\bcs\library\sdib.cpp"
#include "c:\bcs\library\filearay.cpp"
#include "c:\bcs\library\stopwatc.cpp"
#include "wlife2d.rh"
#define NUMSTATES 16 // # of possible automaton states (dib is always 256 colors)
const char AppName[] = "Game of Life";
char INIFilename[] = "wlife2d.ini";
char HelpFilename[] = "wlife2d.hlp";
//////////////////////////////////////////////////////////////////////////////
// A LifeRule holds the rule that determines whether a cell survives or is
// born, and has the functions for doing so.
// Adding 1d option will require some changes:
// a flag to mark as 1d or 2d
// a flag for position dependent vs. outer totalistic, for either 1d or 2d
// an array for holding neighborhood matching strings (position dependent)
// maybe a better array for holding neighborcount values (outer totalistic)
// file load routine that can also determine type of rule it's loading
// either use some lines to explicitly declare, or define by filename extension.
//----------------------------------------------------------------------------
class LifeRule
{
public:
LifeRule(); // standard game only
LifeRule(BOOL standardgame); // choice of standard or random
BOOL operator == (const LifeRule&) const;
friend ostream& operator << (ostream&, const LifeRule&); // put to
friend istream& operator >> (istream&, LifeRule&); // get from
void standardize();
void randomize();
void sortarrays();
BOOL survive(int neighborcount); // determines whether a cell survives
BOOL born(int neighborcount); // determines whether a cell is born
string comment; // user-supplied text comment about the set
string filesource; // name of the file this set came from, if any
protected:
int s[9]; // counts defining when a cell will live
int b[9]; // counts defining when a new cell is born
int survivecount; // number of elements in s[] actually in use
int borncount; // number of elements in b[] actually in use
static int compareints(int*,int*); // for qsort
};
//----------------------------------------------------------------------------
// default constructor
LifeRule::LifeRule()
{
standardize();
} //constructor
//----------------------------------------------------------------------------
// constructor
LifeRule::LifeRule(BOOL standardgame)
{
if(standardgame)
standardize();
else
randomize();
} //constructor
//----------------------------------------------------------------------------
// This should sort and determine if all elements of s[] and b[] are the same
// in both sets. Could then be used to prevent duplicates. Don't compare
// comment or filesource, but then beware when using containers of LifeRules.
BOOL LifeRule::operator == (const LifeRule& other) const
{
return(&other == this);
}
//----------------------------------------------------------------------------
// Output to a stream. File format (3 lines each rule set):
// It assumes s[] and b[] are already sorted, with the 10s, if any, at the end.
// 2 3 <-Survive
// 3 <-Born
// Standard game <-Comment
// 1d option requires changes
ostream& operator << (ostream& os, const LifeRule& r)
{
// only write the active elements
for(int i = 0 ; (i < 9) && (r.s[i] < 10) ; i++)
os << r.s[i] << " ";
os << endl;
for(i = 0 ; (i < 9) && (r.b[i] < 10) ; i++)
os << r.b[i] << " ";
os << endl;
os << r.comment << endl;
return(os);
}
//----------------------------------------------------------------------------
// Read from a stream.
// 1d option requires changes
istream& operator >> (istream& is, LifeRule& r)
{
for(int i = 0 ; i < 9 ; i++) // initialize arrays to non-values
r.s[i] = r.b[i] = 10;
r.survivecount = r.borncount = 0;
string s; // to receive unknown-length file line
int j;
char* buf;
is >> ws; // eat whitespace in case there are blank lines
// READ THE survive VALUES
getline(is,s,'\n'); // read the file line
buf = new char[s.length() + 1]; // make a matching size char array
strcpy(buf,s.c_str()); // copy the string to it
s.remove(0); // free some memory
istrstream inbuf(buf); // a stream we can read values from
i = 0;
while(inbuf >> j) // while we can read values,
if(i < 9) // (but not more than 9)
{
r.s[i++] = j; // add them to the iset array
r.survivecount++;
}
delete[] buf; // deallocate buf
buf = 0; // make it null
// READ THE born VALUES
getline(is,s,'\n'); // read the file line
buf = new char[s.length() + 1]; // make a matching size char array
strcpy(buf,s.c_str()); // copy the string to it
s.remove(0); // free some memory
istrstream inbuf2(buf); // a stream we can read values from
i = 0;
while(inbuf2 >> j) // while we can read values,
if(i < 9) // (but not more than 9)
{
r.b[i++] = j; // add them to the iset array
r.borncount++;
}
delete[] buf; // deallocate buf
buf = 0; // make it null
r.comment.remove(0);
r.comment.read_file(is); // get new comment, if any (read to end of file)
r.filesource.remove(0); // name of the file we read from is unknown here
r.sortarrays(); // ensure the arrays are sorted (old files weren't)
return(is);
}
//---------------------------------------------------------------------------
// determines whether a point survives, based on its rule
// 1d option requires changes
BOOL LifeRule::survive(int neighborcount)
{
for(int i = 0 ; i < survivecount ; i++)
if(neighborcount == s[i]) // if its current neighbor count is listed,
return(TRUE); // then it survives.
return(FALSE);
} //survive
//---------------------------------------------------------------------------
// determines whether a point is born, based on its rule
// 1d option requires changes
BOOL LifeRule::born(int neighborcount)
{
for(int i = 0 ; i < borncount ; i++)
if(neighborcount == b[i])
return(TRUE);
return(FALSE);
} //born
//----------------------------------------------------------------------------
// initialize with born/survive rules for a standard Game of Life
// 1d option requires changes
void LifeRule::standardize()
{
for(int i = 0 ; i < 9 ; i++) // initialize arrays to non-values
s[i] = b[i] = 10;
s[0] = 2; // then set up as standard Life game
s[1] = 3;
b[0] = 3;
survivecount = 2;
borncount = 1;
comment = "Standard Life Game Rules";
filesource.remove(0);
} //standardize
//----------------------------------------------------------------------------
// comparison routine for qsort, sorts integers
// only works as a static or global. Weird error as a member.
int LifeRule::compareints(int* l, int* r)
{
return(*l - *r);
}
//----------------------------------------------------------------------------
// pack valid elements at the beginning so searches don't waste time testing unused ones
void LifeRule::sortarrays()
{
qsort(s, 9, sizeof(s[0]),(int (*)(const void *,const void *)) compareints);
qsort(b, 9, sizeof(b[0]),(int (*)(const void *,const void *)) compareints);
}
//----------------------------------------------------------------------------
// assign survive/birth rules randomly. Fill from 1 to 9 slots in s[] and b[]
// with values from 0-8 without repeating any value.
// 1d option requires changes
void LifeRule::randomize()
{
int i, j, k;
for(i = 0 ; i < 9 ; i++) // fill both arrays with entire number set 0-8
s[i] = b[i] = i; // This ensures no repeat values
j = random(9); // get number of array elements to delete (max. 8 of the 9)
survivecount = 9 - j;
for(i = 0 ; i < j ; i++)
{
do
{
k = random(9); // find an array element not already deleted
}
while(s[k] == 10); // it's already deleted if its count is 10
s[k] = 10; // delete by setting it to an impossible neighborcount of 10
}
j = random(9); // number of array elements to delete (max. 8 of the 9)
borncount = 9 - j;
for(i = 0 ; i < j ; i++)
{
do
{
k = random(9); // get an array element not already deleted
}
while(b[k] == 10);
b[k] = 10; // delete by setting it to an impossible neighborcount
}
sortarrays(); // sort and get all the unused 10s to the end of the array
comment = "New random rule set"; // reminder
filesource.remove(0);
} //randomize
//---------------------------------------------------------------------------
// end of class LifeRule
#if 0
// THIS CLASS WAS WORK IN PROGRESS AND WAS NEVER USED.
//////////////////////////////////////////////////////////////////////////////
// a LifeField is a gameboard in which a game of life takes place
// It contains everything needed for the maintenance and generation cycling
// of a cellular automaton. Its purpose is to remove as much of this
// functionality as possible from the SLifeWindow so it can be used for
// more complex automata. Should probably allow for position-dependent rules.
// 1d option requires changes. Maybe stop work on this. It is not yet used.
class LifeField
{
public:
LifeField();
~LifeField();
friend ostream& operator << (ostream&, LifeField&); // put-to operator
protected:
// pointers
char** board; // this is the field itself, width * height
// other variables
// the rule table governing this game. its format will have to be revised
LifeRule liferule;
BOOL randomrules; // generate a new random rule set with each new screen?
long generations; // # of generations
long population; // count of live cells
int statetally[NUMSTATES]; // holds tally of how many neighbors are in each state
int width; // size of board
int height;
double percentfill; // % (0-100) of a new window to fill with random dots
// normal functions
BOOL initialize();
BOOL initialize(const char* filename); // create rule set from file data
void randomscreen(); // set up screen with initial random dots
BOOL makenewboard(); // create or recreate the board
void eraseboard(); // erase to its background color, draw data area
void cycle(); // calculate the next generation
void resetcounts() { generations = population = 0L; } // reset counting variables
int commoneststateindex();
};
//----------------------------------------------------------------------------
// constructor
LifeField::LifeField()
{
width = 80;
height = 60; // 80x60 and 160x120 are good
percentfill = 30.;
population = 0L;
generations = 0L;
// you must dimension it this way because since array subscripts are ints,
// you can't subscript into a single array that is longer than 32767 chars,
// which a 200x200 board would exceed. You also can't use a string object, same reason.
// the result is (height) char arrays, each (width) wide.
// board[y][x] is a single char corresponding to a field (screen) position.
// y,x are backwards because x must vary fastest when traversing in scan-line fashion.
board = newarray2dim(char(),height,width);
} //constructor
//----------------------------------------------------------------------------
// destructor
LifeField::~LifeField()
{
if(board)
deletearray2dim(board,height);
} //destructor
//----------------------------------------------------------------------------
BOOL LifeField::initialize()
{
// randomscreen();
return(TRUE);
} //initialize(randomly assign)
//----------------------------------------------------------------------------
// change to determine commonest state
// determine commonest state among neighbors.
// starts search at a random point within statetally[] and loops around
// through beginning if necessary. Thus, if two or more state tie for the
// mode, the choice of "winner" will be random, not determined by placement.
// returns the index within statetally[] of the commonest state.
// (not yet used)
int LifeField::commoneststateindex()
{
int mode; // the commonest state
int highcount = 0; // the number of occurrences of the commonest state
int start = random(NUMSTATES); // 0-15, a random starting loc. within statetally[]
int i = start; // and save it for testing.
do
{
if(statetally[i] > highcount)
{
highcount = statetally[i];
mode = i;
}
if(++i >= NUMSTATES) // reached end of array, but not back to start point,
i = 0; // so wrap to array start, to continue
}
while(i != start);
return(mode);
} //commoneststateindex
//----------------------------------------------------------------------------
// calculate cell states for the next generation
// #error THIS IS NOWHERE NEAR DONE, AND IS NOTHING MORE THAN IDEAS OF HOW
// THIS FUNCTION MIGHT BE DONE FOR MORE COMPLICATED RULE SETS WHERE NEIGHBORS
// MAY BE NOT JUST ON OR OFF AND WHERE POSITION DEPENDENCE MAY BE USED.
void LifeField::cycle()
{
// a second board to calculate new points onto
char** newboard = newarray2dim(char(),height,width);
int x, y; // point whose neighbors are being counted
int maxx = width - 1;
int maxy = height - 1;
// build in this array a picture of the neighborhood that takes screen-wrap into account.
// It includes a slot for the point itself. (unsure whether it should or not)
// this array becomes the test in an "if" clause of a rule. See complex.doc.
// (also allows a slot for a null, in case we want to make a string out of it)
// (but beware it probably contains nulls anyway)
char neighborhood[10];
for(y = 0 ; y < height ; y++)
{
int above = wrap(y - 1,0,maxy);
int below = wrap(y + 1,0,maxy);
for(x = 0 ; x < width ; x++)
{
// first just build the neighborhood for testing. no tallying, etc. yet
// screen wraps in all directions. note (Height,Width) is off screen
int left = wrap(x - 1,0,maxx);
int right = wrap(x + 1,0,maxx);
neighborhood[0] = board[above][left]; // above left
neighborhood[1] = board[above][x]; // above
neighborhood[2] = board[above][right]; // above right
neighborhood[3] = board[y][left]; // point to left
neighborhood[4] = board[y][x]; // (the point itself)
neighborhood[5] = board[y][right]; // point to right
neighborhood[6] = board[below][left]; // below left
neighborhood[7] = board[below][x]; // below
neighborhood[8] = board[below][right]; // below right
// now use neighborhood to compute various statistics we might use for rules
// that is, neighborcount = number of non-state-0 neighbors
// and you can tally the number of neighbors in each state into statetally[]
// initialize counters, etc.
for(int i = 0 ; i < NUMSTATES ; i++) // zero out tally values
statetally[i] = 0;
int neighborcount = 0;
// here, determine what the new state should be
int newstate = 0; // whatever the criterion is
if(neighborcount)
newstate = 1;
// and set it in the NEW board
newboard[y][x] = (char)newstate;
} // end for(x)
} // end for(y)
// destroy the old board, and set its pointer to point at the new board
deletearray2dim(board,height);
board = newboard;
generations++;
} //cycle
//---------------------------------------------------------------------------
// end class LifeField
//----------------------------------------------------------------------------
#endif // 0
//////////////////////////////////////////////////////////////////////////////
// transfer information for the rule editor dialog box
struct RuleEditorStruct
{
RuleEditorStruct() { filechars[0] = 0; }
char filechars[2000];
};
//////////////////////////////////////////////////////////////////////////////
// dialog box for editing the current survive/born rule set.
class SRulesEditDialog : public TDialog
{
public:
SRulesEditDialog(TWindow*, RuleEditorStruct*, const string&); // constructor
protected:
void CmHelp()
{ WinHelp(HelpFilename,HELP_PARTIALKEY,(DWORD)(LPSTR)"Rule Editor Dialog Box"); }
DECLARE_RESPONSE_TABLE(SRulesEditDialog);
};
DEFINE_RESPONSE_TABLE1(SRulesEditDialog,TDialog)
EV_COMMAND(IDHELP,CmHelp),
END_RESPONSE_TABLE;
//----------------------------------------------------------------------------
// constructor
SRulesEditDialog::SRulesEditDialog(TWindow* parent, RuleEditorStruct* tts,
const string& filename) : TDialog(parent,TResId(RULEFILEEDITOR))
{
// if caption is wider than the box, it's left-aligned, truncated at right
SetCaption(filename.c_str()); // replaces resource's generic caption
new TEdit(this,IDC_RULESEDIT,sizeof(tts->filechars));
SetTransferBuffer(tts);
}
//----------------------------------------------------------------------------
// end class SRulesEditDialog
//////////////////////////////////////////////////////////////////////////////
// transfer struct for the ViewOptionsDialog
struct ViewOpStruct
{
ViewOpStruct(); // ctor
// most of these BOOLS are used "as is" throughout pgm
BOOL autocycle; // whether to automatically calc a generation each IdleAction
BOOL autorandomizecolors; // whether to randomize colors with each new rule set
BOOL autoaborttests; // tests for dead system (no births or deaths)
BOOL randomrules; // whether to use a new random rule set with each new screen
BOOL byneighborcount; // YOU MUST SET dispmode TO MATCH WHAT THIS DICTATES
BOOL beepon; // use sound effects
BOOL fittowindow; // BUT YOU MUST SET SDIBWINDOW'S TO MATCH THIS ONE
char boardsize[20]; // text for dibwidth, dibheight
char percentfill[10]; // text for percentfill
char timelimit[10]; // text for time limit
}; //ViewOpStruct
//----------------------------------------------------------------------------
// constructor
ViewOpStruct::ViewOpStruct()
{
autocycle = TRUE; // set these to what you want for the program startup defaults
autorandomizecolors = FALSE;
autoaborttests = TRUE;
randomrules = FALSE; // always starts with standard game
byneighborcount = FALSE;
beepon = TRUE;
fittowindow = TRUE;
boardsize[0] = percentfill[0] = timelimit[0] = 0;
} //constructor
//----------------------------------------------------------------------------
// end class ViewOpStruct
//////////////////////////////////////////////////////////////////////////////
// Options dialog for the View menu.
class ViewOptionsDialog : public TDialog
{
public:
ViewOptionsDialog(TWindow* parent, ViewOpStruct* ots);
protected:
void CmHelp();
DECLARE_RESPONSE_TABLE(ViewOptionsDialog);
};
//----------------------------------------------------------------------------
DEFINE_RESPONSE_TABLE1(ViewOptionsDialog,TDialog)
EV_COMMAND(IDHELP,CmHelp),
END_RESPONSE_TABLE;
//----------------------------------------------------------------------------
// constructor
ViewOptionsDialog::ViewOptionsDialog(TWindow* parent, ViewOpStruct* ots)
: TDialog(parent,TResId(VIEWOPTIONS))
{
new TRadioButton(this,IDC_AUTOCYCLE,0); // 0 = TGroupBox*
new TRadioButton(this,IDC_RANDCOLORS,0);
new TRadioButton(this,IDC_ABORTTESTS,0);
new TRadioButton(this,IDC_RANDRULES,0);
new TRadioButton(this,IDC_BYNEIGHBORCOUNT,0);
new TRadioButton(this,IDC_SOUND,0);
new TRadioButton(this,IDC_FITTOWINDOW,0);
new TEdit(this,IDC_BOARDSIZE,20);
new TEdit(this,IDC_PCTFILL,10);
new TEdit(this,IDC_TIMELIMIT,10);
SetTransferBuffer(ots);
} // constructor
//----------------------------------------------------------------------------
void ViewOptionsDialog::CmHelp()
{ WinHelp(HelpFilename,HELP_PARTIALKEY,(DWORD)(LPSTR)"View Menu"); }
// CmHelp
//----------------------------------------------------------------------------
// // end class ViewOptionsDialog
//////////////////////////////////////////////////////////////////////////////
// A window in which a game of life takes place
class SLifeWindow : public SDibWindow
{
public:
SLifeWindow(TWindow* parent = 0);
~SLifeWindow();
friend ostream& operator << (ostream&, SLifeWindow&); // put-to operator
BOOL IdleAction(long idlecount);
protected:
// variables
LifeRule liferule; // the born/survive rules governing the current game
ViewOpStruct ots; // contains several View options variables
enum dispmodes { colorbyneighborcount, monochrome } dispmode;
long generations; // # of generations
long population; // count of live cells
long lastpop[10]; // previous generations live cell counts
int dibwidth; // size of dib, if size is user-specified
int dibheight; // if either is 0, dib is sized to current window
double percentfill; // % (0-100) of a new window to fill with random dots
uint timelimit; // in seconds; when exceeded, a new game starts
// functions
BOOL initialize(const char* filename); // create rule set from file data
void randomscreen(); // set up screen with initial random dots
void erasedib(); // erase to its background color
void draw();
void resetcounts(); // reset counting variables
void setdispmode(dispmodes); // changes dispmode
void UpdateViewMenu(); // check menu items to match settings
void SetupCaption(); // builds window caption and sets it
// overridden virtuals
BOOL CanClose();
// event handlers
void CmFileSave();
void CmFileLoadBMP();
void CmFileNew();
void CmFileOpen();
void CmFileEditAgain();
void CmColorsRandomize(); // different from SDibWindow's
void CmEditTransforms();
void CmEditMonoColor();
void CmViewAutoCycle();
void CmViewOptions();
void CmViewPopulation();
void CmHelpAbout();
DECLARE_RESPONSE_TABLE(SLifeWindow);
};
DEFINE_RESPONSE_TABLE1(SLifeWindow, SDibWindow)
EV_COMMAND(CM_EDITTRANSFORMS,CmEditTransforms),
EV_COMMAND(CM_FILENEW,CmFileNew),
EV_COMMAND(CM_FILEOPEN,CmFileOpen),
EV_COMMAND(CM_FILEEDITAGAIN,CmFileEditAgain),
EV_COMMAND(CM_FILESAVE,CmFileSave),
EV_COMMAND(CM_FILELOADBMP,CmFileLoadBMP),
EV_COMMAND(CM_COLORSRANDOMIZE,CmColorsRandomize),
EV_COMMAND(CM_EDITMONOCOLOR,CmEditMonoColor),
EV_COMMAND(CM_VIEWAUTOCYCLE,CmViewAutoCycle),
EV_COMMAND(CM_VIEWOPTIONS,CmViewOptions),
EV_COMMAND(CM_VIEWPOPULATION,CmViewPopulation),
EV_COMMAND(CM_HELPABOUT,CmHelpAbout),
END_RESPONSE_TABLE;
//----------------------------------------------------------------------------
// constructor
SLifeWindow::SLifeWindow(TWindow* parent) : SDibWindow(parent)
{
dibwidth = 160; // 0,0 means size the dib to window later
dibheight = 120; // 80x60 and 160x120 are good
// discard the dib created by SDibWindow, and recreate it in the size we want.
delete dib;
dib = 0;
dib = new SDib(dibwidth,dibheight,256,DIB_RGB_COLORS);
dib->SetColor(0,TColor::Black); // background to match window
dib->SetColor(1,TColor::LtGreen); // a good starting foreground color
dib->RandomizeColors(2); // preserve the 2 colors we just set
// initialize other variables
dispmode = monochrome; // don't use setdispmode because window isn't valid yet
percentfill = 30.; // good for standard rule set
timelimit = 60000; // startup default (it's a standard game)
resetcounts();
} //constructor
//----------------------------------------------------------------------------
// destructor
SLifeWindow::~SLifeWindow()
{
} //destructor
//----------------------------------------------------------------------------
// build and set window caption
void SLifeWindow::SetupCaption()
{
// if it came from a file, use its name, else the comment, either "standard" or "random"
string caption = string(GetApplication()->GetName()) + string(" - ");
caption += liferule.filesource.length() ? liferule.filesource : liferule.comment;
SetParentCaption(caption);
} //SetupCaption
//----------------------------------------------------------------------------
// reset counting variables
void SLifeWindow::resetcounts()
{
generations = population = 0L;
for(int i = 0 ; i < 10 ; i++)
lastpop[i] = 0L;
} //resetcounts
//----------------------------------------------------------------------------
// Initialize a new rule set, read from a file.
// If it fails, it reverts liferule to a standard Life game.
// returns TRUE if successful, FALSE if not.
BOOL SLifeWindow::initialize(const char* filename)
{
string buf(filename); // create string containing file name
buf.to_upper(); // toupper for error messages
if(!buf.contains(".RUL"))
{
string s = buf + "\nFile extension must be .RUL";
MessageBox(s.c_str(),"Cannot Load File",MB_OK);
return(FALSE);
}
ifstream infile(buf.c_str());
if(!infile)
{
string s = "File " + buf + "\nnot found.\n";
MessageBox(s.c_str(),"File Error",MB_OK);
return(FALSE);
}
infile >> liferule;
liferule.filesource = filename; // remember which file was read
ots.randomrules = FALSE; // prevents replacing the set we've just loaded!
ots.autoaborttests = FALSE; // just draw this design until user quits
randomscreen(); // give this set a new starting screen
return(TRUE);
} //initialize(read from file)
//----------------------------------------------------------------------------
// fill the dib with color(0) - seems there should be an easier way to do this
// note color(0) instead of Black in SDib::Erase()
void SLifeWindow::erasedib()
{
dib->Erase(dib->GetColor(0));
Invalidate(FALSE);
} //erasedib
//----------------------------------------------------------------------------
// use an existing .BMP file as the source for a new playing field (dib)
// (intentionally does not use SDibWindow's LoadBMP fn)
void SLifeWindow::CmFileLoadBMP()
{
chdir(DATADir.c_str());
// I don't plan to allow multiple SLifeWindows, but remember
// #error: note this may be shared by all of them.
static TOpenSaveDialog::TData inbfd(OFN_FILEMUSTEXIST, // flags ok
"Bitmap Files (*.BMP)|*.BMP|All Files (*.*)|*.*|",0,0,"BMP");
if((TFileOpenDialog(this,inbfd)).Execute() == IDOK)
{
if(makenewdib(inbfd.FileName,dibwidth,dibheight,FALSE))
{
if(ots.autorandomizecolors)
CmColorsRandomize();
Invalidate(FALSE);
resetcounts(); // blank screen: reset all to zero
}
char buf[MAXPATH]; // see also Get/SetCurrentDirectory (Win32 only)
getcwd(buf,MAXPATH);
DATADir = buf;
}
else
if(inbfd.Error)
MessageBox("Dialog box returned error.","Warning",MB_OK);
} //CmFileLoadBMP
//---------------------------------------------------------------------------
// set up a new screen filled with random dots by the percentage specified by percentfill
// 1d option maybe requires changes
void SLifeWindow::randomscreen()
{
makenewdib(0,dibwidth,dibheight,FALSE); // creates new blank dib
if(ots.autorandomizecolors)
CmColorsRandomize();
erasedib();
resetcounts(); // blank screen: reset all to zero
population = dib->RandomizeField(percentfill); // this creates the random dots
// this is a central place to do it: randomscreen() is called for both file and random sets.
SetupCaption();
Invalidate(FALSE);
// enable this when writing successive populations to disk (also see end of draw())
// ofstream outfile("life.pop");
// outfile << (liferule.filesource.length() ? liferule.filesource : liferule.comment) << endl;
// outfile << population << endl;
} //randomscreen
//----------------------------------------------------------------------------
// If liferule is ever a container of multiple LifeRules, this could be used to
// write the entire set, so it can be reconstructed later.
ostream& operator << (ostream& os, SLifeWindow&)
{
return(os);
}
//----------------------------------------------------------------------------
// calculate and display 1 generation: calculate it FROM one dib to another,
// then let Paint() blt the dib to the screen.
// This should always do one entire screenful before returning,
// so that the dib will always be fully-drawn (not in progress) when it's painted.
// 1d option requires changes, if goal is to grow downward on screen.
void SLifeWindow::draw()
{
if(!ots.autocycle) // do nothing if user turned it off
return;
// don't waste system time if this isn't the active application, or is minimized
if(IsIconic() || (GetActiveWindow() != GetApplication()->GetMainWindow()->HWindow))
return;
Invalidate(FALSE); // force unconditional screen display refresh
// if system is dead, start a new game. also causes the first game to load at startup.
// not an exact test. even a dead system can have cycles going that fool the test for the
// same populations. The 10 levels of test prevent it thinking it's dead when it isn't.
if(ots.autoaborttests)
{
BOOL reset = TRUE;
for(int i = 0 ; i < 10 ; i++)
if(population != lastpop[i])
{
reset = FALSE;
break;
}
if(sw.split() >= timelimit) // timer overrides even if pop still changing
reset = TRUE;
if(reset)
{
if(ots.beepon)
sndPlaySound("sldwst.wav",SND_SYNC);
// THESE ARE INTERESTING POSSIBILITIES COPIED FROM WINBROT:
// // here so you only do it if pgm is in "auto-run" mode (not for every CmFileNew)
// if(MapFiles.GetItemsInContainer()) // choose a .MAP file at random, for variety
// dib->LoadColors(MapFiles.Random());
// if(!random(2)) // doubles the available palettes
// dib->ReverseColors(); // avoid the Invalidate() in CmColorsReverse();
// dib->RotateColors(random(256)); // whichever palette you have, rotate it a bit
// if(autorotatecolors) // only if user has turned autorotate ON, set a direction
// {
// autorotatecolors = random(2); // set to 0 or 1, then 1 more to get 1 or -1,
// CmViewAutoRotateColors(); // and force menu item to match
// }
// old method, is probably fine.
// CmFileNew();
// new method, seems to work, not extensively tested yet:
// draw() is already time-greedy while Windows messages pile up for all apps.
// instead of invoking a lot of hard-to-trace code that may generate still more
// messages during this period when processing is suspended,
// just post the exact same message as if user pressed File|New, for handling by
// the MessageLoop after this IdleAction returns.
PostMessage(WM_COMMAND, CM_FILENEW);
return;
}
} // end if(ots.autoaborttests)
for(int i = 9 ; i > 0 ; i--) // archive populations for next dead system test
lastpop[i] = lastpop[i - 1];
lastpop[0] = population;
SDib source(*dib); // a copy of dib as a source for our calculations
int x, y; // point whose neighbors are being counted
int neighborcount; // number of live neighbors
BOOL isalive; // true if a point either survives or is born
for(y = 0 ; y < source.Height() ; y++)
{
// screen wraps in all directions. note (Height,Width) is off screen
int above = wrap(y - 1,0,source.Height() - 1);
int below = wrap(y + 1,0,source.Height() - 1);
for(x = 0 ; x < source.Width() ; x++)
{
int left = wrap(x - 1,0,source.Width() - 1);
int right = wrap(x + 1,0,source.Width() - 1);
neighborcount = 0;
if(source.GetPixelByte(left,above) != 0) // above left
neighborcount++;
if(source.GetPixelByte(x,above) != 0) // above
neighborcount++;
if(source.GetPixelByte(right,above) != 0) // above right
neighborcount++;
if(source.GetPixelByte(left,y) != 0) // point to left
neighborcount++;
// (source.GetPixelByte(x,y) != 0) // (the point itself)
if(source.GetPixelByte(right,y) != 0) // point to right
neighborcount++;
if(source.GetPixelByte(left,below) != 0) // below left
neighborcount++;
if(source.GetPixelByte(x,below) != 0) // below
neighborcount++;
if(source.GetPixelByte(right,below) != 0) // below right
neighborcount++;
if(source.GetPixelByte(x,y) != 0) // if point already alive,
{ // see if it remains alive
isalive = liferule.survive(neighborcount);
if(!isalive)
population--;
}
else // if cell is dead,
{ // see whether it is born
isalive = liferule.born(neighborcount);
if(isalive)
population++;
}
// set pixel in the destination dib in monochrome, either on or off
dib->SetPixelByte(x,y,isalive ? (uchar)1 : (uchar)0);
// dibdc.SetPixel(x,y,isalive ? dib->GetColor(1) : dib->GetColor(0));
} // end for(x)
} // end for(y)
// recolor points according to how many neighbors they have NOW.
// increases calculation time
// This makes pretty designs, but creates a somewhat misleading display. In a 2-state
// automaton, there are only 2 states, and so should only be 2 colors (monochrome).
// Using more makes you think there are more states than there are, and it only
// works because the calculation considers ANY non-state-zero to be state 1.
// If multi-state automata are made possible by using LifeField, this
// color-by-neighborcount option will be lost.
if(dispmode == colorbyneighborcount)
{
for(y = 0 ; y < dib->Height() ; y++)
{
// screen wraps in all directions. note (Height,Width) is off screen
int above = wrap(y - 1,0,dib->Height() - 1);
int below = wrap(y + 1,0,dib->Height() - 1);
for(x = 0 ; x < dib->Width() ; x++)
{
// skip everything if the point is off (dead) anyway
if(dib->GetPixelByte(x,y) == 0)
continue;
int left = wrap(x - 1,0,dib->Width() - 1);
int right = wrap(x + 1,0,dib->Width() - 1);
neighborcount = 0;
if(dib->GetPixelByte(left,above) != 0) // above left
neighborcount++;
if(dib->GetPixelByte(x,above) != 0) // above
neighborcount++;
if(dib->GetPixelByte(right,above) != 0) // above right
neighborcount++;
if(dib->GetPixelByte(left,y) != 0) // point to left
neighborcount++;
if(dib->GetPixelByte(right,y) != 0) // point to right
neighborcount++;
if(dib->GetPixelByte(left,below) != 0) // below left
neighborcount++;
if(dib->GetPixelByte(x,below) != 0) // below
neighborcount++;
if(dib->GetPixelByte(right,below) != 0) // below right
neighborcount++;
// remember if neighborcount is 0, you can't use that color!
dib->SetPixelByte(x,y,(uchar)max(neighborcount,1)); // revise its color
// dibdc.SetPixel(x,y,dib->GetColor(max(neighborcount,1))); // revise its color
} // end for(x)
} // end for(y)
} // end recolor by new neighborcount
// enable this to write successive populations to disk
// ofstream("life.pop",ios::app) << population << endl;
generations++;
} //draw
//----------------------------------------------------------------------------
// user sets view options in a dialog
void SLifeWindow::CmViewOptions()
{
// explicitly set only the variables not used "in place" in the ots by pgm
ots.byneighborcount = (dispmode == colorbyneighborcount);
ots.fittowindow = fittowindow; // match SDibWindow's
ostrstream(ots.boardsize,sizeof(ots.boardsize)) << dib->Width() << " " << dib->Height() << ends;
ostrstream(ots.percentfill,sizeof(ots.percentfill)) << percentfill << ends;
ostrstream(ots.timelimit,sizeof(ots.timelimit)) << ((double)timelimit / 60.) << ends;
if(ViewOptionsDialog(this,&ots).Execute() != IDOK)
return;
setdispmode(ots.byneighborcount ? colorbyneighborcount : monochrome);
if(fittowindow != ots.fittowindow) // set SDibWindow's to match
CmViewFitToWindow();
double d = percentfill;
istrstream(ots.percentfill) >> d;
if((d > 0.) && (d < 100.))
percentfill = d; // must set this before randomscreen(), below
int x = 0, y = 0;
istrstream(ots.boardsize) >> x >> y;
if((x >= 0) && (y >= 0)) // if both are valid
{
dibwidth = x;
dibheight = y;
}
d = 0.;
istrstream(ots.timelimit) >> d;
d *= 60.; // convert to seconds
if(d < 0.) // #error use range()?
d = 0.;
if(d > MAXUINT)
d = MAXUINT;
timelimit = (uint)d;
// disabled to allow changing options for the current rule set AND screen.
// randomscreen(); // make new dib and fill it up, but keep the current rule set
UpdateViewMenu(); // update the View menu checkmarks
} //CmViewOptions
//----------------------------------------------------------------------------
// check or uncheck the View Menu items, as needed, to match current state.
// these View Menu items are all handy to have available through accelerators
void SLifeWindow::UpdateViewMenu()
{
TMenu menu(*(GetApplication()->GetMainWindow()));
menu.CheckMenuItem(CM_VIEWAUTOCYCLE,
MF_BYCOMMAND | (ots.autocycle ? MF_CHECKED : MF_UNCHECKED));
} // UpdateViewMenu
//----------------------------------------------------------------------------
// reset the display mode and do whatever is required to implement the new mode
void SLifeWindow::setdispmode(dispmodes newmode)
{
dispmode = newmode;
UpdateViewMenu();
} //setdispmode
//----------------------------------------------------------------------------
void SLifeWindow::CmViewAutoCycle()
{
ots.autocycle = !ots.autocycle;
UpdateViewMenu();
}
//----------------------------------------------------------------------------
// set monochrome mode and specify its color
void SLifeWindow::CmEditMonoColor()
{
TChooseColorDialog::TData choose; // variable declaration: a structure
choose.Flags = CC_FULLOPEN | CC_RGBINIT;
choose.Color = dib->GetColor(1);
choose.CustColors = MyCustomColors; // a global colors array in my.h
if(TChooseColorDialog(this, choose).Execute() == IDOK)
{
dib->SetColor(0,TColor::Black); // start with black background; user can change it.
dib->SetColor(1,choose.Color); // obviously, if we specified the color,
setdispmode(monochrome); // we want monochrome mode
ots.autorandomizecolors = FALSE; // and no autorandomize
}
} //CmEditMonoColor
//----------------------------------------------------------------------------
void SLifeWindow::CmColorsRandomize()
{
int startcolor = 0;
// this test is for pgm startup
// comment is only this at program start. also monochrome at start.
if((liferule.comment == "Standard Life Game Rules") && (dispmode == monochrome))
{
dib->SetColor(0,TColor::Black); // background black
dib->SetColor(1,TColor::LtGreen); // a good starting foreground color
startcolor = 2;
}
dib->RandomizeColors(startcolor);
} //CmColorsRandomize
//----------------------------------------------------------------------------
// view stats about the current game
void SLifeWindow::CmViewPopulation()
{
ostrstream os;
os << "Generations: " << generations << endl;
os << "Population: " << population << endl;
os << "Field, Width " << dib->Width() << " x Height " << dib->Height();
os << " = Total: " << dib->Height() * dib->Width();
os << ends;
MessageBox(os.str(),"Statistics",MB_OK);
delete[] os.str();
} //CmViewPopulation
//----------------------------------------------------------------------------
// Edit the rule set file most recently opened (edited or loaded)
// in a dialog box with a single multi-line edit control.
// #error looks like it's (current rule set), not (edited or loaded). comment obsolete?
// After editing, the new design is automatically displayed.
void SLifeWindow::CmFileEditAgain()
{
static RuleEditorStruct tts; // static: it's big, and also allows buffer carryover
string filename = liferule.filesource; // edit the current rule set
if(!filename.length()) // but if it didn't come from a file,
{ // we must save it first.
if(MessageBox("Must save first. Save now?",
"Current rule set is not from a file",MB_YESNO) != IDYES)
return;
CmFileSave(); // so make user save it first,
filename = liferule.filesource; // and retrieve the name it now has
if(!filename.length()) // if user aborted the save
return; // just quit.
}
ifstream infile(filename.c_str(),ios::binary); // binary preserves CR/LF
if(infile) // if file already exists, read its data
{
// put chars from file into transfer struct
infile.get(tts.filechars,sizeof(tts.filechars),'\0');
infile.close(); // must close before renaming
}
// else // if remmed out, the previous edit buffer is automatically
// tts.filechars[0] = '\0'; // carried over & inserted: can be helpful, or confusing.
// the editing is performed on the tts buffer; the code block writes the text to the file
if(SRulesEditDialog(this,&tts,filename).Execute() == IDOK)
{
// save the text into new file; binary because it already has CRLFs
ofstream outfile(backupfile(filename).c_str(),ios::binary);
outfile << tts.filechars;
outfile.close(); // must close before initialize reads it
initialize(filename.c_str()); // reload the just-edited file
}
} //CmFileEditAgain
//----------------------------------------------------------------------------
// choose file to edit (and for subsequent re-edits)
// maybe this should be eliminated:
// just require that a file be File|Opened (loaded) before you can File|Edit Again
void SLifeWindow::CmEditTransforms()
{
chdir(DATADir.c_str());
static TOpenSaveDialog::TData filedata( // flags ok
OFN_PATHMUSTEXIST | OFN_CREATEPROMPT | OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY,
"Rule Files (*.RUL)|*.RUL|All Files (*.*)|*.*|",0,0,"RUL");
if((TFileOpenDialog(this,filedata)).Execute() == IDOK) // select a file
{
string filename(filedata.FileName); // can only edit .RUL
if(filename.contains(".rul"))
{
// initialize here to make sure THIS file is the one loaded, although
// the initialize() does a lot of work that will just be redone after editing
if(initialize(filename.c_str()))
CmFileEditAgain();
}
else
MessageBox("Selected file not .RUL","Cannot Edit",MB_OK);
char buf[MAXPATH]; // see also Get/SetCurrentDirectory (Win32 only)
getcwd(buf,MAXPATH);
DATADir = buf;
}
else
if(filedata.Error)
MessageBox("Dialog box returned an error code.","Warning",MB_OK);
} //CmEditTransforms
//----------------------------------------------------------------------------
// Load next rule set in the list. If a file fails, it goes on to the next one.
void SLifeWindow::CmFileNew()
{
sw.reset(); // every new rule set stops the timer
if(!ots.autocycle) // unsure if this IS desirable: might want to save board first.
CmViewAutoCycle(); // regardless of source, we want it on.
while(FileList.GetItemsInContainer())
{
// copy a name from the list, delete it, try to use it to initialize
if(initialize(FileList.GetNext().c_str()))
return;
}
// last resort, just creates new random screen for the current liferule.
// thus when the list becomes empty, the most recent liferule remains in effect.
if(ots.randomrules) // With each new field, you also have the option of a new rule set.
liferule.randomize();
randomscreen();
sw.start(); // every new random rule set starts the timer
} //CmFileNew
//----------------------------------------------------------------------------
// save the current rule set to a .RUL file.
// Do not merge handling for File|Save and File|SaveAs into 1 fn; they're completely different,
// and see FileSave calls where you must force user to save .RUL: saving as BMP won't suffice.
void SLifeWindow::CmFileSave()
{
chdir(DATADir.c_str());
static TOpenSaveDialog::TData filedata( // flags ok
OFN_PATHMUSTEXIST | OFN_CREATEPROMPT | OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY,
"Rule Files (*.RUL)|*.RUL|All Files (*.*)|*.*|",0,0,"RUL");
if(TFileSaveDialog(this,filedata).Execute() == IDOK) // select file
{
char buf[MAXPATH] = {0}; // also used later; see below
// only prompt for a comment if the set isn't already from a file
// if it's from a file, it likely already has one, which can be any artibrary length
if(!liferule.filesource.length())
{
// comment will be standard or random
ostrstream(buf,sizeof(buf)) << liferule.comment << ends;
if(TInputDialog(this,"Add Comment for Rule Set File?",
"Enter desired annotation, if any:",
buf,sizeof(buf)).Execute() == IDOK)
{
liferule.comment = buf;
}
}
ofstream(filedata.FileName) << liferule;
liferule.filesource = filedata.FileName; // now it has an associated file
SetupCaption(); // file name (for caption) may have changed
getcwd(buf,MAXPATH);
DATADir = buf;
}
} //CmFileSave
//----------------------------------------------------------------------------
void SLifeWindow::CmFileOpen()
{
chdir(DATADir.c_str());
TOpenSaveDialog::TData filedata(OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT, // flags ok
"Rule Files (*.RUL)|*.RUL|All Files (*.*)|*.*|",0,0,"RUL");
if((TFileOpenDialog(this,filedata)).Execute() == IDOK)
{
FileList.AddDialogList(filedata.FileName);
CmFileNew(); // initialize & display the first (valid) one
char buf[MAXPATH];
getcwd(buf,MAXPATH);
DATADir = buf;
}
else
if(filedata.Error)
MessageBox("Dialog box returned error.","Warning",MB_OK);
} //CmFileOpen
//----------------------------------------------------------------------------
void SLifeWindow::CmHelpAbout()
{ MessageBox("Copyright 1989-2001 by Steven Whitney",GetApplication()->GetName(),MB_OK); }
//----------------------------------------------------------------------------
BOOL SLifeWindow::CanClose()
{
if(!SDibWindow::CanClose())
return(FALSE);
return(TRUE);
} //CanClose
//----------------------------------------------------------------------------
BOOL SLifeWindow::IdleAction(long idlecount)
{
SDibWindow::IdleAction(idlecount);
draw();
return(TRUE);
}
//----------------------------------------------------------------------------
// end of class SLifeWindow
//////////////////////////////////////////////////////////////////////////////
class TMyApp : public TApplication
{
public:
TMyApp(const char far *title) : TApplication(title) {} // title used in case of error
protected:
virtual BOOL IdleAction(long idlecount);
virtual void InitMainWindow()
{
TFrameWindow* frame = new TFrameWindow(0,GetName(),new SLifeWindow);
frame->AssignMenu(TResId(MENU_1));
frame->Attr.AccelTable = TResId(MENU_1);
nCmdShow = SW_SHOWMAXIMIZED;
SetMainWindow(frame);
EnableCtl3d(TRUE);
}
};
//----------------------------------------------------------------------------
// a more complicated, hopefully faster, IdleAction bug fix:
// if idlecount is 0, let TFrame do everything it normally does now.
// this preserves the frequency with which TFrame calls IdleAction for *all* its children,
// and does its menu stuff, but the rest of the time just calls IdleAction for the client
// window (child), which does this app's continuous calculations.
BOOL TMyApp::IdleAction(long /* idlecount */)
{
// start with this because it changes as little as possible.
TApplication::IdleAction(0);
return TRUE; // return TRUE to get called back unconditionally
// this alternative should work
// #error something I did while testing this caused a fatal Kernel error in the IDE after
// exiting this pgm. suspect it wasn't this fn, but test more, and keep it in mind.
#if 0
if(idlecount == 0)
TApplication::IdleAction(0);
else
if(GetMainWindow() && GetMainWindow()->GetClientWindow())
return(GetMainWindow()->GetClientWindow()->IdleAction(idlecount));
return TRUE;
#endif // 0
}
//----------------------------------------------------------------------------
// end class TMyApp
//////////////////////////////////////////////////////////////////////////////
// OwlMain
int OwlMain(int /* argc */, char** /* argv */)
{
randomize();
string::set_case_sensitive(0);
// Run() calls: InitApplication(), InitInstance() { (which calls: InitMainWindow() },
// then displays main window.
TMyApp* myapp = new TMyApp(AppName);
int retval = myapp->Run();
delete myapp;
return(retval);
} //OwlMain
|
|
|
|