diff --git a/Makefile b/Makefile index 8158eb7..8c3dbc0 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ LIBS = -lncurses # You shouldn't have to modify anything below this line. # There's a dynamic format in the object-display routines; suppress the warning -CFLAGS = $(DEBUG) $(PROFILE) -Wall -Wno-format-security +CFLAGS = $(DEBUG) $(PROFILE) -Wall -Wno-format-security -fcommon FILES = \ attack.c \ @@ -111,7 +111,7 @@ uninstall: clean: rm -f *.o TAGS vms-empire - rm -f *.6 *.html + rm -f *.html clobber: clean rm -f vms-empire vms-empire-*.tar* diff --git a/README b/README deleted file mode 100644 index b631e3f..0000000 --- a/README +++ /dev/null @@ -1,19 +0,0 @@ - README for vms-empire - -VMS-Empire is a simulation of a full-scale war between two -emperors, the computer and you. Naturally, there is only -room for one, so the object of the game is to destroy -the other. The computer plays by the same rules that you -do. This game is the ancestor of all the multiplayer 4X -simulations out there, including Civilization and Master of Orion. - -This code originated under VMS but has been forward-ported to -modern ANSI C. It is known to run not only on Linux but -also under Windows and Mac OS X; all you need is a local -port of the curses library. - -The files HACKING and BUGS will be of interest if you intend -to modify this code. - -The ancestral code was by Chuck Simmons . Since -around 1990 it has been maintained by Eric S. Raymond diff --git a/README.md b/README.md new file mode 100644 index 0000000..8184379 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# VMS Empire + +VMS Empire is a classic turn-based strategy game played in a text terminal. Units, cities, land, and water are represented using letters and ASCII characters. + +This fork includes small compatibility fixes that allow the game to compile cleanly on modern Linux systems, including Raspberry Pi OS. + +## Changes in this fork + +* Added the GCC `-fcommon` compiler option to fix multiple-definition linker errors on modern GCC versions. +* Corrected two map-array loops in `game.c`. +* Changed: + +In line 126 +```c +i <= MAP_SIZE +``` + +to: + +```c +i < MAP_SIZE +``` + +This prevents the loops from accessing one element beyond the end of the arrays. + +## Requirements + +Install the compiler, development tools, Git, and ncurses library: + +```bash +sudo apt update +sudo apt install -y git build-essential libncurses-dev +``` + +## Download + +Clone this fork: + +```bash +git clone https://github.com/dgdimick/empire.git +cd empire +``` + +## Build + +Compile the game: + +```bash +make +``` + +The executable will be created as: + +```text +vms-empire +``` + +## Run + +Start the game with: + +```bash +./vms-empire +``` + +## Clean and rebuild + +To remove compiled files and rebuild from scratch: + +```bash +make clean +make +``` + +## Repository remotes + +This fork is maintained at: + +```text +https://github.com/dgdimick/empire +``` + +The original repository is: + +```text +https://github.com/slacy/empire +``` + +## Platform tested + +The changes in this fork were tested on Raspberry Pi OS using a Raspberry Pi. + +## License + +This fork retains the original project’s licensing and copyright terms. See the repository files for the original license information. + + + +AI vs AI spectator mode +----------------------- + +This fork adds an unattended spectator mode where the existing computer +player controls both empires. Start it with: + + ./vms-empire -a + +Use the existing -d option to set the delay in milliseconds between map +updates. For example: + + ./vms-empire -a -d 500 + +The full battlefield is shown using the condensed map display. The USER-side AI +is shown in blue and the COMP-side AI is shown in red on color terminals. Press Ctrl-C +to stop an active game. At the end of a game, press any key to exit. + + +AI spectator views +------------------ +Start an AI-versus-AI match in one of four views: + +```bash + ./vms-empire -a -v blue + ./vms-empire -a -v red + ./vms-empire -a -v shared + ./vms-empire -a -v full + +If -v is omitted, shared fog-of-war is used. During a match, press B, R, +S, or F to switch immediately between Blue, Red, Shared, and Full views. +These display modes do not alter either AI's private fog-of-war map. diff --git a/compmove.c b/compmove.c index 1f24d88..83cd89a 100644 --- a/compmove.c +++ b/compmove.c @@ -1061,7 +1061,8 @@ void check_endgame(void) { for (p = comp_obj[ARMY]; p != NULL; p = p->piece_link.next) ncomp_army++; - if (ncomp_city < nuser_city / 3 && ncomp_army < nuser_army / 3) { + if (!ai_vs_ai && ncomp_city < nuser_city / 3 && + ncomp_army < nuser_army / 3) { clear_screen(); prompt("The computer acknowledges defeat. Do"); ksend("The computer acknowledges defeat."); diff --git a/display.c b/display.c index a3a6ea1..2110eed 100644 --- a/display.c +++ b/display.c @@ -430,6 +430,9 @@ void print_zoom_cell(view_map_t *vmap, int row, int col, int row_inc, int col_inc) { int r, c; char cell; +#ifdef A_COLOR + chtype attr = COLOR_PAIR(COLOR_WHITE); +#endif cell = ' '; for (r = row; r < row + row_inc; r++) @@ -438,8 +441,28 @@ void print_zoom_cell(view_map_t *vmap, int row, int col, int row_inc, strchr(zoom_list, cell)) cell = vmap[row_col_loc(r, c)].contents; +#ifdef A_COLOR + if (ai_vs_ai) { + /* In spectator mode, USER is the Blue AI and COMP is the Red AI. */ + if (cell == 'O' || strchr("TCBSDPFAZ", cell) != NULL) + attr = COLOR_PAIR(COLOR_BLUE) | A_BOLD; + else if (cell == 'X' || strchr("tcbsdpafz", cell) != NULL) + attr = COLOR_PAIR(COLOR_RED) | A_BOLD; + else if (cell == MAP_LAND || cell == '+') + attr = COLOR_PAIR(COLOR_GREEN); + else if (cell == MAP_SEA || cell == '.') + attr = COLOR_PAIR(COLOR_CYAN); + } + attron(attr); +#endif + (void)move(row / row_inc + NUMTOPS, col / col_inc); (void)addch((chtype)cell); + +#ifdef A_COLOR + attroff(attr); + attron(COLOR_PAIR(COLOR_WHITE)); +#endif } /* diff --git a/empire.c b/empire.c index 29d6f97..79386f7 100644 --- a/empire.c +++ b/empire.c @@ -11,10 +11,20 @@ parser, and the simple commands. */ #include "empire.h" +#include +#include #include +#include #include "extern.h" void c_examine(void), c_movie(void); +static void ai_vs_ai_loop(void); +static void swap_sides(void); +static void build_spectator_map(void); +static void ai_spectator_delay(void); +static const char *spectator_view_name(void); +static void flip_view_symbols(view_map_t vmap[]); +static view_map_t spectator_map[MAP_SIZE]; /* * 03a 01Apr88 aml .Hacked movement algorithms for computer. @@ -35,13 +45,16 @@ void empire(void) { rndini(); /* init random number generator */ clear_screen(); /* nothing on screen */ - pos_str(7, 0, "EMPIRE, Version 5.00 site Amdahl 1-Apr-1988"); - pos_str(8, 0, "Detailed directions are in EMPIRE.DOC\n"); + pos_str(6, 0, "EMPIRE, Version 5.00 site Amdahl 1-Apr-1988"); + pos_str(7, 0, "AI Spectator Fork 1.0 - Denis Dimick, 2026"); + pos_str(8, 0, "Directions and build information are in README.md\n"); (void)redisplay(); if (!restore_game()) /* try to restore previous game */ init_game(); /* otherwise init a new game */ + if (ai_vs_ai) ai_vs_ai_loop(); + /* Command loop starts here. */ for (;;) { /* until user quits */ @@ -345,3 +358,183 @@ void c_movie(void) { } /* end */ + + +/* + * Run both empires with the existing computer player. The computer + * routines are written specifically for the COMP side, so the second + * half-turn is performed by swapping the two sides, calling comp_move(), + * and then swapping them back. + */ +static void ai_vs_ai_loop(void) { + for (;;) { + build_spectator_map(); + print_zoom(spectator_map); + topmsg(1, "AI vs AI - round %ld - %s view", date, spectator_view_name()); + topmsg(2, "B Blue R Red S Shared F Full Ctrl-C stops"); + ai_spectator_delay(); + + comp_move(1); + if (win != no_win) break; + + swap_sides(); + comp_move(1); + swap_sides(); + if (win != no_win) break; + + save_game(); + } + + /* Save the final position before offering to keep or discard the + AI-match files. */ + save_game(); + + build_spectator_map(); + print_zoom(spectator_map); + topmsg(1, "AI vs AI game finished at round %ld.", date); + topmsg(2, "Y keeps both files; N deletes both files."); + + if (!getyn("Keep empsave.dat and info_list.txt? (Y/N) ")) { + if (remove(savefile) != 0) + error("Could not delete %s.", savefile); + if (remove("info_list.txt") != 0) + error("Could not delete info_list.txt."); + } + + empend(); +} + +static void swap_sides(void) { + static view_map_t temp_map[MAP_SIZE]; + piece_info_t *temp_obj; + int temp_score; + int i; + + (void)memcpy(temp_map, user_map, sizeof(temp_map)); + (void)memcpy(user_map, comp_map, sizeof(temp_map)); + (void)memcpy(comp_map, temp_map, sizeof(temp_map)); + + /* Each view map encodes USER pieces/cities as uppercase/O and COMP + pieces/cities as lowercase/X. Since ownership is about to be + reversed, reverse those symbols too or the AI will mistake enemy + cities for its own. */ + flip_view_symbols(user_map); + flip_view_symbols(comp_map); + + for (i = 0; i < NUM_OBJECTS; i++) { + temp_obj = user_obj[i]; + user_obj[i] = comp_obj[i]; + comp_obj[i] = temp_obj; + } + + for (i = 0; i < LIST_SIZE; i++) { + if (object[i].owner == USER) + object[i].owner = COMP; + else if (object[i].owner == COMP) + object[i].owner = USER; + } + + for (i = 0; i < NUM_CITY; i++) { + if (city[i].owner == USER) + city[i].owner = COMP; + else if (city[i].owner == COMP) + city[i].owner = USER; + } + + temp_score = user_score; + user_score = comp_score; + comp_score = temp_score; +} + + +static void flip_view_symbols(view_map_t vmap[]) { + int i; + char c; + + for (i = 0; i < MAP_SIZE; i++) { + c = vmap[i].contents; + if (c == 'O') + vmap[i].contents = 'X'; + else if (c == 'X') + vmap[i].contents = 'O'; + else { + switch (c) { + case 'A': case 'F': case 'P': case 'S': case 'D': + case 'T': case 'C': case 'B': case 'Z': + vmap[i].contents = c - 'A' + 'a'; + break; + case 'a': case 'f': case 'p': case 's': case 'd': + case 't': case 'c': case 'b': case 'z': + vmap[i].contents = c - 'a' + 'A'; + break; + } + } + } +} + +static const char *spectator_view_name(void) { + switch (spectator_view) { + case VIEW_BLUE: return "Blue fog"; + case VIEW_RED: return "Red fog"; + case VIEW_FULL: return "Full"; + default: return "Shared fog"; + } +} + +static void build_spectator_map(void) { + int i; + + for (i = 0; i < MAP_SIZE; i++) { + spectator_map[i].contents = ' '; + spectator_map[i].seen = false; + + if (spectator_view == VIEW_BLUE) { + spectator_map[i] = user_map[i]; + } + else if (spectator_view == VIEW_RED) { + spectator_map[i] = comp_map[i]; + } + else if (spectator_view == VIEW_FULL) { + if (map[i].on_board) update(spectator_map, i); + } + else { /* shared fog: show anything discovered by either AI */ + if (user_map[i].seen || comp_map[i].seen) { + spectator_map[i].seen = true; + if (map[i].on_board) + update(spectator_map, i); + else if (user_map[i].seen) + spectator_map[i].contents = user_map[i].contents; + else + spectator_map[i].contents = comp_map[i].contents; + } + } + } +} + +static void ai_spectator_delay(void) { + int remaining = delay_time; + int c; + int slice; + + (void)refresh(); + (void)nodelay(stdscr, TRUE); + + do { + c = getch(); + if (c != ERR) { + switch (toupper(c)) { + case 'B': spectator_view = VIEW_BLUE; break; + case 'R': spectator_view = VIEW_RED; break; + case 'S': spectator_view = VIEW_SHARED; break; + case 'F': spectator_view = VIEW_FULL; break; + } + } + + if (remaining <= 0) break; + slice = (remaining > 50) ? 50 : remaining; + (void)napms(slice); + remaining -= slice; + } while (remaining > 0); + + (void)nodelay(stdscr, FALSE); +} \ No newline at end of file diff --git a/empire.h b/empire.h index 5c22a60..b3b719b 100644 --- a/empire.h +++ b/empire.h @@ -218,6 +218,13 @@ typedef struct real_map { /* a cell of the actual map */ piece_info_t *objp; /* list of objects at this location */ } real_map_t; + +/* AI-versus-AI spectator view modes. */ +#define VIEW_BLUE 0 +#define VIEW_RED 1 +#define VIEW_SHARED 2 +#define VIEW_FULL 3 + typedef struct view_map { /* a cell of one player's world view */ char contents; /* MAP_LAND, MAP_SEA, MAP_CITY, 'A', 'a', etc */ long seen; /* date when last updated */ diff --git a/extern.h b/extern.h index cdf5d41..1b9c1ed 100644 --- a/extern.h +++ b/extern.h @@ -72,6 +72,8 @@ extern int user_lines; /* miscellaneous */ long date; /* number of game turns played */ bool automove; /* true iff user is in automove mode */ +bool ai_vs_ai; /* true iff both sides are computer controlled */ +int spectator_view; /* AI spectator view: blue, red, shared, or full */ bool resigned; /* true iff computer resigned */ bool debug; /* true iff in debugging mode */ bool print_debug; /* true iff we print debugging stuff */ @@ -106,6 +108,7 @@ char *savefile; /* global routines */ void empire(void); +void empend(void); void attack(piece_info_t *att_obj, long loc); void comp_move(int nmoves); @@ -208,6 +211,7 @@ void embark(piece_info_t *ship, piece_info_t *obj); void disembark(piece_info_t *obj); void describe_obj(piece_info_t *obj); void scan(view_map_t vmap[], long loc); +void update(view_map_t vmap[], long loc); void scan_sat(view_map_t *vmap, long loc); void set_prod(city_info_t *cityp); diff --git a/game.c b/game.c index dd3d3eb..cecceb1 100644 --- a/game.c +++ b/game.c @@ -123,7 +123,7 @@ void make_map(void) { /* count the number of cells at each height */ for (i = 0; i <= MAX_HEIGHT; i++) height_count[i] = 0; - for (i = 0; i <= MAP_SIZE; i++) height_count[height[from][i]]++; + for (i = 0; i < MAP_SIZE; i++) height_count[height[from][i]]++; /* find the water line */ loc = MAX_HEIGHT; /* default to all water */ @@ -307,7 +307,10 @@ bool select_cities(void) { "Choose a difficulty level where 0 is easy and %d is hard: ", ncont * ncont - 1); - pair = get_range(jnkbuf, 0, ncont * ncont - 1); + if (ai_vs_ai) + pair = (ncont * ncont - 1) / 2; + else + pair = get_range(jnkbuf, 0, ncont * ncont - 1); comp_cont = pair_tab[pair].comp_cont; user_cont = pair_tab[pair].user_cont; @@ -319,8 +322,10 @@ bool select_cities(void) { userp = cont_tab[user_cont].cityp[useri]; } while (userp == compp); - topmsg(1, "Your city is at %d.", loc_disp(userp->loc)); - delay(); /* let user see output before we set_prod */ + if (!ai_vs_ai) { + topmsg(1, "Your city is at %d.", loc_disp(userp->loc)); + delay(); /* let user see output before we set_prod */ + } /* update city and map */ compp->owner = COMP; @@ -330,8 +335,9 @@ bool select_cities(void) { userp->owner = USER; userp->work = 0; + userp->prod = ARMY; scan(user_map, userp->loc); - set_prod(userp); + if (!ai_vs_ai) set_prod(userp); return (true); } @@ -541,7 +547,7 @@ int restore_game(void) { f = fopen(savefile, "r"); /* open for input */ if (f == NULL) { - perror("Cannot open saved game"); + topmsg(3, "No saved game found. Starting a new game."); return (false); } rbuf(map); diff --git a/main.c b/main.c index 6786995..be9b56e 100644 --- a/main.c +++ b/main.c @@ -25,11 +25,70 @@ main.c -- parse command line for empire #include #include +#include #include #include "empire.h" #include "extern.h" -#define OPTFLAGS "w:s:d:S:f:" +#define OPTFLAGS "av:w:s:d:S:f:" + +static void startup_menu(int *delay_ms) { + char input[64]; + int choice; + + (void)printf("VMS Empire 5.00 - AI Spectator Fork 1.0\n\n"); + (void)printf("1. Normal game\n"); + (void)printf("2. AI vs AI - Shared view\n"); + (void)printf("3. AI vs AI - Blue view\n"); + (void)printf("4. AI vs AI - Red view\n"); + (void)printf("5. AI vs AI - Full view\n"); + (void)printf("6. Quit\n\n"); + (void)printf("Select mode [1]: "); + (void)fflush(stdout); + + if (fgets(input, sizeof(input), stdin) == NULL || input[0] == '\n') + choice = 1; + else + choice = atoi(input); + + switch (choice) { + case 1: + ai_vs_ai = false; + return; + case 2: + ai_vs_ai = true; + spectator_view = VIEW_SHARED; + break; + case 3: + ai_vs_ai = true; + spectator_view = VIEW_BLUE; + break; + case 4: + ai_vs_ai = true; + spectator_view = VIEW_RED; + break; + case 5: + ai_vs_ai = true; + spectator_view = VIEW_FULL; + break; + case 6: + exit(0); + default: + (void)printf("Invalid selection. Starting normal game.\n"); + ai_vs_ai = false; + return; + } + + (void)printf("Delay between updates in milliseconds [500]: "); + (void)fflush(stdout); + if (fgets(input, sizeof(input), stdin) != NULL && input[0] != '\n') { + int entered_delay = atoi(input); + if (entered_delay >= 0 && entered_delay <= 30000) + *delay_ms = entered_delay; + else + (void)printf("Invalid delay. Using 500 milliseconds.\n"); + } +} int main(argc, argv) int argc; char *argv[]; @@ -46,6 +105,13 @@ char *argv[]; dflg = 2000; Sflg = 10; savefile = "empsave.dat"; + ai_vs_ai = false; + spectator_view = VIEW_SHARED; + + if (argc == 1) { + dflg = 500; + startup_menu(&dflg); + } /* * extract command line options @@ -53,6 +119,23 @@ char *argv[]; while ((c = getopt(argc, argv, OPTFLAGS)) != EOF) { switch (c) { + case 'a': + ai_vs_ai = true; + break; + case 'v': + if (strcmp(optarg, "blue") == 0) + spectator_view = VIEW_BLUE; + else if (strcmp(optarg, "red") == 0) + spectator_view = VIEW_RED; + else if (strcmp(optarg, "shared") == 0) + spectator_view = VIEW_SHARED; + else if (strcmp(optarg, "full") == 0) + spectator_view = VIEW_FULL; + else { + (void)printf("empire: -v must be blue, red, shared, or full.\n"); + exit(1); + } + break; case 'w': wflg = atoi(optarg); break; @@ -74,7 +157,7 @@ char *argv[]; } } if (errflg || (argc - optind) != 0) { - (void)printf("empire: usage: empire [-w water] [-s smooth] [-d delay]\n"); + (void)printf("empire: usage: empire [-a] [-v blue|red|shared|full] [-w water] [-s smooth] [-d delay]\n"); exit(1); }