diff --git a/src/scripts/TerrainMapGenerator.ts b/src/scripts/TerrainMapGenerator.ts index 2827d96dc..53f50f5dd 100644 --- a/src/scripts/TerrainMapGenerator.ts +++ b/src/scripts/TerrainMapGenerator.ts @@ -25,6 +25,7 @@ class Terrain { export async function generateMap( imageBuffer: Buffer, + removeSmall = true, ): Promise<{ map: Uint8Array; miniMap: Uint8Array }> { const stream = Readable.from(imageBuffer); const img = await decodePNGFromStream(stream); @@ -56,8 +57,10 @@ export async function generateMap( } } - removeSmallIslands(terrain); - removeSmallLakes(terrain); + if (removeSmall) { + removeSmallIslands(terrain); + removeSmallLakes(terrain); + } const shorelineWaters = processShore(terrain); processDistToLand(shorelineWaters, terrain); processOcean(terrain); diff --git a/tests/Warship.test.ts b/tests/Warship.test.ts new file mode 100644 index 000000000..aa6d23b6e --- /dev/null +++ b/tests/Warship.test.ts @@ -0,0 +1,109 @@ +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../src/core/game/Game"; +import { SpawnExecution } from "../src/core/execution/SpawnExecution"; +import { setup } from "./util/Setup"; +import { constructionExecution } from "./util/utils"; + +let game: Game; +let player1: Player; +let player2: Player; + +describe("Warship", () => { + beforeEach(async () => { + game = await setup("half_land_half_ocean", { + infiniteGold: true, + instantBuild: true, + }); + const player_1_info = new PlayerInfo( + "us", + "boat dude", + PlayerType.Human, + null, + "player_1_id", + ); + game.addPlayer(player_1_info, 1000); + const player_2_info = new PlayerInfo( + "us", + "boat dude", + PlayerType.Human, + null, + "player_2_id", + ); + game.addPlayer(player_2_info, 1000); + + const spawnTile = game.map().ref(0, 0); + game.addExecution( + new SpawnExecution(game.player(player_1_info.id).info(), spawnTile), + new SpawnExecution(game.player(player_2_info.id).info(), spawnTile), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + player1 = game.player(player_1_info.id); + player2 = game.player(player_2_info.id); + }); + + test("Warship heals only if player has port", async () => { + const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth; + + const port = player1.buildUnit(UnitType.Port, 0, game.ref(0, 0)); + const warship = player1.buildUnit(UnitType.Warship, 0, game.ref(7, 7)); + + game.executeNextTick(); + + expect(warship.health()).toBe(maxHealth); + warship.modifyHealth(-10); + expect(warship.health()).toBe(maxHealth - 10); + game.executeNextTick(); + expect(warship.health()).toBe(maxHealth - 9); + + port.delete(); + + game.executeNextTick(); + expect(warship.health()).toBe(maxHealth - 9); + }); + + test("Warship captures trade if player has port", async () => { + constructionExecution(game, player1.id(), 0, 0, UnitType.Port); + constructionExecution(game, player1.id(), 7, 7, UnitType.Warship); + // Warship need one more tick (for warship exec to actually build warship) + game.executeNextTick(); + expect(player1.units(UnitType.Warship)).toHaveLength(1); + + // Cannot buildExec with trade ship as it's not buildable (but + // we can obviously directly add it to the player) + const tradeShip = player2.buildUnit(UnitType.TradeShip, 0, game.ref(6, 6)); + + expect(tradeShip.owner().id()).toBe(player2.id()); + // Let plenty of time for A* to execute + for (let i = 0; i < 10; i++) { + game.executeNextTick(); + } + expect(tradeShip.owner().id()).toBe(player1.id()); + }); + + test("Warship do not capture trade if player has no port", async () => { + constructionExecution(game, player1.id(), 0, 0, UnitType.Port); + constructionExecution(game, player1.id(), 7, 7, UnitType.Warship); + expect(player1.units(UnitType.Warship)).toHaveLength(1); + + player1.units(UnitType.Port)[0].delete(); + // Cannot buildExec with trade ship as it's not buildable (but + // we can obviously directly add it to the player) + const tradeShip = player2.buildUnit(UnitType.TradeShip, 0, game.ref(6, 6)); + + expect(tradeShip.owner().id()).toBe(player2.id()); + // Let plenty of time for A* to execute + for (let i = 0; i < 10; i++) { + game.executeNextTick(); + } + expect(tradeShip.owner().id()).toBe(player2.id()); + }); +}); diff --git a/tests/testdata/half_land_half_ocean.png b/tests/testdata/half_land_half_ocean.png new file mode 100755 index 000000000..059596dfa Binary files /dev/null and b/tests/testdata/half_land_half_ocean.png differ diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index 0dbe457b5..89724843c 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -7,12 +7,13 @@ import { TestConfig } from "./TestConfig"; import { TestServerConfig } from "./TestServerConfig"; import { UserSettings } from "../../src/core/game/UserSettings"; import { Difficulty, GameType } from "../../src/core/game/Game"; +import { GameConfig } from "../../src/core/Schemas"; -export async function setup(mapName: string) { +export async function setup(mapName: string, _gameConfig: GameConfig = {}) { // Load the specified map const mapPath = path.join(__dirname, "..", "testdata", `${mapName}.png`); const imageBuffer = await fs.readFile(mapPath); - const { map, miniMap } = await generateMap(imageBuffer); + const { map, miniMap } = await generateMap(imageBuffer, false); const gameMap = await genTerrainFromBin(String.fromCharCode.apply(null, map)); const miniGameMap = await genTerrainFromBin( String.fromCharCode.apply(null, miniMap), @@ -30,6 +31,7 @@ export async function setup(mapName: string) { infiniteGold: false, infiniteTroops: false, instantBuild: false, + ..._gameConfig, }; const config = new TestConfig(serverConfig, gameConfig, new UserSettings()); diff --git a/tests/util/utils.ts b/tests/util/utils.ts new file mode 100644 index 000000000..17ca1a447 --- /dev/null +++ b/tests/util/utils.ts @@ -0,0 +1,23 @@ +// Either someone can straight up call player.buildUnit. It's simpler and immediate (no tick required) +// Either someone can straight up call player.buildUnit. It's simpler and immediate (no tick required) +// However buildUnit do not create executions (e.g.: WarshipExecution) +// If you also need execution use function below. Does not work with things not + +import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution"; +import { Game, PlayerID, UnitType } from "../../src/core/game/Game"; + +// built via UI (e.g.: trade ships) +export function constructionExecution( + game: Game, + playerID: PlayerID, + x: number, + y: number, + unit: UnitType, +) { + game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit)); + // Init + game.executeNextTick(); + // Exec + game.executeNextTick(); + game.executeNextTick(); +}