1
0
mirror of https://git.zap.org.au/git/trader.git synced 2025-02-02 15:08:13 -05:00
trader/src/game.c
John Zaitseff 040e1a5ad6 Rework the signal handler
Rework the signal handler to be somewhat more unsafe, but conceptually
cleaner, in operation.  It now ends the use of Curses, then reraises the
signal.  Remove almost all references to abort_game.
2011-08-09 23:20:56 +10:00

803 lines
20 KiB
C

/************************************************************************
* *
* Star Traders: A Game of Interstellar Trading *
* Copyright (C) 1990-2011, John Zaitseff *
* *
************************************************************************/
/*
Author: John Zaitseff <J.Zaitseff@zap.org.au>
$Id$
This file, game.c, contains the implementation of the starting and
ending game functions used in Star Traders, as well as the functions
for displaying the galaxy map and the player's status.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
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, see http://www.gnu.org/licenses/.
*/
#include "trader.h"
/************************************************************************
* Module-specific function prototypes *
************************************************************************/
/*
Function: ask_number_players - Ask for the number of players
Parameters: (none)
Returns: int - Number of players, 0 to load game, ERR to cancel
This internal function asks the user how many people will play. It
returns a number 1 to MAX_PLAYERS as a response, or 0 if a previous
game is to be loaded, or ERR if the user wishes to abort.
Please note that the window opened by this function is NOT closed!
*/
static int ask_number_players (void);
/*
Function: ask_game_number - Ask for the game number
Parameters: (none)
Returns: int - Game number (1-9) or ERR to cancel
This internal function asks the user which game number to load. It
returns a number 1 to 9 as a response, or ERR if the user wishes to
cancel loading a game.
Please note that the window opened by this function is NOT closed!
*/
static int ask_game_number (void);
/*
Function: ask_player_names - Ask for each of the players' names
Parameters: (none)
Returns: (nothing)
This internal function asks each player to type in their name. After
doing so, the players are asked whether they need instructions on
playing the game.
On entry, the global variable number_players is used to determine how
many people are playing. On exit, each player[].name is set. The
windows created by this function ARE closed, but not any other window.
Note also that txrefresh() is NOT called.
*/
static void ask_player_names (void);
/*
Function: cmp_player - Compare two player[] elements for sorting
Parameters: a, b - Pointers to elements to compare
Returns: int - Comparison of a and b
This internal function compares two player[] elements (of type
player_info_t) on the basis of total value, and returns -1 if a > b, 0
if a == b and 1 if a < b (note the change from the usual order!). It
is used for sorting players into descending total value order. Note
that the sort_value field MUST be computed before calling qsort()!
*/
static int cmp_player (const void *a, const void *b);
/************************************************************************
* Game function definitions *
************************************************************************/
/* These functions are documented either in the file "game.h" or in the
comments above. */
/***********************************************************************/
// init_game: Initialise a new game or load an old one
void init_game (void)
{
// Try to load an old game, if possible
if (game_num != 0) {
newtxwin(5, 30, 6, WCENTER, true, attr_status_window);
center(curwin, 2, attr_status_window, "Loading game %d... ", game_num);
wrefresh(curwin);
game_loaded = load_game(game_num);
deltxwin();
txrefresh();
}
// Initialise game data, if not already loaded
if (! game_loaded) {
number_players = 0;
while (number_players == 0) {
int choice = ask_number_players();
if (choice == ERR) {
abort_game = true;
return;
} else if (choice == 0) {
choice = ask_game_number();
if (choice != ERR) {
game_num = choice;
// Try to load the game, if possible
newtxwin(5, 30, 9, WCENTER, true, attr_status_window);
center(curwin, 2, attr_status_window,
"Loading game %d... ", game_num);
wrefresh(curwin);
game_loaded = load_game(game_num);
deltxwin();
txrefresh();
}
deltxwin(); // "Enter game number" window
deltxwin(); // "Number of players" window
txrefresh();
} else {
number_players = choice;
}
}
if (! game_loaded) {
int i, j, x, y;
ask_player_names();
deltxwin(); // "Number of players" window
txrefresh();
// Initialise player data (other than names)
for (i = 0; i < number_players; i++) {
player[i].cash = INITIAL_CASH;
player[i].debt = 0.0;
player[i].in_game = true;
for (j = 0; j < MAX_COMPANIES; j++) {
player[i].stock_owned[j] = 0;
}
}
// Initialise company data
for (i = 0; i < MAX_COMPANIES; i++) {
company[i].name = strdup(gettext(company_name[i]));
company[i].share_price = 0.0;
company[i].share_return = INITIAL_RETURN;
company[i].stock_issued = 0;
company[i].max_stock = 0;
company[i].on_map = false;
}
// Initialise galaxy map
for (x = 0; x < MAX_X; x++) {
for (y = 0; y < MAX_Y; y++) {
galaxy_map[x][y] = (randf() < STAR_RATIO) ?
MAP_STAR : MAP_EMPTY;
}
}
// Miscellaneous initialisation
interest_rate = INITIAL_INTEREST_RATE;
max_turn = option_max_turn ? option_max_turn : DEFAULT_MAX_TURN;
turn_number = 1;
// Select who is to go first
if (number_players == 1) {
first_player = 0;
current_player = 0;
} else {
first_player = randi(number_players);
current_player = first_player;
newtxwin(7, 50, 8, WCENTER, true, attr_normal_window);
center(curwin, 2, attr_normal, "The first player to go is");
center(curwin, 3, attr_highlight, "%s",
player[first_player].name);
wait_for_key(curwin, 5, attr_waitforkey);
deltxwin();
txrefresh();
}
}
}
quit_selected = false;
abort_game = false;
}
/***********************************************************************/
// ask_number_players: Ask for the number of players
static int ask_number_players (void)
{
int key, ret;
bool done;
// Ask for the number of players
newtxwin(5, 62, 3, WCENTER, true, attr_normal_window);
mvwaddstr(curwin, 2, 2, "Enter number of players ");
waddstr(curwin, "[");
attrpr(curwin, attr_keycode, "1");
waddstr(curwin, "-");
attrpr(curwin, attr_keycode, "%d", MAX_PLAYERS);
waddstr(curwin, "]");
waddstr(curwin, " or ");
attrpr(curwin, attr_keycode, "<C>");
waddstr(curwin, " to continue a game: ");
curs_set(CURS_ON);
wrefresh(curwin);
done = false;
while (! done) {
key = toupper(gettxchar(curwin));
if (key >= '1' && key <= MAX_PLAYERS + '0') {
wechochar(curwin, key | A_BOLD);
ret = key - '0';
done = true;
} else {
switch (key) {
case KEY_ESC:
case KEY_CANCEL:
case KEY_EXIT:
case KEY_CTRL('C'):
case KEY_CTRL('G'):
case KEY_CTRL('\\'):
ret = ERR;
done = true;
break;
case 'C':
wechochar(curwin, key | A_BOLD);
ret = 0;
done = true;
break;
default:
beep();
}
}
}
curs_set(CURS_OFF);
return ret;
}
/***********************************************************************/
// ask_game_number: Ask for the game number
int ask_game_number (void)
{
int key, ret;
bool done;
// Ask which game to load
newtxwin(5, 54, 6, WCENTER, true, attr_normal_window);
mvwaddstr(curwin, 2, 2, "Enter game number ");
waddstr(curwin, "[");
attrpr(curwin, attr_keycode, "1");
waddstr(curwin, "-");
attrpr(curwin, attr_keycode, "9");
waddstr(curwin, "]");
waddstr(curwin, " or ");
attrpr(curwin, attr_keycode, "<CTRL><C>");
waddstr(curwin, " to cancel: ");
curs_set(CURS_ON);
wrefresh(curwin);
done = false;
while (! done) {
key = gettxchar(curwin);
if (key >= '1' && key <= '9') {
wechochar(curwin, key | A_BOLD);
ret = key - '0';
done = true;
} else {
switch (key) {
case KEY_ESC:
case KEY_CANCEL:
case KEY_EXIT:
case KEY_CTRL('C'):
case KEY_CTRL('G'):
case KEY_CTRL('\\'):
ret = ERR;
done = true;
break;
default:
beep();
}
}
}
curs_set(CURS_OFF);
return ret;
}
/***********************************************************************/
// ask_player_names: Ask for each of the players' names
void ask_player_names (void)
{
if (number_players == 1) {
// Ask for the player's name
newtxwin(5, WIN_COLS - 4, 9, WCENTER, true, attr_normal_window);
mvwaddstr(curwin, 2, 2, "Please enter your name: ");
int x = getcurx(curwin);
int w = getmaxx(curwin) - x - 2;
player[0].name = NULL;
while (true) {
int ret = gettxstr(curwin, &player[0].name, NULL, false,
2, x, w, attr_input_field);
if (ret == OK && strlen(player[0].name) != 0) {
break;
} else {
beep();
}
}
newtxwin(5, 44, 6, WCENTER, true, attr_normal_window);
mvwaddstr(curwin, 2, 2, "Do you need any instructions?");
if (answer_yesno(curwin, attr_keycode)) {
show_help();
}
} else {
// Ask for all of the player names
bool entered[MAX_PLAYERS];
bool done, modified;
int cur, len, i;
newtxwin(number_players + 5, WIN_COLS - 4, 9, WCENTER,
true, attr_normal_window);
center(curwin, 1, attr_title, " Enter Player Names ");
for (i = 0; i < number_players; i++) {
player[i].name = NULL;
entered[i] = false;
mvwprintw(curwin, i + 3, 2, "Player %d:", i + 1);
}
int x = getcurx(curwin) + 1;
int w = getmaxx(curwin) - x - 2;
cur = 0;
done = false;
while (! done) {
int ret = gettxstr(curwin, &player[cur].name, &modified, true,
3 + cur, x, w, attr_input_field);
switch (ret) {
case OK:
// Make sure name is not an empty string
len = strlen(player[cur].name);
entered[cur] = (len != 0);
if (len == 0) {
beep();
}
// Make sure name has not been entered already
for (i = 0; i < number_players; i++) {
if (i != cur && player[i].name != NULL
&& strcmp(player[i].name, player[cur].name) == 0) {
entered[cur] = false;
beep();
break;
}
}
// Move to first name for which ENTER has not been pressed
done = true;
for (cur = 0; cur < number_players; cur++) {
if (! entered[cur]) {
done = false;
break;
}
}
break;
case ERR:
beep();
break;
case KEY_UP:
// Scroll up to previous name (with wrap-around)
if (modified) {
entered[cur] = false;
}
if (cur == 0) {
cur = number_players - 1;
} else {
cur--;
}
break;
case KEY_DOWN:
// Scroll down to next name (with wrap-around)
if (modified) {
entered[cur] = false;
}
if (cur == number_players - 1) {
cur = 0;
} else {
cur++;
}
break;
default:
beep();
}
}
newtxwin(5, 50, 6, WCENTER, true, attr_normal_window);
mvwaddstr(curwin, 2, 2, "Does any player need instructions?");
if (answer_yesno(curwin, attr_keycode)) {
show_help();
}
}
deltxwin(); // "Need instructions?" window
deltxwin(); // "Enter player names" window
}
/***********************************************************************/
// end_game: Finish playing the current game
void end_game (void)
{
int i;
char *buf;
if (abort_game) {
// init_game() was cancelled by user
return;
}
buf = malloc(BUFSIZE);
if (buf == NULL) {
err_exit_nomem();
}
newtxwin(7, 40, 9, WCENTER, true, attr_error_window);
center(curwin, 1, attr_error_title, " Game Over ");
center(curwin, 3, attr_error_highlight, "The game is over after %d turns",
turn_number - 1);
wait_for_key(curwin, 5, attr_error_waitforkey);
deltxwin();
for (i = 0; i < number_players; i++) {
show_status(i);
}
if (number_players == 1) {
l_strfmon(buf, BUFSIZE, "%1n", total_value(0));
newtxwin(9, 60, 8, WCENTER, true, attr_normal_window);
center(curwin, 1, attr_title, " Total Value ");
center2(curwin, 4, attr_normal, attr_highlight,
"Your total value was ", "%s", buf);
wait_for_key(curwin, 7, attr_waitforkey);
deltxwin();
} else {
// Sort players on the basis of total value
for (i = 0; i < number_players; i++) {
player[i].sort_value = total_value(i);
}
qsort(player, number_players, sizeof(player_info_t), cmp_player);
newtxwin(number_players + 10, WIN_COLS - 4, 3, WCENTER,
true, attr_normal_window);
center(curwin, 1, attr_title, " Game Winner ");
center2(curwin, 3, attr_normal, attr_highlight, "The winner is ",
"%s", player[0].name);
if (player[0].sort_value == 0.0) {
center2(curwin, 4, attr_normal, attr_blink, "who is ",
"*** BANKRUPT ***");
} else {
l_strfmon(buf, BUFSIZE, "%1n", player[0].sort_value);
center2(curwin, 4, attr_normal, attr_highlight,
"with a value of ", "%s", buf);
}
int w = getmaxx(curwin) - 33;
wattrset(curwin, attr_subtitle);
snprintf(buf, BUFSIZE, "Total Value (%s)", lconvinfo.currency_symbol);
mvwprintw(curwin, 6, 2, "%5s %-*.*s %18s ", "", w, w, "Player", buf);
wattrset(curwin, attr_normal);
for (i = 0; i < number_players; i++) {
l_strfmon(buf, BUFSIZE, "%!18n", player[i].sort_value);
mvwprintw(curwin, i + 7, 2, "%5s %-*.*s %18s ",
gettext(ordinal[i + 1]), w, w, player[i].name, buf);
}
wait_for_key(curwin, getmaxy(curwin) - 2, attr_waitforkey);
deltxwin();
}
free(buf);
}
/***********************************************************************/
// show_map: Display the galaxy map on the screen
void show_map (bool closewin)
{
int n, x, y;
newtxwin(MAX_Y + 4, WIN_COLS, 1, WCENTER, true, attr_map_window);
// Draw various borders
mvwaddch(curwin, 2, 0, ACS_LTEE);
whline(curwin, ACS_HLINE, getmaxx(curwin) - 2);
mvwaddch(curwin, 2, getmaxx(curwin) - 1, ACS_RTEE);
// Display current player and turn number
wattrset(curwin, attr_mapwin_title);
mvwaddstr(curwin, 1, 2, " ");
waddstr(curwin, "Player: ");
n = getmaxx(curwin) - getcurx(curwin) - 4;
wattrset(curwin, attr_mapwin_highlight);
wprintw(curwin, "%-*.*s", n, n, player[current_player].name);
wattrset(curwin, attr_mapwin_title);
waddstr(curwin, " ");
if (turn_number != max_turn) {
const char *initial = "Turn: ";
char *buf = malloc(BUFSIZE);
if (buf == NULL) {
err_exit_nomem();
}
int len1 = strlen(initial);
int len2 = snprintf(buf, BUFSIZE, "%d", turn_number);
mvwaddstr(curwin, 1, getmaxx(curwin) - (len1 + len2) - 6, " ");
waddstr(curwin, initial);
attrpr(curwin, attr_mapwin_highlight, "%s", buf);
waddstr(curwin, " ");
free(buf);
} else {
const char *buf = "*** Last Turn ***";
int len = strlen(buf);
mvwaddstr(curwin, 1, getmaxx(curwin) - len - 6, " ");
attrpr(curwin, attr_mapwin_blink, "%s", buf);
waddstr(curwin, " ");
}
wattrset(curwin, attr_map_window);
// Display the actual map
for (y = 0; y < MAX_Y; y++) {
wmove(curwin, y + 3, 2);
for (x = 0; x < MAX_X; x++) {
map_val_t m = galaxy_map[x][y];
switch (m) {
case MAP_EMPTY:
waddch(curwin, PRINTABLE_MAP_VAL(m) | attr_map_empty);
break;
case MAP_OUTPOST:
waddch(curwin, PRINTABLE_MAP_VAL(m) | attr_map_outpost);
break;
case MAP_STAR:
waddch(curwin, PRINTABLE_MAP_VAL(m) | attr_map_star);
break;
default:
waddch(curwin, PRINTABLE_MAP_VAL(m) | attr_map_company);
break;
}
waddch(curwin, ' ' | attr_map_empty);
}
}
if (closewin) {
// Wait for the user to press any key
wrefresh(curwin);
newtxwin(WIN_LINES - MAX_Y - 5, WIN_COLS, MAX_Y + 5, WCENTER,
true, attr_normal_window);
wait_for_key(curwin, 2, attr_waitforkey);
deltxwin(); // Wait for key window
deltxwin(); // Galaxy map window
txrefresh();
}
}
/***********************************************************************/
// show_status: Display the player's status
void show_status (int num)
{
double val;
int i, line;
assert(num >= 0 && num < number_players);
newtxwin(MAX_COMPANIES + 15, WIN_COLS, 1, WCENTER, true,
attr_normal_window);
center(curwin, 1, attr_title, " Stock Portfolio ");
center2(curwin, 2, attr_normal, attr_highlight, "Player: ", "%s",
player[num].name);
val = total_value(num);
if (val == 0.0) {
center(curwin, 11, attr_blink, "* * * B A N K R U P T * * *");
} else {
char *buf = malloc(BUFSIZE);
if (buf == NULL) {
err_exit_nomem();
}
// Check to see if any companies are on the map
bool none = true;
for (i = 0; i < MAX_COMPANIES; i++) {
if (company[i].on_map) {
none = false;
break;
}
}
if (none) {
center(curwin, 8, attr_normal, "No companies on the map");
} else {
// Handle the locale's currency symbol
snprintf(buf, BUFSIZE, "share (%s)", lconvinfo.currency_symbol);
wattrset(curwin, attr_subtitle);
mvwprintw(curwin, 4, 2, " %-22s %12s %10s %10s %10s ",
"", "Price per", "", "Holdings", "Company");
mvwprintw(curwin, 5, 2, " %-22s %12s %10s %10s %10s ",
"Company", buf, "Return (%)", "(shares)", "owner (%)");
wattrset(curwin, attr_normal);
for (line = 6, i = 0; i < MAX_COMPANIES; i++) {
if (company[i].on_map) {
l_strfmon(buf, BUFSIZE, "%!12n", company[i].share_price);
mvwprintw(curwin, line, 2,
" %-22s %10s %10.2f %'10ld %10.2f ",
company[i].name, buf,
company[i].share_return * 100.0,
player[num].stock_owned[i],
(company[i].stock_issued == 0) ? 0.0 :
((double) player[num].stock_owned[i] * 100.0)
/ company[i].stock_issued);
line++;
}
}
}
line = 15;
l_strfmon(buf, BUFSIZE, "%18n", player[num].cash);
center2(curwin, line++, attr_normal, attr_highlight, "Current cash: ",
" %s ", buf);
if (player[num].debt != 0.0) {
l_strfmon(buf, BUFSIZE, "%18n", player[num].debt);
center2(curwin, line++, attr_normal, attr_highlight,
"Current debt: ", " %s ", buf);
center2(curwin, line++, attr_normal, attr_highlight,
"Interest rate: ", " %17.2f%% ", interest_rate * 100.0);
}
l_strfmon(buf, BUFSIZE, "%18n", val);
center2(curwin, line + 1, attr_highlight, attr_title,
"Total value: ", " %s ", buf);
free(buf);
}
wait_for_key(curwin, getmaxy(curwin) - 2, attr_waitforkey);
deltxwin();
txrefresh();
}
/***********************************************************************/
// total_value: Calculate a player's total financial worth
double total_value (int num)
{
double val;
int i;
assert(num >= 0 && num < number_players);
val = player[num].cash - player[num].debt;
for (i = 0; i < MAX_COMPANIES; i++) {
if (company[i].on_map) {
val += player[num].stock_owned[i] * company[i].share_price;
}
}
return val;
}
/***********************************************************************/
// cmp_player: Compare two player[] elements for sorting
int cmp_player (const void *a, const void *b)
{
/* This implementation assumes that each player[] element has already
had its sort_value set to the result of total_value(). Note also
that the function result is reversed from the normal order, so
that players are sorted into descending order */
const player_info_t *aa = (const player_info_t *) a;
const player_info_t *bb = (const player_info_t *) b;
if (aa->sort_value > bb->sort_value) {
return -1;
} else if (aa->sort_value < bb->sort_value) {
return 1;
} else {
return 0;
}
}
/***********************************************************************/
// End of file