C++ ncurses help

kbjradmin

New Member
Messages
512
Reaction score
2
Points
0
i am teaching myself C++ and as a project to get used to the language i am making a simple dice game using ncurses. i got everything to work up to the point where i tried to put the individual dice in their own windows. there is no error occurring, but the window isn't showing up. please help.

Code:
/*****************************************************************************
* 
* dice.cpp
* Author: James Brumond
* Date Created: 24 October, 2009
* 
* A simple dice game.
* 
*****************************************************************************/

#include <string>
#include <string.h>
#include <iostream>
#include <sstream>
#include <ctime>
#include <cstdlib>
#include <stdio.h>
#include <stdlib.h>
#include <ncurses.h>

using namespace std;

// function prototypes
void endProg(int);
void startCurses();
WINDOW *createNewWin(int, int, int, int);

// initialize windows
WINDOW *wins [5] = { createNewWin(5, 9, 0, 10), createNewWin(5, 9, 0, 20), createNewWin(5, 9, 0, 30), createNewWin(5, 9, 0, 40),
            createNewWin(5, 9, 0, 50) };

// the die class
class Die {
    private:
        int side;
        int value;
        int values [6];
        const char* display;
        const char* displays [6];
    public:
        Die(unsigned int);
        void roll();
        void set(int);
        int getValue();
        const char* getDisplay();
        int win;
        void draw();
};

// initialize the die
// set all of the die's values, display texts, and the window where the die
// outputs data to
Die::Die(unsigned int window) {
    // when die is a one
    values[0] = 1;
    displays[0] = (char*)"\n         \n    o    \n         \n\n  Die %d", (window + 1);
    
    // when die is two
    values[1] = 2;
    displays[1] = (char*)"\n  o      \n         \n      o  \n\n  Die %d", (window + 1);
    
    // when die is three
    values[2] = 3;
    displays[2] = (char*)"\n  o      \n    o    \n      o  \n\n  Die %d", (window + 1);
    
    // when die is four
    values[3] = 4;
    displays[3] = (char*)"\n  o   o  \n         \n  o   o  \n\n  Die %d", (window + 1);
    
    // when die is five
    values[4] = 5;
    displays[4] = (char*)"\n  o   o  \n    o    \n  o   o  \n\n  Die %d", (window + 1);
    
    // when die is six
    values[5] = 6;
    displays[5] = (char*)"\n  o   o  \n  o   o  \n  o   o  \n\n  Die %d", (window + 1);
    
    // set values
    side = 0;
    value = values[side];
    display = displays[side];
    win = window;
}

// takes a number between 0 and 5
// sets the die's value and display according to 
void Die::set(int newSide) {
    if (newSide > 5 || newSide < 0) {
        cout << "void Die::set(int) must recieve a value between 0 and 5." << endl;
        cout << "fatal error: aborting..." << endl;
        endProg(1);
    }
    else {
        side = newSide;
        value = values[side];
        display = displays[side];
    }
}

// roll the die
// uses srand with the current time for seed and rand % 6 to get
// a random value between 0 and 5
void Die::roll() {
    srand(time(0));
    side = rand() % 6;
    set(side);
}

// get the numerical value of the die
int Die::getValue() {
    return value;
}

// get the text to be displayed for the die
const char* Die::getDisplay() {
    return display;
}

void Die::draw() {
    Die::roll();
    wprintw(wins[win], Die::getDisplay());
    wborder(wins[win], '|', '|', '-', '-', '+', '+', '+', '+');
    
}

// end curses and exit the program
void endProg(int status) {
    try {
        endwin();
    }
    catch(...) {  }
    exit(status);
}

// start the curses library
void startCurses() {
    initscr();
    cbreak();
    keypad(stdscr, true);
}

// create a new window with a border
WINDOW *createNewWin(int height, int width, int y, int x) {
    WINDOW *localWin;
    localWin = newwin(height, width, y, x);
    wborder(localWin, '|', '|', '-', '-', '+', '+', '+', '+');
    wrefresh(localWin);
    return localWin;
}

// main entry point of the program
int main() {
    startCurses();
    Die dice [5] = { Die(0), Die(1), Die(2), Die(3), Die(4) };
    dice[0].roll();
    dice[0].draw();
    while (0 == 0) {
        char ch = getch();
        if (ch == 'q') {
            break;
        }
        dice[0].roll();
        dice[0].draw();
    }
    endProg(0);
}
 
Last edited:

misson

