/************************************************************************ * * * Star Traders: A Game of Interstellar Trading * * Copyright (C) 1990-2024, John Zaitseff * * * ************************************************************************/ /* Author: John Zaitseff SPDX-License-Identifier: GPL-3.0-or-later $Id$ This file, move.c, contains the implementation of functions that make and process a game move in Star Traders. 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 https://www.gnu.org/licenses/. */ #include "trader.h" /************************************************************************ * Module-specific macros * ************************************************************************/ // Calculate positions near (x,y), taking the edge of the galaxy into account #define GALAXY_MAP_LEFT(x, y) (((x) <= 0) ? MAP_EMPTY : galaxy_map[(x) - 1][(y)]) #define GALAXY_MAP_RIGHT(x, y) (((x) >= (MAX_X - 1)) ? MAP_EMPTY : galaxy_map[(x) + 1][(y)]) #define GALAXY_MAP_UP(x, y) (((y) <= 0) ? MAP_EMPTY : galaxy_map[(x)][(y) - 1]) #define GALAXY_MAP_DOWN(x, y) (((y) >= (MAX_Y - 1)) ? MAP_EMPTY : galaxy_map[(x)][(y) + 1]) #define assign_vals(x, y, left, right, up, down) \ do { \ (left) = GALAXY_MAP_LEFT((x), (y)); \ (right) = GALAXY_MAP_RIGHT((x), (y)); \ (up) = GALAXY_MAP_UP((x), (y)); \ (down) = GALAXY_MAP_DOWN((x), (y)); \ } while (0) /************************************************************************ * Module-specific function prototypes * ************************************************************************/ /* Function: bankrupt_player - Make the current player bankrupt Parameters: forced - True if bankruptcy is forced by Bank Returns: (nothing) This function makes the current player bankrupt, whether by their own choice or as a result of action by the Interstellar Trading Bank. All shares are returned to the appropriate companies, any debt is cancelled and any cash is confiscated. On exit, quit_selected is true if all players are bankrupt. */ static void bankrupt_player (bool forced); /* Function: try_start_new_company - See if a new company can be started Parameters: x, y - Coordinates of position on map Returns: (nothing) This function attempts to establish a new company if the position (x,y) is in a suitable location and if no more than MAX_COMPANIES are already present. */ static void try_start_new_company (int x, int y); /* Function: merge_companies - Merge two companies together Parameters: a, b - Companies to merge Returns: (nothing) This function merges two companies on the galaxy map; the one with the highest value takes over. The parameters a and b are actual values from the galaxy map. */ static void merge_companies (map_val_t a, map_val_t b); /* Function: include_outpost - Include any outposts into the company Parameters: num - Company on which to operate x, y - Coordinates of position on map Returns: (nothing) This function includes the outpost at (x,y) into company num, increasing the share price by calling inc_share_price(). It also checks surrounding locations for further outposts to include. */ static void include_outpost (int num, int x, int y); /* Function: inc_share_price - Increase the share price of a company Parameters: num - Company on which to operate inc - Base increment for the share price Returns: (nothing) This function increments the share price, maximum stock available and the share return of company num, using inc as the basis for doing so. */ static void inc_share_price (int num, double inc); /* Function: adjust_values - Adjust various company-related values Parameters: (none) Returns: (nothing) This function adjusts the cost of shares for companies on the galaxy map, their return, the Bank interest rate, etc. */ static void adjust_values (void); /* Function: cmp_game_move - Compare two game_move[] elements for sorting Parameters: a, b - Elements to compare Returns: int - Comparison of a and b This internal function compares two game_move[] elements (of type move_rec_t) and returns -1 if a < b, 0 if a == b and 1 if a > b. It is used for sorting game moves into ascending order. */ static int cmp_game_move (const void *a, const void *b); /************************************************************************ * Game move function definitions * ************************************************************************/ // These functions are documented in the file "move.h" /***********************************************************************/ // select_moves: Select NUMBER_MOVES random moves void select_moves (void) { int count; int x, y, i, j; int tx, ty; bool unique; // How many empty spaces are there in the galaxy map? count = 0; for (x = 0; x < MAX_X; x++) { for (y = 0; y < MAX_Y; y++) { if (galaxy_map[x][y] == MAP_EMPTY) { count++; } } } if (count < NUMBER_MOVES) { quit_selected = true; return; } // Generate unique random moves for (i = 0; i < NUMBER_MOVES; i++) { do { do { tx = randi(MAX_X); ty = randi(MAX_Y); } while (galaxy_map[tx][ty] != MAP_EMPTY); unique = true; for (j = i - 1; j >= 0; j--) { if (tx == game_move[j].x && ty == game_move[j].y) { unique = false; break; } } } while (! unique); game_move[i].x = tx; game_move[i].y = ty; } // Sort moves from left to right qsort(game_move, NUMBER_MOVES, sizeof(move_rec_t), cmp_game_move); quit_selected = false; } /***********************************************************************/ // get_move: Wait for the player to enter their move selection_t get_move (void) { selection_t selection = SEL_NONE; if (quit_selected || abort_game) { return SEL_QUIT; } // Display map without closing window show_map(false); // Display current move choices on the galaxy map for (int i = 0; i < NUMBER_MOVES; i++) { chtype *movestr = CHTYPE_GAME_MOVE(i); wmove(curwin, game_move[i].y + 3, game_move[i].x * 2 + 2); while (*movestr != 0) { waddch(curwin, *movestr++); } } wrefresh(curwin); // Show menu of choices for the player newtxwin(5, WIN_COLS, 19, WCENTER, false, 0); while (selection == SEL_NONE) { chtype *promptbuf = xmalloc(BUFSIZE * sizeof(chtype)); int promptend, promptwidth; wbkgdset(curwin, attr_normal_window); werase(curwin); box(curwin, 0, 0); left(curwin, 2, 2, attr_normal, attr_keycode, 0, 1, /* TRANSLATORS: Each label may be up to 37 characters wide (for <1> and <2>) or 38 characters wide (for <3> and ). The sequences "^{" and "^}" change the character rendition (attributes) and take up no space. */ _("^{<1>^} Display stock portfolio")); left(curwin, 3, 2, attr_normal, attr_keycode, 0, 1, _("^{<2>^} Declare bankruptcy")); left(curwin, 2, getmaxx(curwin) / 2, attr_normal, attr_keycode, 0, 1, _("^{<3>^} Save and end the game")); left(curwin, 3, getmaxx(curwin) / 2, attr_normal, attr_keycode, 0, 1, _("^{^} Quit the game")); mkchstr(promptbuf, BUFSIZE, attr_normal, attr_keycode, attr_choice, 1, getmaxx(curwin) - 4, &promptwidth, 1, /* TRANSLATORS: The maximum column width is either 38 characters (including the trailing space), or 76 characters minus the length of the longest of the four strings above this one. The sequences "^{", "^}", "^[" and "^]" do not take up any room. "%lc" takes up either one or two columns, depending on the appropriate "output|GameMoves" string in the current PO file. */ _("Select move [^[%lc^]-^[%lc^]/^{1^}-^{3^}/^{^}]: "), (wint_t) PRINTABLE_GAME_MOVE(0), (wint_t) PRINTABLE_GAME_MOVE(NUMBER_MOVES - 1)); if (promptwidth > getmaxx(curwin) / 2 - 2) { promptend = promptwidth + 2; leftch(curwin, 1, 2, promptbuf, 1, &promptwidth); } else { promptend = getmaxx(curwin) / 2; rightch(curwin, 1, promptend, promptbuf, 1, &promptwidth); } curs_set(CURS_ON); wrefresh(curwin); free(promptbuf); // Get the actual selection made by the player while (selection == SEL_NONE) { wint_t key; if (gettxchar(curwin, &key) == OK) { // Ordinary wide character int i; bool found; if (iswupper(*keycode_game_move)) { key = towupper(key); } else if (iswlower(*keycode_game_move)) { key = towlower(key); } for (i = 0, found = false; keycode_game_move[i] != L'\0'; i++) { if (keycode_game_move[i] == (wchar_t) key) { found = true; selection = (selection_t) i; curs_set(CURS_OFF); left(curwin, 1, promptend, attr_normal, attr_choice, 0, 1, /* TRANSLATORS: A game usually consists of DEFAULT_MAX_TURN (50) turns. On each turn, the computer randomly selects NUMBER_MOVES (20) moves (positions on the map); each player selects just one move per turn. "Move" refers to the player's choice. */ _("Move ^{%lc^}"), (wint_t) PRINTABLE_GAME_MOVE(i)); break; } } if (! found) { switch (key) { case L'1': curs_set(CURS_OFF); show_status(current_player); curs_set(CURS_ON); break; case L'2': selection = SEL_BANKRUPT; curs_set(CURS_OFF); left(curwin, 1, promptend, attr_normal, attr_normal | A_BOLD, 0, 1, _("^{<2>^} (Declare bankruptcy)")); break; case L'3': selection = SEL_SAVE; curs_set(CURS_OFF); left(curwin, 1, promptend, attr_normal, attr_normal | A_BOLD, 0, 1, _("^{<3>^} (Save and end the game)")); break; default: beep(); } } } else { // Function or control key switch (key) { case KEY_ESC: case KEY_CANCEL: case KEY_EXIT: case KEY_CTRL('C'): case KEY_CTRL('G'): case KEY_CTRL('\\'): selection = SEL_QUIT; curs_set(CURS_OFF); left(curwin, 1, promptend, attr_normal, attr_normal | A_BOLD, 0, 1, _("^{^} (Quit the game)")); break; default: beep(); } } } // Clear the menu choices (but not the prompt!) mvwhline(curwin, 2, 2, ' ' | attr_normal, getmaxx(curwin) - 4); mvwhline(curwin, 3, 2, ' ' | attr_normal, getmaxx(curwin) - 4); // Ask the player to confirm their choice right(curwin, 2, promptend, attr_normal, attr_keycode, 0, 1, _("Are you sure? [^{Y^}/^{N^}] ")); wrefresh(curwin); if (! answer_yesno(curwin)) { selection = SEL_NONE; } // Save the game if required if (selection == SEL_SAVE) { chtype *chbuf = xmalloc(BUFSIZE * sizeof(chtype)); int width; bool saved = false; if (game_loaded) { // Save the game to the same game number mkchstr(chbuf, BUFSIZE, attr_status_window, 0, 0, 1, WIN_COLS - 7, &width, 1, _("Saving game %d... "), game_num); newtxwin(5, width + 5, 7, WCENTER, true, attr_status_window); centerch(curwin, 2, 0, chbuf, 1, &width); wrefresh(curwin); saved = save_game(game_num); deltxwin(); txrefresh(); } if (! saved) { // Ask which game to save bool done; int widthbuf[2]; int lines, maxwidth; int choice; lines = mkchstr(chbuf, BUFSIZE, attr_normal, attr_keycode, 0, 2, WIN_COLS - 7, widthbuf, 2, _("Enter game number [^{1^}-^{9^}] " "or ^{^} to cancel: ")); assert(lines == 1 || lines == 2); maxwidth = ((lines == 1) ? widthbuf[0] : MAX(widthbuf[0], widthbuf[1])) + 5; newtxwin(lines + 4, maxwidth, 8, WCENTER, true, attr_normal_window); leftch(curwin, 2, 2, chbuf, lines, widthbuf); curs_set(CURS_ON); wrefresh(curwin); done = false; while (! done) { wint_t key; if (gettxchar(curwin, &key) == OK) { // Ordinary wide character if (key >= L'1' && key <= L'9') { left(curwin, getcury(curwin), getcurx(curwin), A_BOLD, 0, 0, 1, "%lc", key); wrefresh(curwin); choice = key - L'0'; done = true; } else { beep(); } } else { // Function or control key switch (key) { case KEY_ESC: case KEY_CANCEL: case KEY_EXIT: case KEY_CTRL('C'): case KEY_CTRL('G'): case KEY_CTRL('\\'): choice = ERR; done = true; break; default: beep(); } } } curs_set(CURS_OFF); if (choice != ERR) { // Try to save the game, if possible game_num = choice; mkchstr(chbuf, BUFSIZE, attr_status_window, 0, 0, 1, WIN_COLS - 7, &width, 1, _("Saving game %d... "), game_num); newtxwin(5, width + 5, 7, WCENTER, true, attr_status_window); centerch(curwin, 2, 0, chbuf, 1, &width); wrefresh(curwin); saved = save_game(game_num); deltxwin(); txrefresh(); } deltxwin(); // "Enter game number" window txrefresh(); } if (saved) { selection = SEL_QUIT; } else { // Make the next try at saving ask the player for a game number game_loaded = false; game_num = 0; selection = SEL_NONE; } free(chbuf); } } return selection; } /***********************************************************************/ // process_move: Process the move selected by the player void process_move (selection_t selection) { if (selection == SEL_QUIT) { // The players want to end the game quit_selected = true; } if (quit_selected || abort_game) { deltxwin(); // "Select move" window deltxwin(); // Galaxy map window txrefresh(); return; } if (selection == SEL_BANKRUPT) { // A player wants to give up: make them bankrupt bankrupt_player(false); } else { // Process a selection from game_move[] assert(selection >= SEL_MOVE_FIRST && selection <= SEL_MOVE_LAST); map_val_t left, right, up, down; map_val_t nearby, cur; int x = game_move[selection].x; int y = game_move[selection].y; assign_vals(x, y, left, right, up, down); if ( left == MAP_EMPTY && right == MAP_EMPTY && up == MAP_EMPTY && down == MAP_EMPTY) { // The position is out in the middle of nowhere... galaxy_map[x][y] = MAP_OUTPOST; } else if ( ! IS_MAP_COMPANY(left) && ! IS_MAP_COMPANY(right) && ! IS_MAP_COMPANY(up) && ! IS_MAP_COMPANY(down)) { // See if a company can be established try_start_new_company(x, y); } else { // See if two (or more!) companies can be merged if (IS_MAP_COMPANY(left) && IS_MAP_COMPANY(right) && left != right) { galaxy_map[x][y] = left; merge_companies(left, right); assign_vals(x, y, left, right, up, down); } if (IS_MAP_COMPANY(left) && IS_MAP_COMPANY(up) && left != up) { galaxy_map[x][y] = left; merge_companies(left, up); assign_vals(x, y, left, right, up, down); } if (IS_MAP_COMPANY(left) && IS_MAP_COMPANY(down) && left != down) { galaxy_map[x][y] = left; merge_companies(left, down); assign_vals(x, y, left, right, up, down); } if (IS_MAP_COMPANY(right) && IS_MAP_COMPANY(up) && right != up) { galaxy_map[x][y] = right; merge_companies(right, up); assign_vals(x, y, left, right, up, down); } if (IS_MAP_COMPANY(right) && IS_MAP_COMPANY(down) && right != down) { galaxy_map[x][y] = right; merge_companies(right, down); assign_vals(x, y, left, right, up, down); } if (IS_MAP_COMPANY(up) && IS_MAP_COMPANY(down) && up != down) { galaxy_map[x][y] = up; merge_companies(up, down); assign_vals(x, y, left, right, up, down); } } // See if an existing company can be expanded nearby = (IS_MAP_COMPANY(left) ? left : (IS_MAP_COMPANY(right) ? right : (IS_MAP_COMPANY(up) ? up : (IS_MAP_COMPANY(down) ? down : MAP_EMPTY)))); if (nearby != MAP_EMPTY) { galaxy_map[x][y] = nearby; inc_share_price(MAP_TO_COMPANY(nearby), SHARE_PRICE_INC); } /* If a company expanded (or merged or formed), see if share price should be incremented */ cur = galaxy_map[x][y]; if (IS_MAP_COMPANY(cur)) { // Is a star nearby? if (left == MAP_STAR) { inc_share_price(MAP_TO_COMPANY(cur), SHARE_PRICE_INC_STAR); } if (right == MAP_STAR) { inc_share_price(MAP_TO_COMPANY(cur), SHARE_PRICE_INC_STAR); } if (up == MAP_STAR) { inc_share_price(MAP_TO_COMPANY(cur), SHARE_PRICE_INC_STAR); } if (down == MAP_STAR) { inc_share_price(MAP_TO_COMPANY(cur), SHARE_PRICE_INC_STAR); } // Is an outpost nearby? if (left == MAP_OUTPOST) { include_outpost(MAP_TO_COMPANY(cur), x - 1, y); } if (right == MAP_OUTPOST) { include_outpost(MAP_TO_COMPANY(cur), x + 1, y); } if (up == MAP_OUTPOST) { include_outpost(MAP_TO_COMPANY(cur), x, y - 1); } if (down == MAP_OUTPOST) { include_outpost(MAP_TO_COMPANY(cur), x, y + 1); } } } if (! quit_selected) { adjust_values(); } deltxwin(); // "Select move" window deltxwin(); // Galaxy map window txrefresh(); } /***********************************************************************/ // next_player: Get the next player void next_player (void) { int i; bool all_out; all_out = true; for (i = 0; i < number_players; i++) { if (player[i].in_game) { all_out = false; break; } } if (all_out) { quit_selected = true; } else { do { current_player++; if (current_player == number_players) { current_player = 0; } if (current_player == first_player) { turn_number++; } } while (! player[current_player].in_game); } } /************************************************************************ * Module-specific function definitions * ************************************************************************/ // These functions are documented at the start of this file /***********************************************************************/ // bankrupt_player: Make the current player bankrupt void bankrupt_player (bool forced) { if (forced) { txdlgbox(MAX_DLG_LINES, 50, 7, WCENTER, attr_error_window, attr_error_title, attr_error_highlight, 0, 0, attr_error_waitforkey, _(" Bankruptcy Court "), /* TRANSLATORS: %ls is the player's name. */ _("%ls has been declared bankrupt " "by the Interstellar Trading Bank."), player[current_player].name); } else { txdlgbox(MAX_DLG_LINES, 50, 7, WCENTER, attr_error_window, attr_error_title, attr_error_highlight, 0, 0, attr_error_waitforkey, _(" Bankruptcy Court "), /* TRANSLATORS: %ls is the player's name. */ _("%ls has declared bankruptcy."), player[current_player].name); } txrefresh(); // Confiscate all assets belonging to player player[current_player].in_game = false; for (int i = 0; i < MAX_COMPANIES; i++) { company[i].stock_issued -= player[current_player].stock_owned[i]; player[current_player].stock_owned[i] = 0; } player[current_player].cash = 0.0; player[current_player].debt = 0.0; // Is anyone still left in the game? bool all_out = true; for (int i = 0; i < number_players; i++) { if (player[i].in_game) { all_out = false; break; } } if (all_out) { quit_selected = true; } } /***********************************************************************/ // try_start_new_company: See it a new company can be started void try_start_new_company (int x, int y) { bool all_on_map; map_val_t left, right, up, down; int i, j; assert(x >= 0 && x < MAX_X); assert(y >= 0 && y < MAX_Y); assign_vals(x, y, left, right, up, down); if ( left != MAP_OUTPOST && left != MAP_STAR && right != MAP_OUTPOST && right != MAP_STAR && up != MAP_OUTPOST && up != MAP_STAR && down != MAP_OUTPOST && down != MAP_STAR) { return; } all_on_map = true; for (i = 0; i < MAX_COMPANIES; i++) { if (! company[i].on_map) { all_on_map = false; break; } } if (all_on_map) { // The galaxy cannot support any more companies galaxy_map[x][y] = MAP_OUTPOST; } else { // Create the new company txdlgbox(MAX_DLG_LINES, 50, 7, WCENTER, attr_normal_window, attr_title, attr_normal, attr_highlight, 0, attr_waitforkey, _(" New Company "), _("A new company has been formed!\nIts name is ^{%ls^}."), company[i].name); txrefresh(); galaxy_map[x][y] = (map_val_t) COMPANY_TO_MAP(i); company[i].share_price = INITIAL_SHARE_PRICE; company[i].share_return = INITIAL_RETURN; company[i].stock_issued = INITIAL_STOCK_ISSUED; company[i].max_stock = INITIAL_MAX_STOCK; company[i].on_map = true; for (j = 0; j < number_players; j++) { player[j].stock_owned[i] = 0; } player[current_player].stock_owned[i] = INITIAL_STOCK_ISSUED; } } /***********************************************************************/ // merge_companies: Merge two companies together void merge_companies (map_val_t a, map_val_t b) { int aa = MAP_TO_COMPANY(a); int bb = MAP_TO_COMPANY(b); assert(aa >= 0 && aa < MAX_COMPANIES); assert(bb >= 0 && bb < MAX_COMPANIES); double val_aa = company[aa].share_price * company[aa].stock_issued * (1.0 + company[aa].share_return); double val_bb = company[bb].share_price * company[bb].stock_issued * (1.0 + company[bb].share_return); double bonus; long int old_stock, new_stock, total_new; chtype *chbuf = xmalloc(BUFSIZE * sizeof(chtype)); int lines, width, widthbuf[4]; chtype *chbuf_aa, *chbuf_bb; int width_aa, width_bb; int x, y, w, i, ln; if (val_aa < val_bb) { // Make sure aa is the dominant company map_val_t t; int tt; t = a; a = b; b = t; tt = aa; aa = bb; bb = tt; } // Display information about the merger lines = mkchstr(chbuf, BUFSIZE, attr_normal, attr_highlight, 0, 4, WIN_COLS - 8, widthbuf, 4, _("^{%ls^} has just merged into ^{%ls^}.\n" "Please note the following transactions:\n"), company[bb].name, company[aa].name); newtxwin(number_players + lines + 10, WIN_COLS - 4, lines + 6 - number_players, WCENTER, true, attr_normal_window); center(curwin, 1, 0, attr_title, 0, 0, 1, _(" Company Merger ")); centerch(curwin, 3, 0, chbuf, lines, widthbuf); mkchstr(chbuf, BUFSIZE, attr_highlight, 0, 0, 1, getmaxx(curwin) / 2, &width_aa, 1, "%ls", company[aa].name); chbuf_aa = xchstrdup(chbuf); mkchstr(chbuf, BUFSIZE, attr_highlight, 0, 0, 1, getmaxx(curwin) / 2, &width_bb, 1, "%ls", company[bb].name); chbuf_bb = xchstrdup(chbuf); mkchstr(chbuf, BUFSIZE, attr_normal, 0, 0, 1, getmaxx(curwin) / 2, &width, 1, /* TRANSLATORS: "Old stock" refers to the company that has just ceased existence due to a merger. Note that the "Old stock" and "New stock" labels MUST be the same length and must contain a trailing space for the display routines to work correctly. The maximum length of each label is 36 characters. */ pgettext("label", "Old stock: ")); w = getmaxx(curwin); x = (w + width - MAX(width_aa, width_bb)) / 2; rightch(curwin, lines + 3, x, chbuf, 1, &width); leftch(curwin, lines + 3, x, chbuf_bb, 1, &width_bb); right(curwin, lines + 4, x, attr_normal, 0, 0, 1, /* TRANSLATORS: "New stock" refers to the company that has absorbed the other due to a merger. */ pgettext("label", "New stock: ")); leftch(curwin, lines + 4, x, chbuf_aa, 1, &width_aa); mvwhline(curwin, lines + 6, 2, ' ' | attr_subtitle, w - 4); left(curwin, lines + 6, 4, attr_subtitle, 0, 0, 1, /* TRANSLATORS: "Player" is used as a column title in a table containing all player names. */ pgettext("subtitle", "Player")); right(curwin, lines + 6, w - 4, attr_subtitle, 0, 0, 1, /* TRANSLATORS: "Bonus" refers to the bonus cash amount paid to each player after two companies merge. %ls is the currency symbol in the current locale. The maximum column width is 12 characters INCLUDING the currency symbol (see MERGE_BONUS_COLS in src/intf.h). */ pgettext("subtitle", "Bonus (%ls)"), currency_symbol); right(curwin, lines + 6, w - 6 - MERGE_BONUS_COLS, attr_subtitle, 0, 0, 1, /* TRANSLATORS: "Total" refers to the total number of shares in the new company after a merger. The maximum column width is 8 characters (see MERGE_TOTAL_STOCK_COLS in src/intf.h). */ pgettext("subtitle", "Total")); right(curwin, lines + 6, w - 8 - MERGE_BONUS_COLS - MERGE_TOTAL_STOCK_COLS, attr_subtitle, 0, 0, 1, /* TRANSLATORS: "New" refers to how many (new) shares each player receives in the surviving company after a merger. The maximum column width is 8 characters (see MERGE_NEW_STOCK_COLS in src/intf.h). */ pgettext("subtitle", "New")); right(curwin, lines + 6, w - 10 - MERGE_BONUS_COLS - MERGE_TOTAL_STOCK_COLS - MERGE_NEW_STOCK_COLS, attr_subtitle, 0, 0, 1, /* TRANSLATORS: "Old" refers to how many shares each player had in the company ceasing existence. The maximum column width is 8 characters (see MERGE_OLD_STOCK_COLS in src/intf.h). */ pgettext("subtitle", "Old")); total_new = 0; for (ln = lines + 7, i = 0; i < number_players; i++) { if (player[i].in_game) { // Calculate new stock and any bonus old_stock = player[i].stock_owned[bb]; new_stock = (double) old_stock * MERGE_STOCK_RATIO; total_new += new_stock; bonus = (company[bb].stock_issued == 0) ? 0.0 : MERGE_BONUS_RATE * ((double) player[i].stock_owned[bb] / company[bb].stock_issued) * company[bb].share_price; player[i].stock_owned[aa] += new_stock; player[i].stock_owned[bb] = 0; player[i].cash += bonus; mkchstr(chbuf, BUFSIZE, attr_normal, 0, 0, 1, w - 12 - MERGE_BONUS_COLS - MERGE_TOTAL_STOCK_COLS - MERGE_NEW_STOCK_COLS - MERGE_OLD_STOCK_COLS, &width, 1, "%ls", player[i].name); leftch(curwin, ln, 4, chbuf, 1, &width); right(curwin, ln, w - 4, attr_normal, 0, 0, 1, "%!N", bonus); right(curwin, ln, w - 6 - MERGE_BONUS_COLS, attr_normal, 0, 0, 1, "%'ld", player[i].stock_owned[aa]); right(curwin, ln, w - 8 - MERGE_BONUS_COLS - MERGE_TOTAL_STOCK_COLS, attr_normal, 0, 0, 1, "%'ld", new_stock); right(curwin, ln, w - 10 - MERGE_BONUS_COLS - MERGE_TOTAL_STOCK_COLS - MERGE_NEW_STOCK_COLS, attr_normal, 0, 0, 1, "%'ld", old_stock); ln++; } } // Adjust the company records appropriately company[aa].stock_issued += total_new; company[aa].max_stock += total_new; company[aa].share_price += company[bb].share_price * (randf() * (MERGE_PRICE_ADJUST_MAX - MERGE_PRICE_ADJUST_MIN) + MERGE_PRICE_ADJUST_MIN); company[bb].stock_issued = 0; company[bb].max_stock = 0; company[bb].on_map = false; // Adjust the galaxy map appropriately for (x = 0; x < MAX_X; x++) { for (y = 0; y < MAX_Y; y++) { if (galaxy_map[x][y] == b) { galaxy_map[x][y] = a; } } } wait_for_key(curwin, getmaxy(curwin) - 2, attr_waitforkey); deltxwin(); // "Company merger" window txrefresh(); free(chbuf_bb); free(chbuf_aa); free(chbuf); } /***********************************************************************/ // include_outpost: Include any outposts into the company void include_outpost (int num, int x, int y) { map_val_t left, right, up, down; assert(num >= 0 && num < MAX_COMPANIES); assert(x >= 0 && x < MAX_X); assert(y >= 0 && y < MAX_Y); assign_vals(x, y, left, right, up, down); galaxy_map[x][y] = (map_val_t) COMPANY_TO_MAP(num); inc_share_price(num, SHARE_PRICE_INC_OUTPOST); // Outposts next to stars are more valuable: increment again if (left == MAP_STAR) { inc_share_price(num, SHARE_PRICE_INC_OUTSTAR); } if (right == MAP_STAR) { inc_share_price(num, SHARE_PRICE_INC_OUTSTAR); } if (up == MAP_STAR) { inc_share_price(num, SHARE_PRICE_INC_OUTSTAR); } if (down == MAP_STAR) { inc_share_price(num, SHARE_PRICE_INC_OUTSTAR); } // Include any nearby outposts if (left == MAP_OUTPOST) { include_outpost(num, x - 1, y); } if (right == MAP_OUTPOST) { include_outpost(num, x + 1, y); } if (up == MAP_OUTPOST) { include_outpost(num, x, y - 1); } if (down == MAP_OUTPOST) { include_outpost(num, x, y + 1); } } /***********************************************************************/ // inc_share_price: Increase the share price of a company void inc_share_price (int num, double inc) { assert(num >= 0 && num < MAX_COMPANIES); company[num].share_price += inc * (randf() * (PRICE_INC_ADJUST_MAX - PRICE_INC_ADJUST_MIN) + PRICE_INC_ADJUST_MIN); company[num].max_stock += inc * (randf() * (MAX_STOCK_RATIO_MAX - MAX_STOCK_RATIO_MIN) + MAX_STOCK_RATIO_MIN); if (randf() < CHANGE_RETURN_GROWING) { double change = randf() * GROWING_MAX_CHANGE; if (randf() < DEC_RETURN_GROWING) { change = -change; } company[num].share_return += change; if ( company[num].share_return > MAX_COMPANY_RETURN || company[num].share_return < MIN_COMPANY_RETURN) { company[num].share_return -= 2.0 * change; } } } /***********************************************************************/ // adjust_values: Adjust various company-related values void adjust_values (void) { int which; // Declare a company bankrupt! if (randf() > (1.0 - COMPANY_BANKRUPTCY)) { which = randi(MAX_COMPANIES); if (company[which].on_map && company[which].share_return <= 0.0) { if (randf() < ALL_ASSETS_TAKEN) { txdlgbox(MAX_DLG_LINES, 60, 6, WCENTER, attr_error_window, attr_error_title, attr_error_highlight, attr_error_normal, 0, attr_error_waitforkey, _(" Bankruptcy Court "), /* TRANSLATORS: %ls represents the company name. */ _("%ls has been declared bankrupt " "by the Interstellar Trading Bank.\n\n" "^{All assets have been taken " "to repay outstanding loans.^}"), company[which].name); txrefresh(); } else { double rate = randf(); chtype *chbuf = xmalloc(BUFSIZE * sizeof(chtype)); chtype *chbuf_amt; int w, x, lines, width, width_amt, widthbuf[6]; for (int i = 0; i < number_players; i++) { if (player[i].in_game) { player[i].cash += player[i].stock_owned[which] * company[which].share_price * rate; } } lines = mkchstr(chbuf, BUFSIZE, attr_error_highlight, attr_error_normal, 0, 6, 60 - 4, widthbuf, 6, /* TRANSLATORS: %ls represents the company name. */ _("%ls has been declared bankrupt by the " "Interstellar Trading Bank.\n\n" "^{The Bank has agreed to pay stock holders ^}" "%.2f%%^{ of the share value on each share " "owned.^}"), company[which].name, rate * 100.0); newtxwin(9 + lines, 60, 4, WCENTER, true, attr_error_window); w = getmaxx(curwin); center(curwin, 1, 0, attr_error_title, 0, 0, 1, _(" Bankruptcy Court ")); centerch(curwin, 3, 0, chbuf, lines, widthbuf); mkchstr(chbuf, BUFSIZE, attr_error_highlight, 0, 0, 1, w / 2, &width_amt, 1, "%N", company[which].share_price); chbuf_amt = xchstrdup(chbuf); mkchstr(chbuf, BUFSIZE, attr_error_normal, 0, 0, 1, w / 2, &width, 1, /* TRANSLATORS: The label "Amount paid per share" refers to payment made by the Interstellar Trading Bank to each player upon company bankruptcy. This label MUST be the same length as "Old share value" and MUST have at least one trailing space for the display routines to work correctly. The maximum length is 28 characters. */ pgettext("label", "Amount paid per share: ")); x = (w + width - width_amt) / 2; right(curwin, lines + 4, x, attr_error_normal, 0, 0, 1, /* TRANSLATORS: "Old share value" refers to the share price of a company before it was forced into bankruptcy by the Bank. This label must be the same width as "Amount paid per share". */ pgettext("label", "Old share value: ")); leftch(curwin, lines + 4, x, chbuf_amt, 1, &width_amt); rightch(curwin, lines + 5, x, chbuf, 1, &width); left(curwin, lines + 5, x, attr_error_highlight, 0, 0, 1, "%N", company[which].share_price * rate); wait_for_key(curwin, getmaxy(curwin) - 2, attr_error_waitforkey); deltxwin(); txrefresh(); free(chbuf_amt); free(chbuf); } for (int i = 0; i < number_players; i++) { player[i].stock_owned[which] = 0; } company[which].share_price = 0.0; company[which].share_return = 0.0; company[which].stock_issued = 0; company[which].max_stock = 0; company[which].on_map = false; for (int x = 0; x < MAX_X; x++) { for (int y = 0; y < MAX_Y; y++) { if (galaxy_map[x][y] == COMPANY_TO_MAP((unsigned int) which)) { galaxy_map[x][y] = MAP_EMPTY; } } } } } // Increase or decrease company return if (randf() < CHANGE_COMPANY_RETURN) { which = randi(MAX_COMPANIES); if (company[which].on_map) { double change = randf() * RETURN_MAX_CHANGE; if (randf() < DEC_COMPANY_RETURN) { change = -change; } company[which].share_return += change; if ( company[which].share_return > MAX_COMPANY_RETURN || company[which].share_return < MIN_COMPANY_RETURN) { company[which].share_return -= 2.0 * change; } } } // Increase or decrease share price if (randf() < CHANGE_SHARE_PRICE) { which = randi(MAX_COMPANIES); if (company[which].on_map) { double change = randf() * company[which].share_price * PRICE_CHANGE_RATE; if (randf() < DEC_SHARE_PRICE) { change = -change; } company[which].share_price += change; } } // Give the current player the companies' dividends for (int i = 0; i < MAX_COMPANIES; i++) { if (company[i].on_map && company[i].stock_issued != 0) { player[current_player].cash += player[current_player].stock_owned[i] * company[i].share_price * company[i].share_return + ((double) player[current_player].stock_owned[i] / company[i].stock_issued) * company[i].share_price * OWNERSHIP_BONUS; } } // Has the player lost money due to negative share returns? if (player[current_player].cash < 0.0) { double borrowed = -player[current_player].cash; txdlgbox(MAX_DLG_LINES, 60, 7, WCENTER, attr_error_window, attr_error_title, attr_error_highlight, 0, 0, attr_error_waitforkey, _(" Interstellar Trading Bank "), /* xgettext:c-format */ _("You were forced to borrow %N\n" "to cover losses from company shares."), borrowed); txrefresh(); player[current_player].cash = 0.0; player[current_player].debt += borrowed; } // Change the interest rate if (randf() < CHANGE_INTEREST_RATE) { double change = randf() * INTEREST_MAX_CHANGE; if (randf() < DEC_INTEREST_RATE) { change = -change; } interest_rate += change; if ( interest_rate > MAX_INTEREST_RATE || interest_rate < MIN_INTEREST_RATE) { interest_rate -= 2.0 * change; } } // Calculate current player's debt player[current_player].debt *= interest_rate + 1.0; // Check if a player's debt is too large if (total_value(current_player) <= -MAX_OVERDRAFT) { double impounded = MIN(player[current_player].cash, player[current_player].debt); txdlgbox(MAX_DLG_LINES, 60, 7, WCENTER, attr_error_window, attr_error_title, attr_error_highlight, attr_error_normal, 0, attr_error_waitforkey, _(" Interstellar Trading Bank "), /* xgettext:c-format */ _("Your debt has amounted to %N!\n" "^{The Bank has impounded ^}%N^{ from your cash.^}"), player[current_player].debt, impounded); txrefresh(); player[current_player].cash -= impounded; player[current_player].debt -= impounded; if (player[current_player].cash < ROUNDING_AMOUNT) { player[current_player].cash = 0.0; } if (player[current_player].debt < ROUNDING_AMOUNT) { player[current_player].debt = 0.0; } // Shall we declare them bankrupt? if (total_value(current_player) <= 0.0 && randf() < MAKE_BANKRUPT) { bankrupt_player(true); } } } /***********************************************************************/ // cmp_game_move: Compare two game_move[] elements for sorting int cmp_game_move (const void *a, const void *b) { const move_rec_t *aa = (const move_rec_t *) a; const move_rec_t *bb = (const move_rec_t *) b; if (aa->x < bb->x) { return -1; } else if (aa->x > bb->x) { return 1; } else { if (aa->y < bb->y) { return -1; } else if (aa->y > bb->y) { return 1; } else { return 0; } } } /***********************************************************************/ // End of file