freecol/test/src/net/sf/freecol/util/test/FreeColTestCase.java

761 lines
25 KiB
Java

/**
* Copyright (C) 2002-2022 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* FreeCol is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.util.test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import junit.framework.TestCase;
import net.sf.freecol.FreeCol;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.io.FreeColTcFile;
import net.sf.freecol.common.model.AbstractGoods;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.CombatModel;
import net.sf.freecol.common.model.CombatModel.CombatOdds;
import net.sf.freecol.common.model.CombatModel.CombatResult;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Nation;
import net.sf.freecol.common.model.NationOptions;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Region;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.TileType;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.UnitChangeType;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.model.UnitTypeChange;
import net.sf.freecol.common.model.WorkLocation;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.server.model.ServerGame;
import net.sf.freecol.server.model.ServerIndianSettlement;
import net.sf.freecol.server.model.ServerPlayer;
import net.sf.freecol.server.model.ServerUnit;
/**
* The base class for all FreeCol tests. Contains useful methods used by the
* individual tests.
*/
public class FreeColTestCase extends TestCase {
/** We now have lots of maps, restrict testing to just the old ones. */
public static String[] STANDARD_MAPS = {
"data/maps/M_Africa_Gilolat.fsm",
"data/maps/S_AustraliaOceania_Gilolat.fsm",
"data/maps/L_America_JsTheDude.fsm",
"data/maps/S_Caribbean_Phil.fsm"
};
private static java.util.Map<String, Specification> specifications
= new HashMap<>();
/**
* use getGame to access this.
*/
static Game game;
static boolean updateLocale = true;
@Override
protected void setUp() throws Exception {
FreeColTcFile.loadTCs();
if (updateLocale) {
updateLocale = false;
Messages.loadMessageBundle(Locale.US);
}
}
@Override
protected void tearDown() throws Exception {
// If a game has been created destroy it.
game = null;
}
/**
* Get a game pseudo-singleton, i.e. the same instance will be returned
* until getStandardGame() is called, which resets the singleton to a new
* value.
*
* Calling this method repetitively without calling getStandardGame() will
* result in the same Game being returned.
*
* @return The game singleton.
*/
public static Game getGame() {
if (game == null) {
game = getStandardGame();
}
return game;
}
/**
* Specifically sets the game instance to run with. Necessary for
* server tests that create their own game instances. Allows for
* same interface for accessing the game instance for all types of
* tests.
*
* @param newGame Game instance to work with
*/
public static void setGame(Game newGame) {
game = newGame;
}
public static Specification spec() {
return getSpecification("freecol");
}
public static Specification spec(String name) {
return getSpecification(name);
}
public static Specification getSpecification(String name) {
Specification result = specifications.get(name);
if (result == null) {
FreeColTcFile.loadTCs();
try {
FreeColTcFile tc = FreeColTcFile.getFreeColTcFile(name);
result = FreeCol.loadSpecification(tc, null, "model.difficulty.medium");
specifications.put(name, result);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return result;
}
/**
* Returns a new game, with all players set.
*
* As a side effect this call will reset the singleton game value that can
* be accessed using getGame().
*
* @return A new game with with players for each nation added.
*/
public static Game getStandardGame() {
return getStandardGame("freecol");
}
/**
* Returns a new game, with all players set.
*
* As a side effect this call will reset the singleton game value that can
* be accessed using getGame().
*
* @param specName a {@code String} value
* @return A new game with with players for each nation added.
*/
public static Game getStandardGame(String specName) {
Specification specification = spec(specName);
game = new ServerGame(specification);
NationOptions nationOptions = new NationOptions(specification);
for (Nation nation : specification.getEuropeanNations()) {
nationOptions.setNationState(nation, NationOptions.NationState.AVAILABLE);
}
game.setNationOptions(nationOptions);
specification.applyDifficultyLevel("model.difficulty.medium");
for (Nation n : specification.getNations()) {
if (n.isUnknownEnemy()) continue;
Player p = new ServerPlayer(game, false, n);
boolean ai = !n.getType().isEuropean() || n.getType().isREF();
p.setAI(ai);
if (ai || game.canAddNewPlayer()) game.addPlayer(p);
}
return game;
}
/**
* Creates a standardized map on which all fields have the plains type.
*
* Uses the getGame() method to access the currently running game.
*
* Does not call Game.setMap(Map) with the returned map. The map
* is unexplored.
*
* @return The map created as described above.
*/
public static Map getTestMap() {
MapBuilder builder = new MapBuilder(getGame());
return builder.build();
}
/**
* Creates a standardized map on which all fields have the same given type.
*
* Uses the getGame() method to access the currently running game.
*
* Does not call Game.setMap(Map) with the returned map. The map
* is unexplored.
*
* @param tileType The type of land with which to initialize the map.
*
* @return The map created as described above.
*/
public static Map getTestMap(TileType tileType) {
MapBuilder builder = new MapBuilder(getGame());
builder.setBaseTileType(tileType);
return builder.build();
}
/**
* Creates a standardized map on which all fields have the same given type.
*
* Uses the getGame() method to access the currently running game.
*
* Does not call Game.setMap(Map) with the returned map.
*
* @param tileType The type of land with which to initialize the map.
* @param explored Set to true if you want all the tiles on the
* map to have been explored by all players.
* @return The map created as described above.
*/
public static Map getTestMap(TileType tileType, boolean explored) {
MapBuilder builder = new MapBuilder(getGame());
builder.setBaseTileType(tileType).setExploredByAll(explored);
return builder.build();
}
public static Map getTestMap(boolean explored) {
MapBuilder builder = new MapBuilder(getGame());
builder.setExploredByAll(explored);
return builder.build();
}
public static Map getCoastTestMap(TileType landTileType) {
return getCoastTestMap(landTileType, false);
}
/**
* Creates a standardized map, half land (left), half sea (right)
*
* The land half has the same given type.
*
* Uses the getGame() method to access the currently running game.
*
* Does not call Game.setMap(Map) with the returned map.
*
* @param landTileType The type of land with which to initialize the map.
*
* @param explored Set to true if you want all the tiles on the map to have been explored by all players.
*
* @return The map created as described above.
*/
public static Map getCoastTestMap(TileType landTileType, boolean explored) {
int totalWidth = 20;
int totalHeight = 15;
final TileType oceanType = spec().getTileType("model.tile.ocean");
MapBuilder builder = new MapBuilder(getGame());
builder.setDimensions(totalWidth, totalHeight).setBaseTileType(oceanType);
if (explored) {
builder.setExploredByAll(true);
}
// Fill half with land, the builder will fill the rest with ocean
int landWidth = (int) Math.floor(totalWidth/2);
for (int x = 0; x < landWidth; x++) {
for (int y = 0; y < totalHeight; y++) {
builder.setTileType(x, y, landTileType);
}
}
// Add high seas.
final TileType highSeasType = spec().getTileType("model.tile.highSeas");
for (int y = 0; y < totalHeight; y++) {
builder.setTileType(totalWidth - 1, y, highSeasType);
}
return builder.build();
}
/**
* Get a standard colony at the location 5,8 with one free colonist
*
* @return The {@code Colony} as specified.
*/
public Colony getStandardColony() {
return getStandardColony(1, 5, 8);
}
/**
* Get a colony with the given number of settlers
*
* @param numberOfSettlers The number of settlers to put into the
* colony. Must be >= 1.
* @return The {@code Colony} as specified.
*/
public Colony getStandardColony(int numberOfSettlers) {
return getStandardColony(numberOfSettlers, 5, 8);
}
/**
* Get a colony with the given number of settlers, at a specified
* location.
*
* @param numberOfSettlers The number of settlers to put into the
* colony. Must be >= 1.
* @param tileX Coordinate of tile for the colony.
* @param tileY Coordinate of tile for the colony.
* @return The {@code Colony} as specified.
*/
public Colony getStandardColony(int numberOfSettlers,
int tileX, int tileY) {
Game game = getGame();
Map map = game.getMap();
Tile tile = map.getTile(tileX, tileY);
FreeColTestUtils.ColonyBuilder builder
= FreeColTestUtils.getColonyBuilder();
builder.colonyTile(tile).initialColonists(numberOfSettlers);
Colony ret = builder.build();
((ServerPlayer)ret.getOwner()).exploreForSettlement(ret);
return ret;
}
/**
* Useful utility to make sure a work location is empty before doing
* some test that implicates it.
*
* @param wl The {@code WorkLocation} to clear.
* @return True if the work location is clear, false if there was a problem
* removing a unit.
*/
public boolean clearWorkLocation(WorkLocation wl) {
for (Unit u : wl.getUnitList()) {
WorkLocation w = find(wl.getColony().getCurrentWorkLocations(),
w2 -> w2 != wl && w2.canAdd(u));
if (w != null) u.setLocation(w);
}
return wl.isEmpty();
}
public static class MapBuilder{
// Required parameter
private final Game game;
private TileType[][] tileTypes = null;
private int width;
private int height;
private TileType baseTile;
private boolean exploredByAll;
private boolean initiated;
public MapBuilder(Game game){
this.game = game;
setStartingParams();
}
private void setStartingParams(){
width = 20;
height = 15;
baseTile = spec().getTileType("model.tile.plains");
exploredByAll = false;
initiated = false;
// set empty grid
if(tileTypes == null){
tileTypes = new TileType[width][height];
}
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
tileTypes[x][y] = null;
}
}
}
public MapBuilder setBaseTileType(TileType baseType){
if(baseType == null){
throw new NullPointerException("Base tile type cannot be null");
}
this.baseTile = baseType;
return this;
}
public MapBuilder setDimensions(int width, int heigth){
if(width <= 0){
throw new RuntimeException("Width must be positive");
}
if(heigth <= 0){
throw new RuntimeException("Heigth must be positive");
}
if(initiated){
throw new IllegalStateException("Cannot resize map after setting a tile");
}
this.width = width;
this.height = heigth;
this.tileTypes = new TileType[width][height];
return this;
}
public MapBuilder setExploredByAll(boolean exploredByAll){
this.exploredByAll = exploredByAll;
return this;
}
public MapBuilder setTileType(int x, int y, TileType tileType){
if(x < 0 || y < 0){
throw new RuntimeException("Coordenates cannot be negative");
}
if(x >= width || y >= height ){
throw new RuntimeException("Coordenate out of bounds");
}
if(tileType == null){
throw new NullPointerException("Tile type cannot be null");
}
tileTypes[x][y]= tileType;
initiated = true;
return this;
}
// Implementation method, completes grid by setting uninitialized tiles
//to the base tile type
private void completeWorkingGrid(){
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
// Already manually set by the tester
if(tileTypes[x][y] != null){
continue;
}
tileTypes[x][y] = baseTile;
}
}
initiated=true;
}
public Map build(){
completeWorkingGrid();
Map map = new Map(game, width, height);
Region region = new Region(game);
map.populateTiles((x, y) -> {
TileType tileType = tileTypes[x][y];
Tile t = new Tile(game, tileType, x, y);
t.setRegion(region);
return t;
});
map.resetContiguity();
map.resetHighSeasCount();
if (exploredByAll) {
map.forEachTile(t -> {
for (Player p : game.getLiveEuropeanPlayerList()) {
t.setExplored(p, true);
}
});
}
return map;
}
public MapBuilder reset() {
setStartingParams();
return this;
}
}
public static class IndianSettlementBuilder{
private static final String defaultIndianPlayer = "model.nation.tupi";
// Required parameter
private final Game game;
private Player indianPlayer;
private String skillTaught;
private int initialBravesInCamp;
private Tile settlementTile;
private static int settlementNumber = 1;
private boolean isCapital;
private Unit residentMissionary;
public IndianSettlementBuilder(Game game) {
this.game = game;
setStartingParams();
}
private void setStartingParams() {
// Some params can only be set in build(), because the
// default values may not be valid for the game set
// However, the tester himself may set them to valid values
// later, so they are set to null for now
indianPlayer = null;
initialBravesInCamp = 1;
settlementTile = null;
skillTaught = "model.unit.masterCottonPlanter";
isCapital = false;
residentMissionary = null;
}
public IndianSettlementBuilder player(Player player) {
if (player == null
|| none(game.getPlayers(matchKey(player)))) {
throw new RuntimeException("Indian player not in game");
}
this.indianPlayer = player;
return this;
}
public IndianSettlementBuilder initialBravesInCamp(int nBraves) {
if (nBraves <= 0) {
throw new RuntimeException("Number of braves must be positive");
}
this.initialBravesInCamp = nBraves;
return this;
}
public IndianSettlementBuilder settlementTile(Tile tile) {
Tile tileOnMap = this.game.getMap()
.getTile(tile.getX(), tile.getY());
if (tile != tileOnMap) {
throw new RuntimeException("Given tile not on map");
}
this.settlementTile = tile;
return this;
}
public IndianSettlementBuilder capital(boolean isCapital) {
this.isCapital = isCapital;
return this;
}
public IndianSettlementBuilder missionary(Unit missionary) {
this.residentMissionary = missionary;
return this;
}
public IndianSettlementBuilder skillToTeach(String skill) {
this.skillTaught = skill;
return this;
}
private String getSimpleName(Player player, boolean isCapital) {
return (isCapital) ? player.getName() + "-capital"
: "Settlement-" + settlementNumber++;
}
public IndianSettlement build() {
UnitType skillToTeach = null;
if (skillTaught != null) {
skillToTeach = spec().getUnitType(skillTaught);
}
// indianPlayer not set, get default
if (indianPlayer == null) {
indianPlayer = game.getPlayerByNationId(defaultIndianPlayer);
if (indianPlayer == null) {
throw new RuntimeException("Default Indian player "
+ defaultIndianPlayer + " not in game");
}
}
UnitType indianBraveType = spec().getDefaultUnitType(indianPlayer);
// settlement tile no set, get default
if (settlementTile == null) {
settlementTile = game.getMap().getTile(5, 8);
if (settlementTile == null) {
throw new RuntimeException("Default tile not in game");
}
}
IndianSettlement camp = new ServerIndianSettlement(game,
indianPlayer, getSimpleName(indianPlayer, isCapital),
settlementTile, isCapital, skillToTeach, residentMissionary);
indianPlayer.addSettlement(camp);
// Add braves
for (int i = 0; i < initialBravesInCamp; i++) {
Unit brave = new ServerUnit(game, camp, indianPlayer,
indianBraveType);
if (brave == null) {
throw new RuntimeException("Null brave");
}
camp.addOwnedUnit(brave);
}
camp.placeSettlement(true);
return camp;
}
public IndianSettlementBuilder reset() {
setStartingParams();
return this;
}
}
/**
* Set the production bonus of the given colony to the given
* value.
*
* @param colony a {@code Colony} value
* @param value an {@code int} value
*/
public void setProductionBonus(Colony colony, int value) {
Field productionBonus;
try {
productionBonus = Colony.class.getDeclaredField("productionBonus");
productionBonus.setAccessible(true);
productionBonus.setInt(colony, value);
colony.invalidateCache();
} catch (NoSuchFieldException|IllegalAccessException ex) {
fail(ex.toString());
}
}
/**
* Build/place a colony with a unit, without requiring the server.
*/
public void nonServerBuildColony(Unit builder, Colony colony) {
colony.placeSettlement(true);//-vis
colony.getOwner().invalidateCanSeeTiles();//+vis
nonServerJoinColony(builder, colony);
}
/**
* Join a colony with a unit, without requiring the server.
*/
public void nonServerJoinColony(Unit builder, Colony colony) {
builder.setLocation(colony);
builder.setMovesLeft(0);
}
/**
* Repeatedly ask the CombatModel for an attack result until it
* gives the primary one we want (WIN, LOSE, NO_RESULT).
*/
public List<CombatResult> fakeAttackResult(CombatResult result,
FreeColGameObject attacker,
FreeColGameObject defender)
{
List<CombatResult> crs;
final double delta = 0.02;
CombatModel combatModel = getGame().getCombatModel();
CombatOdds combatOdds = combatModel.calculateCombatOdds(attacker, defender);
double p = combatOdds.win;
MockPseudoRandom mr = new MockPseudoRandom();
List<Integer> number = new ArrayList<>();
number.add(-1);
do {
p += (result == CombatResult.WIN) ? -delta : delta;
if (p < 0.0 || p >= 1.0) {
throw new IllegalStateException("f out of range: "
+ Double.toString(p));
}
number.set(0, (int)(Integer.MAX_VALUE * p));
mr.setNextNumbers(number, true);
crs = combatModel.generateAttackResult(mr, attacker, defender);
} while (crs.get(0) != result);
return crs;
}
/**
* Check a combat result list.
*
* @param name A base string for the error message.
* @param crs The list of {@code CombatResult} to check.
* @param results The expected {@code CombatResult}s.
*/
public void checkCombat(String name, List<CombatResult> crs,
CombatResult... results) {
int i = 0;
for (CombatResult cr : results) {
CombatResult expect = (i < crs.size()) ? crs.get(i) : null;
if (expect != cr) break;
i++;
}
if (i == results.length) {
if (crs.size() == i) return;
i++;
}
String err = name + ", failed at " + i + ":";
for (CombatResult cr : results) {
err += " " + cr;
}
err += " !=";
for (CombatResult cr : crs) {
err += " " + cr;
}
fail(err);
}
/**
* Check a list of goods.
*
* @param err A base string for the error message.
* @param goods A list of expected {@code AbstractGoods} to check.
* @param results The expected {@code AbstractGoods}.
*/
public void checkGoods(String err, List<AbstractGoods> goods,
AbstractGoods... results) {
List<AbstractGoods> check = new ArrayList<>(goods);
for (AbstractGoods ag : results) {
assertTrue(err + " requires " + ag, check.contains(ag));
check.remove(ag);
}
assertTrue(err + " requires more goods", check.isEmpty());
}
public void addUnitTypeChange(String type, UnitType from, UnitType to,
int probability, int turns) {
UnitTypeChange utc = new UnitTypeChange(type + "."
+ from.getSuffix() + "-" + to.getSuffix(), spec());
utc.from = from;
utc.to = to;
utc.probability = probability;
utc.turns = turns;
UnitChangeType uct = spec().getUnitChangeType(type);
if (uct == null) {
uct = new UnitChangeType(type, spec());
spec().getUnitChangeTypeList().add(uct);
}
uct.addUnitTypeChange(utc);
}
/**
* Helper function to get a server player by id.
*
* @param game The {@code Game} to look in.
* @param id The player identifier.
* @return The {@code ServerPlayer} found.
*/
public ServerPlayer getServerPlayer(Game game, String id) {
return (ServerPlayer)game.getPlayerByNationId(id);
}
}