Community Paragon
Community Support
Messages
2,572
Reaction score
72
Points
48
i am teaching myself C++ and as a project to get used to the language i am making a simple dice game using ncurses. i got everything to work up to the point where i tried to put the individual dice in their own windows. there is no error occurring, but the window isn't showing up. please help.
Learn to use whatever debugger you have available. Break on Die::draw() and you'll see that wins[win] is NULL. Keep reading for an explanation.

Code:
// initialize windows
WINDOW *wins [5] = { createNewWin(5, 9, 0, 10), createNewWin(5, 9, 0, 20), createNewWin(5, 9, 0, 30), createNewWin(5, 9, 0, 40),
            createNewWin(5, 9, 0, 50) };
Initializing globals happens before main() is invoked, which means the createNewWin calls happen before initscr() is called. Either move the initialization of wins[] to within main() or go fully OO. The former is easier, the latter purer and more instructive. It's also more complex. First you apply the Resource Acquisition Is Initialization (RAII) principle--create a curses class that handles curses initialization; creating an instance of the curses class calls the curses initialization functions. Since you only want to initialize curses once, make the curses class a singleton. Then write a Curses::Window class (the "Curses::" refers to a namespace, not an outer class) that gets the curses instance in the Curses::Window constructor. The first window that gets created causes curses to be initialized. If no window is created, curses isn't initialized.

In any case, globals are evil. Try to find another way.

Magic numbers are also evil. "5", representing the number of dice, is scattered all over. If you changed the number of dice, you might miss an instance. The simplest solution is to define a global constant that sets the number of dice.

Code:
const int diceCount=5;

Another solution is to write a macro that calculates the length of an array:
Code:
#define $LENGTH(arr) (sizeof(arr) / sizeof(*arr))
WINDOW *wins [5];

