hypercurse of hyperwar
11. 8. 2021 #kód
run it via dub:
dub run -b release --single hcohw.d
or compile
dub build -b release --single hcohw.d
controls:
- space - place flag (people migrate towards it)
- b - build a house (generates people, costs 100 gold from mines
$$
) - y - show paths to flags
- +/- - change speed
- p - pause
/+ dub.sdl: name "hypercurse" license "GPL3" dependency "ncurses" version="~>0.0.149" +/ import core.stdc.stdio; import core.sys.posix.fcntl; import deimos.ncurses; import std.algorithm; import std.format; import std.random; import std.range; import std.conv; import std.math; import std.typecons; enum playerCount = 5; struct Pos { int x; int y; Pos opBinary(string op)(Pos p) { return mixin("Pos(x "~op~" p.x, y "~op~" p.y)"); } Pos signum() { return Pos( (x > 0) ? 1 : ((x < 0) ? -1 : 0), (y > 0) ? 1 : ((y < 0) ? -1 : 0) ); } float distSq(Pos p) { return (float(x) - p.x)^^2 + (float(y) - p.y)^^2; } float dist(Pos p) { return sqrt(distSq(p)); } } enum Land { Flat, Mountain, Mine, Water } struct Tile { int x, y; Land land = Land.Flat; int cityLevel = 0; int[playerCount] pops = 0; bool[playerCount] flags = false; int mineOwner = -1; // updated every tick Pos targetFlagPos; // debug bool isHabitable() { return land == Land.Flat; } int owner() { return cast(int) pops[].maxIndex; } bool isPopulated() { return pops[].any!"a > 0"; } } struct Player { int gold = 100; int pop = 0; } struct Board { int w, h; Tile[] tiles; Player[playerCount] players; ulong tick = 0; enum playerIdx = 0; enum maxCityLevel = 3; enum maxPop = 999; enum buildingCost = 100; this(int w, int h) { this.w = w; this.h = h; tiles = new Tile[w * h]; foreach (x; 0 .. w) { foreach (y; 0 .. h) { opIndex(x, y).x = x; opIndex(x, y).y = y; } } } Tile* opIndex(int x, int y) { assert(isValid(Pos(x, y))); return &tiles[y * w + x]; } Tile* opIndex(Pos pos) { return opIndex(pos.x, pos.y); } int area() { return w * h; } bool isValid(Pos p) { return p.x >= 0 && p.x < w && p.y >= 0 && p.y < h; } private immutable _neighbors = [Pos(1, 0), Pos(-1, 0), Pos(0, 1), Pos(0, -1)]; auto neighbors(int x, int y) { return _neighbors.map!(p => Pos(x+p.x, y+p.y)) .filter!(p => p.x >= 0 && p.y >= 0 && p.x < w && p.y < h && this[p].isHabitable) .map!(p => this[p]); } auto neighbors(Pos pos) { return neighbors(pos.x, pos.y); } Pos[] _positions; auto positions() { if (_positions.length == 0) { _positions = iota(0, h).map!(y => iota(0, w).map!(x => Pos(x, y))).joiner.array; } return _positions; } auto iterateTiles() { return positions.map!(p => this[p]); } auto build(Pos pos, int pl) { Player* player = &players[pl]; auto tile = this[pos]; if (player.gold < buildingCost || !tile.isHabitable || tile.cityLevel >= maxCityLevel) return; tile.cityLevel++; player.gold -= buildingCost; } } extern(C) void usleep(int); void _addstr(int x, int y, string str) { mvaddnstr(x, y, str.ptr, cast(int) str.length); } void _addstr(string str) { addnstr(str.ptr, cast(int) str.length); } struct UI { Pos selected = Pos(0, 0); bool paused = false; int speed = 1; bool showRoutes = false; enum playerColors = [4, 3, 7, 8, 5]; enum neutralColor = 6; } void main() { { import core.stdc.locale; setlocale(LC_ALL, ""); } initscr(); scope (exit) endwin(); initscr(); /* initialize the library and screen */ cbreak(); /* put terminal into non-blocking input mode */ noecho(); /* turn off echo */ start_color(); clear(); /* clear the screen */ curs_set(0); /* hide the cursor */ refresh(); use_default_colors(); init_pair(0, COLOR_WHITE, COLOR_BLACK); init_pair(1, COLOR_WHITE, COLOR_BLACK); init_pair(2, COLOR_BLACK, COLOR_BLACK); init_pair(3, COLOR_RED, COLOR_BLACK); init_pair(4, COLOR_GREEN, COLOR_BLACK); init_pair(5, COLOR_BLUE, COLOR_BLACK); init_pair(6, COLOR_YELLOW, COLOR_BLACK); init_pair(7, COLOR_MAGENTA, COLOR_BLACK); init_pair(8, COLOR_CYAN, COLOR_BLACK); attrset(A_BOLD | COLOR_PAIR(0)); int fd_flags = fcntl(0, F_GETFL); fcntl(0, F_SETFL, (fd_flags|O_NONBLOCK)); // create world Board board = Board(30, 20); alias randPos = () => Pos(uniform(0, board.w), uniform(0, board.h)); foreach (i; 0 .. board.area/12) { board[randPos()].land = Land.Mountain; } foreach (i; 0 .. board.area/20) { board[randPos()].land = Land.Water; } foreach (i; 0 .. board.area/12) { board[randPos()].land = Land.Mine; } foreach (i; 0 .. board.area/50) { auto tile = board[randPos()]; if (tile.land == Land.Flat) { tile.cityLevel = 1; } } { auto w = board.w; auto h = board.h; foreach (pl, tile; [board[1, 1], board[w-2, 1], board[1, h-2], board[w-2, h-2], board[w/2+1, h/2+1]]) { tile.pops[pl] = 100; tile.cityLevel = 3; tile.land = Land.Flat; } } Paths paths = floydWarshall(board); int tickSleep = 20000; UI ui; // main loop while (true) { // keyboard actions char c = '\0'; while (fread(&c, 1, 1, stdin) == 1) { enum KeyUp = 65; enum KeyDown = 66; enum KeyRight = 67; enum KeyLeft = 68; switch (c) { case KeyUp: ui.selected.y--; break; case KeyDown: ui.selected.y++; break; case KeyLeft: ui.selected.x--; break; case KeyRight: ui.selected.x++; break; case ' ': if (board[ui.selected].isHabitable) { board[ui.selected].flags[board.playerIdx] ^= true; } break; case 'r': // TODO remove all flags break; case '+': ui.speed *= 2; break; case '-': ui.speed = max(1, ui.speed / 2); break; case 'p': ui.paused = !ui.paused; break; case 'b': board.build(ui.selected, board.playerIdx); break; case 'q': return; case 'y': ui.showRoutes = !ui.showRoutes; break; default: } ui.selected.x = clamp(ui.selected.x, 0, board.w-1); ui.selected.y = clamp(ui.selected.y, 0, board.h-1); } // update world bool lost, won; if (!ui.paused) { foreach (_; 0 .. ui.speed) { board.tick++; // population growth foreach (tile; board.iterateTiles) { if (tile.cityLevel > 0 && tile.isPopulated && board.tick % 5 == 0) { auto p = &tile.pops[tile.owner]; *p = clamp(*p + tile.cityLevel, 0, board.maxPop); } } auto flagPositions = iota(0, playerCount).map!(pl => board.positions.filter!(pos => board[pos].flags[pl]).array ).array; auto move(Tile* from, Tile* to, int player, int n) { n = min(n, from.pops[player]); n = min(n, board.maxPop - to.pops[player]); from.pops[player] -= n; to.pops[player] += n; } // migration foreach (pos; board.positions) { auto tile = board[pos]; foreach (pl; 0 .. playerCount) { foreach (neighbor; board.neighbors(pos)) { auto delta = tile.pops[pl] - neighbor.pops[pl]; if (delta > 0) { move(tile, neighbor, pl, delta / 10); } } // move in direction of the closest flag if (flagPositions[pl].length > 0) { auto flpos = flagPositions[pl].minElement!(p => p.distSq(pos)); auto dist = pos.dist(flpos); auto dest = paths.direction(pos, flpos); if (board.isValid(dest) && board[dest].isHabitable && uniform01 < 0.1) { move(tile, board[dest], pl, tile.pops[pl] / 12); } if (pl == 0) { board[pos].targetFlagPos = flpos; } } } } // fighting foreach (pos; board.positions) { auto tile = board[pos]; auto sides = tile.pops[].count!"a > 0"; if (sides > 1) { foreach (attacker; 0 .. playerCount) { if (tile.pops[attacker] <= 0) continue; // nobody to do attacking auto defender = uniform(0, 4); if (defender == attacker || tile.pops[defender] <= 0) continue; // nobody to attack // fight! auto n = min(tile.pops[attacker], tile.pops[defender]); tile.pops[attacker] -= max(1, n / 10); tile.pops[defender] -= max(1, n / 10); } } } // mine ownership foreach (pos; board.positions) { auto tile = board[pos]; if (tile.land == Land.Mine) { int[4] pops; foreach (neighbor; board.neighbors(pos)) { pops[] += neighbor.pops[]; } if (pops[].sum == 0) { tile.mineOwner = -1; } else { tile.mineOwner = cast(int) pops[].maxIndex; if (board.tick % 50 == 0) { board.players[tile.mineOwner].gold++; } } } } // AI actions, building, placing flags foreach (plIdx; 0 .. playerCount) { if (plIdx == board.playerIdx) continue; auto player = &board.players[plIdx]; // build randomly if (player.gold > board.buildingCost) { auto pos = randPos(); if (board[pos].pops[plIdx] > 0) { board.build(pos, plIdx); } } if (plIdx == 1) { board[0, 0].flags[plIdx] = true; } } // update player populations { int[playerCount] pops; foreach (pos; board.positions) { pops[] += board[pos].pops[]; } foreach (pl, ref p; board.players) { p.pop = pops[pl]; } } auto lastPlayerAlive = board.players[].count!"a.pop > 0" == 1; if (lastPlayerAlive) { if (board.players[board.playerIdx].pop == 0) { lost = true; } else { won = true; } } }} draw(board, ui, paths); usleep(tickSleep); } } void draw(ref Board board, ref UI ui, ref Paths paths) { // draw world immutable dots = [" ", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"]; immutable cities = [" ", "☖", "☗", "♜"]; immutable mountain = "⛰"; immutable flag = "🏳"; foreach (y; 0 .. board.h) { foreach (x; 0 .. board.w) { alias str = (row, col, str) => _addstr(y*2 + row, x*4 + col, str); attrset(A_NORMAL | COLOR_PAIR(1)); str(0, 0, " "); str(1, 0, " "); auto tile = board[x, y]; if (tile.land == Land.Mountain) { str(0, 0, mountain~mountain); str(1, 1, mountain~mountain); } else if (tile.land == Land.Mine) { str(0, 0, mountain~mountain~mountain~mountain); str(1, 0, "⎧$$⎫"); if (tile.mineOwner != -1) { attrset(A_NORMAL | COLOR_PAIR(UI.playerColors[tile.mineOwner])); str(1, 1, "$$"); } } else if (tile.land == Land.Water) { attrset(A_NORMAL | COLOR_PAIR(8)); str(0, 0, "≋≋≋≋"); str(1, 0, "≋≋≋≋"); } auto o = tile.owner; auto neutral = tile.pops[o] == 0; attrset(A_NORMAL | COLOR_PAIR(neutral ? UI.neutralColor : UI.playerColors[o])); if (tile.pops[o] > 0) { auto pop = max(1, tile.pops[o]/6); int[8] spots; spots[0 .. min(8, pop/8)] = 8; if (pop/8 < 8) { spots[pop/8] = pop%8; } alias dot = (pop) => dots[clamp(pop, 0, dots.length-1)]; str(0, 0, dot(spots[4])); str(0, 1, dot(spots[0])); str(0, 2, dot(spots[1])); str(0, 3, dot(spots[5])); str(1, 0, dot(spots[6])); str(1, 1, dot(spots[2])); str(1, 2, dot(spots[3])); str(1, 3, dot(spots[7])); } if (tile.cityLevel > 0) { str(1, 1, cities[tile.cityLevel] ~ cities[tile.cityLevel]); } if (tile.flags[board.playerIdx]) { attrset(A_NORMAL | COLOR_PAIR(0)); str(1, 1, flag); } attrset(A_NORMAL | COLOR_PAIR(0)); if (ui.selected.x == x && ui.selected.y == y) { str(0, 0, "◸"); str(0, 3, "◹"); str(1, 0, "◺"); str(1, 3, "◿"); } } } if (ui.showRoutes) { foreach (pos; paths.path(ui.selected, board[ui.selected].targetFlagPos)) { alias str = (row, col, str) => _addstr(pos.y*2 + row, pos.x*4 + col, str); str(0, 0, "P"); } } // bottom panel { alias str = (row, col, str) => _addstr(row + board.h*2, col, str); str(0, 0, "pop on tile: "); foreach (pl, pop; board[ui.selected].pops) { attrset(A_NORMAL | COLOR_PAIR(UI.playerColors[pl])); _addstr(format!"%3d "(pop)); } str(1, 0, "global pop: "); foreach (pl, ref player; board.players) { attrset(A_NORMAL | COLOR_PAIR(UI.playerColors[pl])); _addstr(format!"%5d "(player.pop)); } attrset(A_NORMAL | COLOR_PAIR(0)); str(2, 0, "gold: " ~ format!"%6d"(board.players[board.playerIdx].gold)); } // top panel { alias str = (row, col, str) => _addstr(row, board.w * 4 + 1 + col, str); str(0, 0, ui.speed.to!string ~ "x" ~ (ui.paused ? " (paused)" : "")); } refresh(); } struct Array2D(T) { int w, h; private T[] arr; this(int w, int h, T init = T.init) { this.w = w; this.h = h; arr = new T[w * h]; arr[] = init; } T opIndex(int x, int y) { assert(isValid(x, y)); return arr[y * w + x]; } void opIndexAssign(T t, int x, int y) { assert(isValid(x, y)); arr[y * w + x] = t; } bool isValid(int xx, int yy) { return xx >= 0 && xx < w && yy >= 0 && yy < h; } } auto floydWarshall(Board)(ref Board board) { auto V = board.w * board.h; auto dist = Array2D!float(V, V, float.infinity); auto next = Array2D!int(V, V, -1); alias posToIdx = (pos) => pos.y * board.w + pos.x; alias idxToPos = (idx) => Pos(idx % board.w, idx / board.w); foreach (pos; board.positions) { foreach (nei; board.neighbors(pos)) { auto u = posToIdx(pos); auto v = posToIdx(nei); assert(pos == idxToPos(posToIdx(pos))); dist[u, v] = 1; next[u, v] = v; } } foreach (v; 0 .. V) { dist[v, v] = 0; next[v, v] = v; } foreach (k; 0 .. V) { foreach (i; 0 .. V) { if (dist[i, k] == float.infinity) continue; foreach (j; 0 .. V) { if (dist[i, j] > dist[i, k] + dist[k, j]) { dist[i, j] = dist[i, k] + dist[k, j]; next[i, j] = next[i, k]; } } } } return Paths(next, board.w); } struct Paths { Array2D!int next; int w; auto posToIdx(Pos pos) { return pos.y * w + pos.x; } auto idxToPos(int idx) { return Pos(idx % w, idx / w); } Pos[] path(Pos from, Pos to) { int u = posToIdx(from); int v = posToIdx(to); if (next[u, v] == -1) { return []; } auto path = [idxToPos(u)]; while (u != v) { u = next[u, v]; path ~= idxToPos(u); } return path; } auto direction(Pos from, Pos to) { int fromIdx = posToIdx(from); int toIdx = posToIdx(to); if (next[fromIdx, toIdx] == -1) { return from; // stay where you are } return idxToPos(next[fromIdx, toIdx]); } }