// // notes / improvements // 3) change sim to allow dynamic resolution updates // import std.stream; import std.stdio; import std.c.stdlib; import std.string; import std.c.time; import bcd.curses.curses; const int ERR = 255; alias extern (C) _win_st * curses_screen; // names for cell states const int LIVE=1; const int DEAD=0; // KEY CODES const int TERM_UP=3; const int TERM_DOWN=2; const int TERM_LEFT=4; const int TERM_RIGHT=5; const int TERM_ESC=27; const int TERM_ENTER=13; // console screen size short ROWMAX; short COLMAX; /* life Soup: * this is the object that maintains both the displayed and * cooking version of the life experiment. it knows how to * load a population, compute the next population and * display the current population */ class Soup { private: char[] sbuf; // the output buffer int ncount; int rminus, rplus, cminus, cplus; public: ubyte surface[][]; // the surface contents int cellpop; // the population this() // constructor { // allocate dynamic arrays sbuf = new char[ROWMAX*COLMAX+1]; // the output buffer surface = new ubyte[][](ROWMAX,COLMAX); // the surface contents // since D is "declaration is initialization" i dont have to set // all of the cells to 'false'. that's the default. // initialize the output buffer to spaces. for (int i=0; i // rows,cols start at 0,0 int irow, icol; char[][] celloc; File f = new File(filename); while (!f.eof()) { celloc = split(f.readLine(), ","); irow = std.string.atoi(celloc[0]); icol = std.string.atoi(celloc[1]); surface[irow][icol] = LIVE; sbuf[irow*COLMAX+icol] = '*'; cellpop++; } f.close(); } // returns the number of live neighbors int neighbors(int row, int col) { ncount = 0; // soup wraps around at limits (toroidal) rminus = row-1; if (rminus < 0) rminus = ROWMAX-1; rplus = row+1; if (rplus == ROWMAX) rplus = 0; cminus = col-1; if (cminus < 0) cminus = COLMAX-1; cplus = col+1; if (cplus == COLMAX) cplus = 0; // check for neighbors // since were using 0 and 1 for cell contents, we can simply add them up return surface[rminus][cminus] + surface[rminus][col] + surface[rminus][cplus] + surface[row][cminus] + surface[row][cplus] + surface[rplus][cminus] + surface[rplus][col] + surface[rplus][cplus]; } // make a cell active void spawn(int row, int col) { surface[row][col] = LIVE; cellpop++; } // kill a cell void kill(int row, int col) { surface[row][col] = DEAD; cellpop--; } // cook up a batch of this soup from soup stock void cook(Soup stock) { int curNeighbors, soffset; // compute the next gen soffset = 0; cellpop = stock.cellpop; // copy pop count // we could probably do this a little more elegantly // with foreach, though i'm not sure it would be more readable for (int srow=0; srow3) { // no, it dies surface[srow][scol] = DEAD; sbuf[soffset] = ' '; cellpop--; } else { // cell survives, copy it surface[srow][scol] = LIVE; sbuf[soffset] = '*'; } } else if (curNeighbors == 3) { // its lifeless, but spawns surface[srow][scol] = LIVE; cellpop++; sbuf[soffset] = '*'; } else { // its lifeless and stays lifeless // (probably not needed) surface[srow][scol] = DEAD; sbuf[soffset] = ' '; } soffset++; // bump output buffer loc } } } // edit this batch of soup // returns true if successful edit, else // user requested exit bool edit() { bool editing = true; int crow=0, ccol=0; char ch; nodelay(stdscr, FALSE); while (editing) { move(crow,ccol); ch = wgetch(stdscr); switch (ch) { case TERM_ENTER: // finished successful edit return true; case ' ': // toggle cell state surface[crow][ccol] = ! surface[crow][ccol]; if (surface[crow][ccol]) { cellpop++; // bump population echochar('*'); } else { cellpop--; // reduce population echochar(' '); } break; case TERM_UP: case 'i': // cursor up crow--; if (crow < 0) crow = ROWMAX-1; break; case TERM_DOWN: case 'k': // cursor down crow++; if (crow == ROWMAX) crow = 0; break; case TERM_LEFT: case 'j': // cursor left ccol--; if (ccol < 0) ccol = COLMAX-1; break; case TERM_RIGHT: case 'l': // cursor right ccol++; if (ccol == COLMAX) ccol = 0; break; case TERM_ESC: // exit editor return false; default: break; } status(); } } void display(curseWindow win) { // writes to default window move(0,0); addstr(this.sbuf.ptr); } void status() { move(ROWMAX-1,COLMAX-30); printw("pop %d",cellpop); } } /* * the 'lifemachine' is the simulator object. you could conceivably * run more than one at a time. we model the life simulation as two * collections of 'life' soup. the 'bowl' of soup is the one we're viewing. * the soup pot is (not suprisingly) where we cook up the next life * generation. I could have used 'petri dishes' as the metaphor but bowl and * pot seemed to work 8-) * * the lifeMachine just controls the initialization and swapping of the * bowl and pot at every tick. the soup knows how to cook up a new * generation and display it. */ class LifeMachine { private: Soup bowl; // the currently displayed life soup Soup pot; // the soup thats cooking (next gen) Soup temp; // for swap int gen; // current generation curseWindow curWin; public: this(curseWindow win) { // load the initial soup bowl = new Soup(); pot = new Soup(); bowl.load("life.conf"); // remember the current window curWin = win; // reset the generation count gen = 0; } void status() { move(ROWMAX-1, COLMAX-40); printw("gen %d",gen); } void tick() { // display the contents of the soup bowl on the screen bowl.display(curWin); // update status (generation, population) status(); bowl.status(); refresh(); // cook up the new soup pot.cook(bowl); // swap soups temp = bowl; bowl = pot; pot = temp; // bump generation gen++; } void simulate() { // the simulation loop. char ch; bool running = false; bool simulating = true; bool step = false; // since running is initially false, we start in edit mode while (simulating) { bowl.display(curWin); status(); bowl.status(); running = bowl.edit(); if (! running) simulating = false; // exit req from editor // now we're in run mode nodelay(stdscr, !step); // non-blocking char i/o if not single stepping while (running) { tick(); if (! step) usleep(31250); // sleep momentarily if ((ch = wgetch(stdscr)) != ERR) { // user wants something switch (ch) { case TERM_ENTER: // enter edit mode running = false; break; case 27: // exit the sim running = false; simulating = false; break; case ' ': // enter/exit single step mode // any other char (except enter or ESC) in step mode advances // one generation. step = ! step; nodelay(stdscr, ! step); default: // writefln("found spurious char %d",ch); // exit(1); break; } } } } } } /* curseWindow: * this isn't really necessary until we make something more complex than a * single window. right now it just handles curses initialization and shutdown. */ class curseWindow { private: curses_screen cScreen; // for time being just use default screen public: this() { // initialize curses window initscr(); cbreak(); noecho(); nonl(); intrflush(stdscr,FALSE); keypad(stdscr, TRUE); // use the standard screen cScreen = stdscr; // retrieve row and column max ROWMAX = stdscr._maxy + 1; COLMAX = stdscr._maxx + 1; } ~this() { // destroy and cleanup curses window. // we should probably explicitly return // window to "sane" state. endwin(); } // some methods void clear() { move(0,0); clear(); } // display a D string on it void show(string s) { addstr(std.string.toStringz(s)); } void cursorTo(int row, int col) { move(row,col); } void flush() { refresh(); } } /* * dlife: * a simple curses-based implementation of John Conway's Life. * its a double buffered implementation with no sparse-cell * optimizations. */ int main() { // create a window for the simulation auto cWin = new curseWindow(); // popup welcome screen wmove(stdscr,ROWMAX/2-1,COLMAX/2-1-9); waddstr(stdscr,"Conway's Life in D"); wmove(stdscr,ROWMAX/2,COLMAX/2-17); waddstr(stdscr,"By Jim Burnes. v1.0 04/13/2008 "); refresh(); sleep(3); erase(); // create the life machine on the window auto life = new LifeMachine(cWin); // start the machine life.simulate(); return 0; }