class Die {
    public:
        Die(unsigned int=0);
...
    Dice dice[$LENGTH(wins)];
    // start from 1 since dice[0] is already initialized to Dice(0)
    for (int i=1; i < $LENGTH(wins); ++i) {
        dice[i] = Dice(i);
    }
Note that $LENGTH won't work with pointers.

Code:
    displays[0] = (char*)"\n         \n    o    \n         \n\n  Die %d", (window + 1);
I don't know what you think this line does, but it doesn't do it. The comma operator causes the left side to be evaluated, then the right. The value of the comma is the value of the right side (the left side's value is discarded). The line is equivalent to:
Code:
    displays[0] = (char*)"\n         \n    o    \n         \n\n  Die %d";
    window + 1;
Note that the comma operator has lowest precedence. You can overload the comma, but that's generally a bad idea.

Code:
void Die::roll() {
    srand(time(0));
    side = rand() % 6;
You only need to seed a random number generator once. Seeding it more than once will have a pronounced affect on the quality of the numbers it produces. Of course, since we're dealing with rand(), it's not like they're quality to begin with.


Code:
void Die::draw() {
    Die::roll();
    wprintw(wins[win], Die::getDisplay());
    wborder(wins[win], '|', '|', '-', '-', '+', '+', '+', '+');
    
}
...
        dice[0].roll();
        dice[0].draw();
roll() gets called twice with each iteration of the loop. Since the call to roll() within draw() is almost certainly wrong, remove that one.

You never refresh the window in Die::draw so it gets redrawn on the screen.

Having to redraw the frame is messy. One solution is to draw the frame in a parent and draw the dice face in a child. Another is to make the frame a part of the face.

Code:
    while (0 == 0) {
This works fine. I just wanted to mention the clearer alternatives:
Code:
while (true) {...}
for (;;) {...}
Both of those are idiomatic forever loops. Someone reading the code knows it's intentional. With other forever conditions (such as 0 == 0), there's a chance, however small, that the loop condition is in error. Some people go so far as to "#define forever for (;;)".

Instead of a forever loop, you could move the character test to the loop condition:
Code:
    Die dice [$LENGTH(wins)] = { Die(0), Die(1), Die(2), Die(3), Die(4) };
    do {
        for (int i=0; i<$LENGTH(wins); ++i) {
            dice[i].roll();
            dice[i].draw();
        }
    } while ('q' != getch());


Code:
    while (0 == 0) {
        char ch = getch();
getch() gets a character from the default window, stdscr, which is returned by initscr() (unless there's an error). This ends up covering your other windows. There are various solutions: use wgetch(), resize stdscr with wresize(), make the dice windows subwindows of stdscr using subwin() or derwin() rather than newwin(). I recommend the last.

Code:
void Die::set(int newSide) {
    if (newSide > 5 || newSide < 0) {
        cout << "void Die::set(int) must recieve a value between 0 and 5." << endl;
        cout << "fatal error: aborting..." << endl;
        endProg(1);
    }
...
void endProg(int status) {
    try {
        endwin();
    }
    catch(...) {  }
    exit(status);
}
This is backwards. Die::set should throw an exception. Catch that in main outside the "while" loop, print the error. You use exceptions because the time when an error happens isn't the best time to handle the error. Maybe the function up the call chain from Die::set has a way of fixing the error; by exiting, you prevent recovery. Since uncaught exceptions will cause the program to exit, there's also no need to explicitly call exit().

endwin is from a C library; it won't throw exceptions.


One other issue: a Die is tightly coupled to displaying itself, but the wins and dice arrays are uncoupled; this is backwards. Separate the model (the stuff you'd write if you didn't need to interact with a user) from the view (how the model is displayed). You don't need to go full MVC in this case.
 

kbjradmin

New Member
Messages
512
Reaction score
2
Points
0
thank you misson, i will work on this some more and if i have problems or don't understand something in your post, i'll post back.



edit:

I just wanted to mention that i did get the windows working. now i get to actually make it into a game...

anyway, thanks for the help, misson. here is my new code if you're interested.

Code:
/*****************************************************************************
* 
* dice.cpp
* Author: James Brumond <kbjr14@gmail.com>
* Date Created: 24 October, 2009
* 
* A simple dice game.
* 
*****************************************************************************/

#include <string>
#include <string.h>
#include <iostream>
#include <sstream>
#include <ctime>
#include <cstdlib>
#include <stdio.h>
#include <stdlib.h>
#include <ncurses.h>

using namespace std;

// global definitions
const int diceCount = 5;



/*****************************************************************************
*   START THE CONTROL CLASS
*****************************************************************************/



class Control {
	private:
	public:
		Control();
		void endProg(int status);
} control;

Control::Control() {
	return;
}

// end curses and exit the program
void Control::endProg(int status) {
	exit(status);
}



/*****************************************************************************
*   END THE CONTROL CLASS
*****************************************************************************/



/*****************************************************************************
*   START THE CURSES HANDLING CLASS
*****************************************************************************/

class CursesHandle {
	private:	
		WINDOW *windows [20];
		int windowCount;
	public:
		CursesHandle();
		~CursesHandle();
		WINDOW *createNewWin(int, int, int, int);
} curses;

// initialize curses and the window array
CursesHandle::CursesHandle() {
	initscr();
	cbreak();
	keypad(stdscr, true);
	curs_set(0);
	windows[0] = *&stdscr;
	windowCount = 1;
	return;
}

// stop curses
CursesHandle::~CursesHandle() {
	curs_set(1);
	endwin();
}

// create a new window and add a reference to it to the
// CursesHandle.windows array
WINDOW *CursesHandle::createNewWin(int height, int width, int y, int x) {
	WINDOW *localWin;
	localWin = newwin(height, width, y, x);
	wrefresh(localWin);
	windows[windowCount] = *&localWin;
	windowCount++;
	return *&localWin;
}

/*****************************************************************************
*   END THE CURSES HANDLING CLASS
*****************************************************************************/



/*****************************************************************************
*   START THE DIE CLASS
*****************************************************************************/



class Die {
	private:
		int side;
		int value;
		int values [6];
		const char* display [6];
		const char* displays [6][6];
		WINDOW *win;
		bool held;
	public:
		Die(unsigned int=0);
		void roll();
		void set(int);
		int getValue();
		const char** getDisplay();
		void draw();
		bool isHeld();
		void toggleHold();
		void setHold(bool);
};

// initialize the die
// set all of the die's values, display texts, and the window where the die
// outputs data to
Die::Die(unsigned int die) {
	// build the title string
	const char* title;
	switch (die) {
		case 0: return; break;
		case 1: title = " Die One "; break;
		case 2: title = " Die Two "; break;
		case 3: title = "Die Three"; break;
		case 4: title = " Die Four"; break;
		case 5: title = " Die Five"; break;
	}
	
	// when die is a one
	values[0] = 1;
	displays[0][0] = (char*)"+-------+";
	displays[0][1] = (char*)"|       |";
	displays[0][2] = (char*)"|   o   |";
	displays[0][3] = (char*)"|       |";
	displays[0][4] = (char*)"+-------+";
	displays[0][5] = (char*)title;
	
	// when die is two
	values[1] = 2;
	displays[1][0] = (char*)"+-------+"; 
	displays[1][1] = (char*)"| o     |";
	displays[1][2] = (char*)"|       |";
	displays[1][3] = (char*)"|     o |";
	displays[1][4] = (char*)"+-------+";
	displays[1][5] = (char*)title;
	
	// when die is three
	values[2] = 3;
	displays[2][0] = (char*)"+-------+";
	displays[2][1] = (char*)"| o     |";
	displays[2][2] = (char*)"|   o   |";
	displays[2][3] = (char*)"|     o |"; 
	displays[2][4] = (char*)"+-------+";
	displays[2][5] = (char*)title;
	
	// when die is four
	values[3] = 4;
	displays[3][0] = (char*)"+-------+";
	displays[3][1] = (char*)"| o   o |";
	displays[3][2] = (char*)"|       |";
	displays[3][3] = (char*)"| o   o |";
	displays[3][4] = (char*)"+-------+";
	displays[3][5] = (char*)title;
	
	// when die is five
	values[4] = 5;
	displays[4][0] = (char*)"+-------+";
	displays[4][1] = (char*)"| o   o |";
	displays[4][2] = (char*)"|   o   |";
	displays[4][3] = (char*)"| o   o |";
	displays[4][4] = (char*)"+-------+";
	displays[4][5] = (char*)title;
	
	// when die is six
	values[5] = 6;
	displays[5][0] = (char*)"+-------+";
	displays[5][1] = (char*)"| o   o |";
	displays[5][2] = (char*)"| o   o |";
	displays[5][3] = (char*)"| o   o |";
	displays[5][4] = (char*)"+-------+";
	displays[5][5] = (char*)title;
	
	// set values
	side = 0;
	value = values[side];
	for (int i = 0; i < 6; i++) {
		display[i] = displays[side][i];
	}
	win = curses.createNewWin(6, 9, 0, ((10 * die) - 10));
	held = false;
	return;
}

// takes a number between 0 and 5
// sets the die's value and display according to 
void Die::set(int newSide) {
	if (newSide > 5 || newSide < 0) {
		cout << "void Die::set(int) must recieve a value between 0 and 5." << endl;
		cout << "fatal error: aborting..." << endl;
		control.endProg(1);
	}
	else {
		side = newSide;
		value = values[side];
		for (int i = 0; i < 6; i++) {
			display[i] = displays[side][i];
		}
	}
	return;
}

// roll the die
// uses srand with the current time for seed and rand % 6 to get
// a random value between 0 and 5
void Die::roll() {
	side = (rand() % 6);
	set(side);
	return;
}

// get the numerical value of the die
int Die::getValue() {
	return value;
}

// draw the die to the screen in the proper location
void Die::draw() {
	for (int i = 0; i < 6; i++) {
		mvwprintw(win, i, 0, display[i]);
	}
	wrefresh(win);
	return;
}

// returns whether or not the die is being held
bool Die::isHeld() {
	return held;
}

// toggles whether or not the die is held
void Die::toggleHold() {
	if (isHeld()) {
		held = false;
	}
	else {
		held = true;
	}
	return;
}

// set manually whether or not the die is held
void Die::setHold(bool hold) {
	held = hold;
	return;
}



/*****************************************************************************
*   END THE DIE CLASS
*****************************************************************************/



/*****************************************************************************
*   START THE DICE CLASS
*****************************************************************************/



class Dice {
	private:
	public:
		Dice();
		Die dice [diceCount];
		void rollAll();
		void drawAll();
		void rollUnheld();
		void drawUnheld();
		void unholdAll();
} dice;

// initialize the dice
Dice::Dice() {
	for (int i = 0; i < diceCount; i++) {
		dice[i] = Die(i+1);
	}
}

// roll all of the dice
void Dice::rollAll() {
	for (int i = 0; i < diceCount; i++) {
		dice[i].roll();
	}
}

// draw all of the dice
void Dice::drawAll() {
	for (int i = 0; i < diceCount; i++) {
		dice[i].draw();
	}
}

// roll all of the unheld dice
void Dice::rollUnheld() {
	for (int i = 0; i < diceCount; i++) {
		if (! dice[i].isHeld()) {
			dice[i].roll();
		}
	}
}

// draw all of the unheld dice
void Dice::drawUnheld() {
	for (int i = 0; i < diceCount; i++) {
		if (! dice[i].isHeld()) {
			dice[i].draw();
		}
	}
}

// unhold all of the dice
void Dice::unholdAll() {
	for (int i = 0; i < diceCount; i++) {
		dice[i].setHold(false);
	}
}



/*****************************************************************************
*   END THE DICE CLASS
*****************************************************************************/



// main entry point of the program
int main() {
	// seed rand()
	srand(time(0));
	
	// get user input and take action accordingly
	// exit on keypress 'q'
	do {
		// draw dice to screen
		dice.rollAll();
		dice.drawAll();
	} while (getch() != 'q');
	
	// end ncurses and exit
	control.endProg(0);
}
 
Last edited:

misson

Community Paragon
Community Support
Messages
2,572
Reaction score
72
Points
48
That's better. You still have a few magic fives, but they're easily taken care of by adding a "Die::sides" field and using that instead. Class Control seems a needless indirection, and Die::set() should throw an exception rather than calling exit(). Better yet, make Die::set() protected so you don't need to worry about getting a bad value from outside your Die class hierarchy. Make newSide and Die::side unsigned, and replace the error check in Die::set() with a assert(newSide < sides) for a sanity check in development builds. In main(), there's also no need to call exit(); you can use a return statement.

Now that you've done your own work, here's an illustration of some of the stuff I mentioned.

Singleton class to initialize curses:
Code:
/* in Curses.h */
namespace Curses {
    class Curses {
    public:
        static Curses& getInstance();
        ~Curses();
    private:
        Curses();
    };
}

/* in Curses.cc */
namespace Curses {
    Curses& Curses::getInstance() {
        /* static variable "curses_" creates a single instance of Curses that exists over the lifetime
           of the process and will be destroyed when process exits.
         */
        static Curses curses_;
        return curses_;
    }

    Curses::Curses() {
        initscr();
        savetty();
        cbreak();
        keypad(stdscr, true);
        noecho();
    }

    Curses::~Curses() {
        resetty();
        endwin();
    }
}

Here's how it's used:
Code:
/* in Window.h */
namespace Curses {
    class Window {
    public:
        Window(int x, int y, int width, int height, WINDOW* par=stdscr);
        Window(int x, int y, int width, int height, Window& par);
        virtual ~Window();

        Window& resize(int width, int height);
        // update window on screen
        virtual Window& draw();
        // regenerate content & redraw
        virtual Window& refresh();
        virtual Window& print(const char*);
        virtual Window& print(int i);
        virtual Window& moveCursor(int x, int y);

    protected:
        void init(int x, int y, WINDOW* par);

        WINDOW *wndw_;
        int width_, height_;
    };
}

/* in Window.cc */
    void Window::init(int x, int y, WINDOW* par) {
        Curses& c = Curses::getInstance();
        wndw_ = derwin(par, height_, width_, y, x);
        refresh();
    }
The line: Curses& c = Curses::getInstance(); is what ensures that curses is initialized.

The singleton implementation of Curses isn't superior to your CursesHandle, but it does have the advantage that you can't accidentally initialize curses more than once. Another advantage, one that doesn't matter in this application, is a matter of efficiency: if you don't use curses, you don't have to pay the cost of initializing it. Also, Curses doesn't pollute the global namespace with a global instance, but you could fix that in your code in various ways, such as by making CursesHandle curses into a static member of CursesHandle or by putting it in a namespace.

The downside to Curses is that you might neglect to get the instance in a class that requires curses, but as all curses classes descend from Window in my example, this isn't a problem.

Die is very simple:
Code:
/* in Die.h */
class Die {
public:
    Die::Die(unsigned sides=6) : sides_(sides), value_(0) {}

    unsigned roll();
    operator unsigned();
    unsigned sides();
    Die& setSides(unsigned sides);

protected:
    unsigned sides_;
    unsigned value_;
    Random r_;
};

/* in Die.cc */
unsigned Die::roll() {
    // add code to notify any watchers that value is changing
    return value_ = r_() % sides_;
}

Die::operator unsigned()
{ return value_ + 1; }

unsigned Die::sides()
{ return sides_; }

Die& Die::setSides(unsigned sides) {
    sides_ = sides;
    return *this;
}

Die relies on a simple wrapper around random() named Random that takes care of seeding. Displaying is handled by a DiceView class. Check out the attached source for the rest of the implementation. It's far from perfect, but should be illustrative of various techniques.
 

Attachments

  • dice.zip
    8.9 KB · Views: 4
Last edited:
Top