0xDEADBEEF

RSS odkazy

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:


/+ 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]);
  }
}
píše k47 (@kaja47, k47)