yairm210 / Unciv

Open-source Android/Desktop remake of Civ V
Mozilla Public License 2.0
8.49k stars 1.57k forks source link

Command Line Interface and API-GUI convergence? #5552

Closed will-ca closed 3 years ago

will-ca commented 3 years ago

Is your feature request related to a problem? Please describe. Managing large empires is tedious and painful. A lot of stuff, like build order, unit promotions, finding resources or freeing them up, is highly repetitive.

I like a slick UI as much as the next guy. But in an open-sourced program with clean code, it's frustrating to have to spend several minutes and hundreds of mouse clicks to do something that could be described with a single line of scripting or with a reusable function.

Describe the solution you'd like It would be great to have a CLI that can be used to access all information and perform all actions that are currently available in the UI.

E.G.:

len(Empire.Cities) to count the number of cities to better plan strategic resource use, instead of having to manually count in the Overview screen.

[city for city in Empire.Cities if any("Aluminium" in building.Consumes for building in city.Buildings)] to get a list of all cities that have Spaceship Factories or Hydro Plants, instead of having to click into dozens of cities and scroll through the entire building list for each.

Empire.Cities.map((city) => city.Governor.SetPriority("Food")) to tell all cities to stop assigning scientists and focus on population growth (once governors get implemented).

[city for city in Empire.Cities if city.Tiles.has("Aluminium")] to figure out where your strategic resources are, instead of having to stare at the map for several minutes, checking all city centers separately, and still missing them.

UITools.Messages.ResourceDiscovery("Uranium", target="Mongolia") to manually re-trigger the "You have discovered [X] sources of [Resource]!" notification, but have it target and cycle through resources on visible tiles near a foreign country, instead of having to spend several minutes staring at every tile and then finding out that you've still missed some after you've already declared and ended a war.

for i, city in enumerate(sorted(Empire.Cities, key=lambda city: -city.Stats.Production.Base)); do if i < Empire.Resources["Coal"]; then city.Production.prepend("Factory"); fi; city.Production.append("NukeSub"*3, "Cruiser"); done to automatically build as many factories in your largest cities as you have coal resources, and queue up three nuclear submarines and one cruiser to be built in all cities.

buildorder = ["Monument", "Granary", ...]; for city in Empire.Cities: if not city.Production: for building in buildorder: try: city.Production.append(building) to have cities follow a custom automatic build order.

UITools.Map.HighlightCoords(tile.Coord for tile in Map.Tiles if "Coal" in tile.Resources) to display some sort of visual overlay for all visible coals resources, instead of again having to scour every tile by eye.

TurnData.GetMovements().map((move) => UITools.Map.DrawArrow(move.From, move.To, move.isAttack ? "Red" : "Blue")) to display some sort of visual overlay that shows how every visible unit on the map moved last turn, with red arrows indicating attacks and blue arrows for regular movements.

Empire.Units.map((unit) => [unit, unit.Tile.GetNearestUnit(["Interception" ])]).filter(([unit, aa]) => !(aa.InRange(unit))).map(([unit, aa]) => UITools.Map.DrawArrow(unit.Tile.Coord, aa.Tile.Coord)) to locate all your own units that aren't currently covered by any air defences, and draw a visual overlay on the map pointing them to the nearest AA units. (Or plug in the visible unit list of another empire to figure out where you can safely airstrike.)

To automatically promote all melee units to be generalized, and automatically promote all naval ranged units to maintain a 2:1 ratio of land and naval specialization:

PromotionRatios = {
 ("Naval", "Ranged"): {
  ("Bombardment I", "Bombardment II", "Range"): 2,
  ("Targeting I", "Targeting II", "Sentry"): 1
 },
 ("Melee"): {
  ("Shock I", "Drill I", "Cover I", "Cover II"): 1
 }
}

def GetUnitTypeSig(unit):
 sig = []
 for c, t, f in ((unit.Type.isNaval, "Naval", None), (unit.Type.isRanged, "Ranged", "Melee")):
  s = t if c else f
  if c and s:
   sig.append(s)
 return tuple(s)

def CheckUnitMatchesPromotions(unit, promotions):
 return unit.Promotions and all(p in promotions for p in unit.Promotions)

def PromoteUnitTowards(unit, promotions):
 for promo in promotions:
  try:
   unit.Promotions.Select(promo)
  except XPError:
   pass

