Console mode for multiple game automation (#2777)

* Remove ruleset from GameSetupInfo class

* Remove dependency from Gdx for file IO:
- load Ruleset
- save/init in GameSettings
- get settings in GameSaver

* Remove simulation logging from GameInfo class

* MapGenerator: add switch for RNG seed verbose

* PlayerPickerTable small refactor

* Basic console mode

* Add multithreading to console mode and refactoring.

* Merge branch 'master' into console

* Small refactor
This commit is contained in:
Alexander Korolyov 2020-07-04 20:47:52 +02:00 committed by GitHub
parent 0271fdead2
commit 4bcae5f664
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 316 additions and 41 deletions

View file

@ -52,8 +52,8 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
* Does not update World View changes until finished.
* Set false to disable.
*/
val simulateMaxTurns: Int = 2000
val simulateUntilWin = false
var simulateMaxTurns: Int = 1500
var simulateUntilWin = false
/** Console log battles
*/

View file

@ -102,9 +102,7 @@ class GameInfo {
if (thisPlayer.victoryManager.hasWon() && simulateUntilWin) {
// stop simulation
simulateUntilWin = false
println("Simulation stopped on turn $turns")
val victoryType = thisPlayer.victoryManager.hasWonVictoryType()
println("$thisPlayer won $victoryType victory")
break
}
}
switchTurn()
@ -113,8 +111,6 @@ class GameInfo {
currentPlayer = thisPlayer.civName
currentPlayerCiv = getCivilization(currentPlayer)
if (turns == UncivGame.Current.simulateMaxTurns && UncivGame.Current.simulateUntilWin)
println ("Max simulation turns reached $turns: Draw")
// Start our turn immediately before the player can made decisions - affects whether our units can commit automated actions and then be attacked immediately etc.
notifyOfCloseEnemyUnits(thisPlayer)
@ -379,6 +375,4 @@ class GameInfo {
cityConstructions.inProgressConstructions.remove(oldBuildingName)
}
}
}

View file