def AutoPromoteAll():
 unitsets = {sig: [] for sig in PromotionRatios}
 for unit in Empire.Units:
  sig = GetUnitTypeSig(unit)
  if sig in unitsets:
   unitsets[sig].append(unit)
 for sig, units in unitsets.items():
  targetratios = PromotionRatios[sig]
  currentcounts = {promos: len([u for u in units if CheckUnitMatchesPromotions(unit, promos)]) for promos in targetratios}
  for unit in units:
   if unit.Promotions.available:
    selectedpromos = min({promos: currentcounts[promos]/target for promos, target in targetratios.items()}.items(), key=lambda i: i[1])[0]
   PromoteUnitTowards(unit, selectedpromos)
   if CheckUnitMatchesPromotions(unit, selectedpromos):
    currentcounts[selectedpromos] += 1

AutoPromoteAll()

To quickly distribute cruise missiles evenly across cities, submarines, and missile cruisers:

airbases = [...Empire.Cities, ...(Empire.Units.filter((unit) => unit.hasUnique("Can carry [] Missile units")))];

var ismissile = (unit) => unit.hasUnique("Self-destructs when attacking");

num_missiles = Empire.Units.filter(ismissile).length;

maxmissiles = Math.ceil(num_missiles/airbases.length);

for (let base of airbases) {
 while (base.carriedUnits.filter(ismissile).length > maxmissiles) {
  let missile = base.carriedUnits.find(ismissile);
  missile.actions.doRebase([...missile.RebaseOptions].sort((baseoption) => baseoption.Units.filter(ismissile).length)[0]);
 }
}

…You get the idea

Describe alternatives you've considered Massive UI-bloat to special-case all possible visualizations and automations.

Arbitrary code execution, and custom UI elements in the modding API, then making bindings and packing a Python interpreter or Javascript engine or something into a mod.

Fork of this project that adds this. Or binary mod for Sid Meier's Civ V.

Additional context IMHO it would be cool if the internal API and exposed UI had a 1:1 mapping— If the console exposed some version of the same calls as are made by UI elements, and all checks for legal operations were shared between the console and UI, so there wouldn't be anything cheaty about playing this way. (Being able to add duplicate buildings and previously being able to select incompatible social policies by clicking quickly suggests that definitions of legal actions are currently not separated from UI code?)

Triggering a drop-down with the backtick key, like in a lot of publishers' games, would be a sensible way to expose it on Desktop I think. Maybe an on-screen button could be hidden by default, and toggleable in the Options menu, for mobile and mouse users.

With a flag to enable a separate set of illegal commands/cheats, I suppose this could also make it easier to test normal features? Cheats.Enable(), Cheats.InstantProduction = 1, Cheats.SpawnAtTile("Lancer"), Empire.ActiveUnit.XP += 100, etc. (Properties like .XP would usually be available as well, but they would be read-only until cheats are turned on.)

If implemented, being able to define one (or multiple) start-up scripts/macros to populate the namespace with user-defined constants/functions would be a no-brainer.

An idiomatic and expressive high-level language would be ideal IMO. Gracefully handling null values and such would be good for mapping idempotent operations (E.G. selling a building, choosing a promotion, setting a focus) to lots of cities/units without having to either wrap in try blocks or run .filter()s. Small quirks for enhancing usability like coercing everything to the same capitalization and having reasonable aliases ("Nuclear Submarine", "NukeSub", "NukeSubmarine", etc.) could be nice too.

Converging the player GUI and a CLI/API may also have other benefits— Being able to easily plug Unciv into a machine learning experiment, for example, or quickly prototype traditional AI behaviours.

Perhaps popular user scripts could be a source of proven candidates for reimplementation as part of the core game.

I know this is a very big ask, may not fit within the scope of this project, may require infeasible architectural changes, or simply might not interest any current developers.

yairm210 commented 3 years ago

Some requests are so large that their implementation would be an entirely new separate project. This is one of those.

You make a fair point about some actions being blocked on the ui level and not the game logic level, and yes, those need to be rectified.

will-ca commented 3 years ago

@yairm210 Yep. I was originally going to wait until/unless I could offer to contribute the code myself. But I figured I may as well submit a feature request in case anyone else wants to pick it up.

From that perspective, I'm selfishly glad you agree about abstracting game rules away from the UI, as that would make it easier.

..Which does leave me with a question: If, in a couple months, or a couple years, I've decided to use Kotlin and I've otherwise got my act together enough that I decide to start trying to program this, would there be any consideration for accepting it as a PR (or a series of PRs) (subject to the usual reviews, OFC), assuming nothing unexpected changes between now and then?

yairm210 commented 3 years ago

I see no reason why not :)

ajustsomebody commented 3 years ago

cheats for unciv would absolutely be great

will-ca commented 2 years ago

Some requests are so large that their implementation would be an entirely new separate project. This is one of those.

Yeah, you weren't kidding.