@ -57,7 +57,11 @@ object GameSaver {
}
fun getGeneralSettingsFile(): FileHandle {
return Gdx.files.local(settingsFileName)
try {
return Gdx.files.local(settingsFileName)
} catch (ex: NullPointerException) {
return FileHandle(settingsFileName)
}
}
fun getGeneralSettings(): GameSettings {

View file

@ -372,7 +372,8 @@ class CityInfo {
isPuppet = false
cityConstructions.inProgressConstructions.clear() // undo all progress of the previous civ on units etc.
cityStats.update()
UncivGame.Current.worldScreen.shouldUpdate = true
if (!UncivGame.Current.consoleMode)
UncivGame.Current.worldScreen.shouldUpdate = true
}
/** This happens when we either puppet OR annex, basically whenever we conquer a city and don't liberate it */

View file

@ -51,9 +51,9 @@ class MapGenerator(val ruleset: Ruleset) {
return map
}
private fun seedRNG(seed: Long) {
private fun seedRNG(seed: Long, verbose: Boolean = false) {
randomness.RNG = Random(seed)
println("RNG seeded with $seed")
if (verbose) println("RNG seeded with $seed")
}
private fun spawnLakesAndCoasts(map: TileMap) {

View file

@ -39,13 +39,13 @@ class GameSettings {
init {
// 26 = Android Oreo. Versions below may display permanent icon in notification bar.
if (Gdx.app.type == Application.ApplicationType.Android && Gdx.app.version < 26) {
if (Gdx.app?.type == Application.ApplicationType.Android && Gdx.app.version < 26) {
multiplayerTurnCheckerPersistentNotificationEnabled = false
}
}
fun save(){
if (!isFreshlyCreated && Gdx.app.type == Application.ApplicationType.Desktop) {
if (!isFreshlyCreated && Gdx.app?.type == Application.ApplicationType.Desktop) {
windowState = WindowState( Gdx.graphics.width, Gdx.graphics.height)
}
GameSaver.setGeneralSettings(this)

View file

@ -1,5 +1,6 @@
package com.unciv.models.ruleset
import com.badlogic.gdx.Files
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants
@ -181,9 +182,20 @@ class Ruleset {
object RulesetCache :HashMap<String,Ruleset>() {
val vanillaRuleset = "Civ V - Vanilla"
fun loadRulesets() {
this[""] = Ruleset().apply { load(Gdx.files.internal("jsons/$vanillaRuleset")) }
try {
this[""] = Ruleset().apply { load(Gdx.files.internal("jsons/$vanillaRuleset")) }
} catch (e: NullPointerException) {
this[""] = Ruleset().apply { load(FileHandle("jsons/$vanillaRuleset")) }
}
for (modFolder in Gdx.files.local("mods").list()) {
var modsHandles: Array<FileHandle>
try {
modsHandles = Gdx.files.local("mods").list()
} catch (ex: NullPointerException) {
modsHandles = FileHandle("mods").list()
}
for (modFolder in modsHandles) {
if (modFolder.name().startsWith('.')) continue
try {
val modRuleset = Ruleset()

View file

@ -0,0 +1,125 @@
package com.unciv.models.simulation
import com.unciv.logic.GameInfo
import com.unciv.logic.GameStarter
import com.unciv.models.ruleset.VictoryType
import com.unciv.ui.newgamescreen.GameSetupInfo
import java.lang.Integer.max
import java.time.Duration
import kotlin.concurrent.thread
class Simulation(val newGameInfo: GameInfo,
val simulationsPerThread: Int = 5,
val threadsNumber: Int = 1
) {
val maxSimulations = threadsNumber * simulationsPerThread
val civilizations = newGameInfo.civilizations.filter { it.civName != "Spectator" }.map { it.civName }
private var startTime: Long = 0
private var endTime: Long = 0
var steps = ArrayList<SimulationStep>()
var winRate = mutableMapOf<String, MutableInt>()
var winRateByVictory = HashMap<String, MutableMap<VictoryType, MutableInt>>()
var avgSpeed = 0f
var avgDuration: Duration = Duration.ZERO
private var totalTurns = 0
private var totalDuration: Duration = Duration.ZERO
var stepCounter: Int = 0
init{
for (civ in civilizations) {
this.winRate[civ] = MutableInt(0)
winRateByVictory[civ] = mutableMapOf<VictoryType,MutableInt>()
for (victory in VictoryType.values())
winRateByVictory[civ]!![victory] = MutableInt(0)
}
}
fun start() {
startTime = System.currentTimeMillis()
val threads: ArrayList<Thread> = ArrayList()
for (threadId in 1..threadsNumber) {
threads.add(thread {
for (i in 1..simulationsPerThread) {
val gameInfo = GameStarter.startNewGame(GameSetupInfo(newGameInfo))
gameInfo.nextTurn()
var step = SimulationStep(gameInfo)
if (step.victoryType != null) {
step.winner = step.currentPlayer
printWinner(step)
}
else
printDraw(step)
updateCounter(threadId)
add(step)
}
})
}
// wait for all threads to finish
for (thread in threads) thread.join()
endTime = System.currentTimeMillis()
}
@Synchronized fun add(step: SimulationStep, threadId: Int = 1) {
// println("Thread $threadId: End simulation ($stepCounter/$maxSimulations)")
steps.add(step)
}
@Synchronized fun updateCounter(threadId: Int = 1) {
stepCounter++
// println("Thread $threadId: Start simulation ($stepCounter/$maxSimulations)")
println("Simulation step ($stepCounter/$maxSimulations)")
}
private fun printWinner(step: SimulationStep) {
println("%s won %s victory on %d turn".format(
step.winner,
step.victoryType,
step.turns
))
}
private fun printDraw(step: SimulationStep) {
println("Max simulation %d turns reached : Draw".format(
step.turns
))
}
fun getStats() {
// win Rate
steps.forEach {
if (it.winner != null) {
winRate[it.winner!!]!!.inc()
winRateByVictory[it.winner!!]!![it.victoryType]!!.inc()
}
}
totalTurns = steps.sumBy { it.turns }
totalDuration = Duration.ofMillis(endTime - startTime)
avgSpeed = totalTurns.toFloat() / totalDuration.seconds
avgDuration = totalDuration.dividedBy(steps.size.toLong())
}
override fun toString(): String {
var outString = ""
for (civ in civilizations) {
outString += "\n$civ:\n"
val wins = winRate[civ]!!.value!! * 100 / max(steps.size, 1)
outString += "$wins% total win rate \n"
for (victory in VictoryType.values()) {
val winsVictory = winRateByVictory[civ]!![victory]!!.value * 100 / max(winRate[civ]!!.value, 1)
outString += "$victory: $winsVictory% "
}
outString += "\n"
}
outString += "\nAverage speed: %.1f turns/s \n".format(avgSpeed)
outString += "Average game duration: " + formatDuration(avgDuration) + "\n"
outString += "Total time: " + formatDuration(totalDuration) + "\n"
return outString
}
}

View file

@ -0,0 +1,15 @@
package com.unciv.models.simulation
import com.unciv.logic.GameInfo
import com.unciv.models.ruleset.VictoryType
import java.time.Duration
class SimulationStep (gameInfo: GameInfo) {
var turns = gameInfo.turns
var victoryType = gameInfo.currentPlayerCiv.victoryManager.hasWonVictoryType()
var winner: String? = null
val currentPlayer = gameInfo.currentPlayer
// val durationString: String = formatDuration(Duration.ofMillis(System.currentTimeMillis() - startTime))
}

View file

@ -0,0 +1,30 @@
package com.unciv.models.simulation
import java.time.Duration
class MutableInt(var value: Int = 0) {
fun inc() { ++value }
fun get(): Int { return value }
fun set(newValue: Int) { value = newValue }
override fun toString(): String {
return value.toString()
}
}
fun formatDuration(d: Duration): String {
var d = d
val days = d.toDays()
d = d.minusDays(days)
val hours = d.toHours()
d = d.minusHours(hours)
val minutes = d.toMinutes()
d = d.minusMinutes(minutes)
val seconds = d.seconds
d = d.minusSeconds(seconds)
val millis = d.toMillis()
return (if (days == 0L) "" else "$days"+"d ") +
(if (hours == 0L) "" else "$hours"+"h ") +
(if (minutes == 0L) "" else "$minutes"+"m ") +
(if (seconds == 0L) "$millis"+"ms" else "$seconds"+"."+"$millis".take(2)+"s")
}

View file

@ -31,7 +31,7 @@ class EditorMapHolder(internal val mapEditorScreen: MapEditorScreen, internal va
// This is a hack to make the unit icons render correctly on the game, even though the map isn't part of a game
// and the units aren't assigned to any "real" CivInfo
tileGroup.tileInfo.getUnits().forEach { it.civInfo= CivilizationInfo()
.apply { nation=mapEditorScreen.gameSetupInfo.ruleset.nations[it.owner]!! } }
.apply { nation=mapEditorScreen.ruleset.nations[it.owner]!! } }
tileGroup.showEntireMap = true
tileGroup.update()

View file

@ -2,6 +2,7 @@ package com.unciv.ui.mapeditor
import com.unciv.UncivGame
import com.unciv.logic.map.Scenario
import com.unciv.models.ruleset.Ruleset
import com.unciv.ui.newgamescreen.GameOptionsTable
import com.unciv.ui.newgamescreen.GameSetupInfo
import com.unciv.ui.newgamescreen.PlayerPickerTable
@ -16,10 +17,11 @@ import com.unciv.ui.utils.*
* @param [mapEditorScreen] previous screen from map editor.
*/
class GameParametersScreen(var mapEditorScreen: MapEditorScreen): IPreviousScreen, PickerScreen() {
override var gameSetupInfo: GameSetupInfo = mapEditorScreen.gameSetupInfo
override var ruleset: Ruleset = mapEditorScreen.ruleset
var playerPickerTable = PlayerPickerTable(this, this.gameSetupInfo.gameParameters)
var gameOptionsTable = GameOptionsTable(mapEditorScreen.gameSetupInfo) { desiredCiv: String -> playerPickerTable.update(desiredCiv) }
var gameOptionsTable = GameOptionsTable(mapEditorScreen) { desiredCiv: String -> playerPickerTable.update(desiredCiv) }
init {
setDefaultCloseAction(mapEditorScreen)
@ -32,6 +34,7 @@ class GameParametersScreen(var mapEditorScreen: MapEditorScreen): IPreviousScree
rightSideButton.onClick {
mapEditorScreen.gameSetupInfo = gameSetupInfo
mapEditorScreen.scenario = Scenario(mapEditorScreen.tileMap, mapEditorScreen.gameSetupInfo.gameParameters)
mapEditorScreen.ruleset = ruleset //TODO: figure out whether it is necessary
mapEditorScreen.tileEditorOptions.update()
mapEditorScreen.mapHolder.updateTileGroups()
UncivGame.Current.setScreen(mapEditorScreen)

View file

@ -12,6 +12,7 @@ import com.unciv.logic.MapSaver
import com.unciv.logic.map.Scenario
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr
import com.unciv.ui.newgamescreen.GameSetupInfo
import com.unciv.ui.newgamescreen.IPreviousScreen
@ -25,6 +26,7 @@ class MapEditorScreen(): IPreviousScreen, CameraStageBaseScreen() {
var tileMap = TileMap()
var scenarioName = "" // when loading map: mapName is taken as default for scenarioName
var scenario: Scenario? = null // main indicator whether scenario information is present
override var ruleset = RulesetCache.getBaseRuleset()
override var gameSetupInfo = GameSetupInfo()
lateinit var mapHolder: EditorMapHolder
@ -66,7 +68,7 @@ class MapEditorScreen(): IPreviousScreen, CameraStageBaseScreen() {
}
fun initialize() {
tileMap.setTransients(gameSetupInfo.ruleset,false)
tileMap.setTransients(ruleset,false)
mapHolder = EditorMapHolder(this, tileMap)
mapHolder.addTiles(stage.width)

View file

@ -34,7 +34,7 @@ class TileEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(Camera
var brushSize = 1
private var currentHex: Actor = Group()
private val ruleset = mapEditorScreen.gameSetupInfo.ruleset
private val ruleset = mapEditorScreen.ruleset
private val gameParameters = mapEditorScreen.gameSetupInfo.gameParameters
private val scrollPanelHeight = mapEditorScreen.stage.height*0.7f - 100f // -100 reserved for currentHex table
@ -353,7 +353,7 @@ class TileEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(Camera
// for the tile image
val tileInfo = TileInfo()
tileInfo.ruleset = mapEditorScreen.gameSetupInfo.ruleset
tileInfo.ruleset = mapEditorScreen.ruleset
val terrain = resource.terrainsCanBeFoundOn.first()
val terrainObject = ruleset.terrains[terrain]!!
if (terrainObject.type == TerrainType.TerrainFeature) {
@ -446,7 +446,7 @@ class TileEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(Camera
}
private fun makeTileGroup(tileInfo: TileInfo): TileGroup {
tileInfo.ruleset = mapEditorScreen.gameSetupInfo.ruleset
tileInfo.ruleset = mapEditorScreen.ruleset
tileInfo.setTerrainTransients()
val group = TileGroup(tileInfo, TileSetStrings())
group.showEntireMap = true

View file

@ -10,10 +10,10 @@ import com.unciv.models.ruleset.VictoryType
import com.unciv.models.translations.tr
import com.unciv.ui.utils.*
class GameOptionsTable(gameSetupInfo: GameSetupInfo, val updatePlayerPickerTable:(desiredCiv:String)->Unit)
class GameOptionsTable(previousScreen: IPreviousScreen, val updatePlayerPickerTable:(desiredCiv:String)->Unit)
: Table(CameraStageBaseScreen.skin) {
var gameParameters = gameSetupInfo.gameParameters
val ruleset = gameSetupInfo.ruleset
var gameParameters = previousScreen.gameSetupInfo.gameParameters
val ruleset = previousScreen.ruleset
var locked = false
init {

View file

@ -12,6 +12,7 @@ import com.unciv.ui.utils.CameraStageBaseScreen
interface IPreviousScreen {
val gameSetupInfo: GameSetupInfo
var stage: Stage
val ruleset: Ruleset
/**
* Method added for compatibility with [PlayerPickerTable] which addresses

View file

@ -20,7 +20,7 @@ import kotlin.concurrent.thread
class GameSetupInfo(var gameId:String, var gameParameters: GameParameters, var mapParameters: MapParameters) {
var ruleset = RulesetCache.getComplexRuleset(gameParameters.mods)
// var ruleset = RulesetCache.getComplexRuleset(gameParameters.mods)
constructor() : this("", GameParameters(), MapParameters())
constructor(gameInfo: GameInfo) : this("", gameInfo.gameParameters.clone(), gameInfo.tileMap.mapParameters)
@ -29,8 +29,9 @@ class GameSetupInfo(var gameId:String, var gameParameters: GameParameters, var m
class NewGameScreen(previousScreen:CameraStageBaseScreen, _gameSetupInfo: GameSetupInfo?=null): IPreviousScreen, PickerScreen() {
override val gameSetupInfo = _gameSetupInfo ?: GameSetupInfo()
override val ruleset = RulesetCache.getComplexRuleset(gameSetupInfo.gameParameters.mods)
var playerPickerTable = PlayerPickerTable(this, gameSetupInfo.gameParameters)
var newGameOptionsTable = GameOptionsTable(gameSetupInfo) { desiredCiv: String -> playerPickerTable.update(desiredCiv) }
var newGameOptionsTable = GameOptionsTable(this) { desiredCiv: String -> playerPickerTable.update(desiredCiv) }
var mapOptionsTable = MapOptionsTable(this)

View file

@ -15,10 +15,12 @@ import com.unciv.logic.civilization.PlayerType
import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.Player
import com.unciv.models.ruleset.Nation
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.translations.tr
import com.unciv.ui.mapeditor.GameParametersScreen
import com.unciv.ui.utils.*
import java.util.*
import kotlin.reflect.typeOf
/**
* This [Table] is used to pick or edit players information for new game/scenario creation.
@ -55,10 +57,10 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
*/
fun update(desiredCiv: String = "") {
playerListTable.clear()
val ruleset = previousScreen.gameSetupInfo.ruleset // the mod picking changes this ruleset
val gameBasics = previousScreen.ruleset // the mod picking changes this ruleset
reassignRemovedModReferences()
val newRulesetPlayableCivs = previousScreen.gameSetupInfo.ruleset.nations.count { it.key != Constants.barbarians }
val newRulesetPlayableCivs = previousScreen.ruleset.nations.count { it.key != Constants.barbarians }
if (gameParameters.players.size > newRulesetPlayableCivs)
gameParameters.players = ArrayList(gameParameters.players.subList(0, newRulesetPlayableCivs))
if (desiredCiv.isNotEmpty()) assignDesiredCiv(desiredCiv)
@ -66,7 +68,7 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
for (player in gameParameters.players) {
playerListTable.add(getPlayerTable(player)).width(civBlocksWidth).padBottom(20f).row()
}
if (gameParameters.players.count() < ruleset.nations.values.count { it.isMajorCiv() }
if (gameParameters.players.count() < gameBasics.nations.values.count { it.isMajorCiv() }
&& !locked) {
playerListTable.add("+".toLabel(Color.BLACK, 30).apply { this.setAlignment(Align.center) }
.surroundWithCircle(50f).onClick {
@ -85,7 +87,7 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
*/
private fun reassignRemovedModReferences() {
for (player in gameParameters.players) {
if (!previousScreen.gameSetupInfo.ruleset.nations.containsKey(player.chosenCiv))
if (!previousScreen.ruleset.nations.containsKey(player.chosenCiv))
player.chosenCiv = "Random"
}
}
@ -118,7 +120,6 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
val playerTypeTextbutton = player.playerType.name.toTextButton()
playerTypeTextbutton.onClick {
// if (locked) return@onClick
if (player.playerType == PlayerType.AI)
player.playerType = PlayerType.Human
else player.playerType = PlayerType.AI
@ -184,7 +185,7 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
.apply { this.setAlignment(Align.center) }
.surroundWithCircle(36f).apply { circle.color = Color.BLACK }
.surroundWithCircle(40f, false).apply { circle.color = Color.WHITE }
else ImageGetter.getNationIndicator(previousScreen.gameSetupInfo.ruleset.nations[player.chosenCiv]!!, 40f)
else ImageGetter.getNationIndicator(previousScreen.ruleset.nations[player.chosenCiv]!!, 40f)
nationTable.add(nationImage).pad(5f)
nationTable.add(player.chosenCiv.toLabel()).pad(5f)
nationTable.touchable = Touchable.enabled
@ -225,10 +226,9 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
if (player.chosenCiv == nation.name)
continue
nationListTable.add(NationTable(nation, nationsPopupWidth, previousScreen.gameSetupInfo.ruleset).onClick {
if (previousScreen is GameParametersScreen) {
nationListTable.add(NationTable(nation, nationsPopupWidth, previousScreen.ruleset).onClick {
if (previousScreen is GameParametersScreen)
previousScreen.mapEditorScreen.tileMap.switchPlayersNation(player, nation)
}
player.chosenCiv = nation.name
nationsPopup.close()
update()
@ -259,7 +259,7 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
*/
private fun getAvailablePlayerCivs(): ArrayList<Nation> {
var nations = ArrayList<Nation>()
for (nation in previousScreen.gameSetupInfo.ruleset.nations.values
for (nation in previousScreen.ruleset.nations.values
.filter { it.isMajorCiv() }) {
if (gameParameters.players.any { it.chosenCiv == nation.name })
continue
@ -267,5 +267,4 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters:
}
return nations
}
}

View file

@ -0,0 +1,88 @@
package com.unciv.app.desktop
import com.unciv.UncivGame
import com.unciv.UncivGameParameters
import com.unciv.logic.GameStarter
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapSize
import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.GameSettings
import com.unciv.models.metadata.GameSpeed
import com.unciv.models.metadata.Player
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.simulation.Simulation
import com.unciv.models.simulation.SimulationStep
import com.unciv.models.simulation.formatDuration
import com.unciv.ui.newgamescreen.GameSetupInfo
import java.time.Duration
import kotlin.concurrent.thread
import kotlin.system.exitProcess
internal object ConsoleLauncher {
@JvmStatic
fun main(arg: Array<String>) {
val version = "0.1"
val consoleParameters = UncivGameParameters(
version,
null,
{ exitProcess(0) },
null,
null,
true
)
val game = UncivGame(consoleParameters)
UncivGame.Current = game
UncivGame.Current.settings = GameSettings().apply { showTutorials = false }
UncivGame.Current.simulateMaxTurns = 1000
UncivGame.Current.simulateUntilWin = true
RulesetCache.loadRulesets()
val gameParameters = getGameParameters("China", "Greece")
val mapParameters = getMapParameters()
val gameSetupInfo = GameSetupInfo(gameParameters, mapParameters)
val newGame = GameStarter.startNewGame(gameSetupInfo)
UncivGame.Current.gameInfo = newGame
var simulation = Simulation(newGame,25,4)
simulation.start()
simulation.getStats()
println(simulation)
}
private fun getMapParameters(): MapParameters {
return MapParameters().apply {
size = MapSize.Tiny
noRuins = true
noNaturalWonders = true
}
}
private fun getGameParameters(civilization1: String, civilization2: String): GameParameters {
return GameParameters().apply {
difficulty = "Chieftain"
gameSpeed = GameSpeed.Quick
noBarbarians = true
players = ArrayList<Player>().apply {
add(Player().apply {
playerType = PlayerType.AI
chosenCiv = civilization1
})
add(Player().apply {
playerType = PlayerType.AI
chosenCiv = civilization2
})
add(Player().apply {
playerType = PlayerType.Human
chosenCiv = "Spectator"
})
}
}
}
}