mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-15 07:13:06 +00:00
Merge branch 'openfrontio:main' into main
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
name: 🚀 Deploy
|
||||
|
||||
on:
|
||||
# Allow contributors to schedule manual deployments.
|
||||
# Permission to deploy can be restricted by requiring approval in environment configuration.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_environment:
|
||||
description: "Deployment Environment"
|
||||
target_domain:
|
||||
description: "Deployment Domain"
|
||||
required: true
|
||||
default: "staging"
|
||||
default: "openfront.dev"
|
||||
type: choice
|
||||
options:
|
||||
- prod
|
||||
- staging
|
||||
- openfront.io
|
||||
- openfront.dev
|
||||
target_host:
|
||||
description: "Deployment Host"
|
||||
required: true
|
||||
@@ -25,53 +27,86 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
# Automatic deployment when pushing to main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
# Don't deploy on push if this is a fork
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.repository == 'openfrontio/OpenFrontIO' }}
|
||||
# Use different logic based on event type
|
||||
name: Deploy to ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}
|
||||
name: Deploy to ${{ inputs.target_subdomain && format('{0}.{1}', inputs.target_subdomain, inputs.target_domain) || inputs.target_domain }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}
|
||||
environment: ${{ inputs.target_subdomain && format('{0}.{1}', inputs.target_subdomain, inputs.target_domain) || inputs.target_domain }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update deployment status
|
||||
env:
|
||||
FQDN: ${{ inputs.target_subdomain && format('{0}.{1}', inputs.target_subdomain, inputs.target_domain) || inputs.target_domain }}
|
||||
run: |
|
||||
cat <<EOF >> $GITHUB_STEP_SUMMARY
|
||||
### In progress :ship:
|
||||
|
||||
Deploying from $GITHUB_REF to $FQDN
|
||||
EOF
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- run: |
|
||||
- name: Create SSH private key
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.SERVER_HOST_STAGING }} >> ~/.ssh/known_hosts
|
||||
- name: Deploy
|
||||
env:
|
||||
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
|
||||
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
DOMAIN: ${{ inputs.target_domain }}
|
||||
ENV: ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}
|
||||
HOST: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}
|
||||
MON_PASSWORD: ${{ secrets.MON_PASSWORD }}
|
||||
MON_USERNAME: ${{ secrets.MON_USERNAME }}
|
||||
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
|
||||
SERVER_HOST_EU: ${{ secrets.SERVER_HOST_EU }}
|
||||
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
|
||||
SERVER_HOST_US: ${{ secrets.SERVER_HOST_US }}
|
||||
SSH_KEY: ~/.ssh/id_rsa
|
||||
SUBDOMAIN: ${{ github.event_name == 'workflow_dispatch' && inputs.target_subdomain || 'main' }}
|
||||
VERSION_TAG: latest
|
||||
run: |
|
||||
echo "::group::deploy.sh"
|
||||
./deploy.sh "$ENV" "$HOST" "$SUBDOMAIN"
|
||||
echo "::endgroup::"
|
||||
- name: Update deployment status ✅
|
||||
if: success()
|
||||
env:
|
||||
FQDN: ${{ inputs.target_subdomain && format('{0}.{1}', inputs.target_subdomain, inputs.target_domain) || inputs.target_domain }}
|
||||
run: |
|
||||
cat <<EOF >> $GITHUB_STEP_SUMMARY
|
||||
### Success! :rocket:
|
||||
|
||||
# Determine environment based on trigger type
|
||||
TARGET_ENV="${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}"
|
||||
TARGET_HOST="${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}"
|
||||
TARGET_SUBDOMAIN="${{ github.event_name == 'workflow_dispatch' && inputs.target_subdomain || 'main' }}"
|
||||
|
||||
cat >.env.$TARGET_ENV <<EOF
|
||||
ADMIN_TOKEN=${{ secrets.ADMIN_TOKEN }}
|
||||
CF_ACCOUNT_ID=${{ secrets.CF_ACCOUNT_ID }}
|
||||
CF_API_TOKEN=${{ secrets.CF_API_TOKEN }}
|
||||
DOCKER_REPO=${{ vars.DOCKERHUB_REPO }}
|
||||
DOCKER_USERNAME=${{ vars.DOCKERHUB_USERNAME }}
|
||||
DOMAIN=${{ vars.DOMAIN }}
|
||||
MON_PASSWORD=${{ secrets.MON_PASSWORD }}
|
||||
MON_USERNAME=${{ secrets.MON_USERNAME }}
|
||||
R2_ACCESS_KEY=${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET=${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY=${{ secrets.R2_SECRET_KEY }}
|
||||
SERVER_HOST_STAGING=${{ secrets.SERVER_HOST_STAGING }}
|
||||
SERVER_HOST_US=${{ secrets.SERVER_HOST_US }}
|
||||
SERVER_HOST_EU=${{ secrets.SERVER_HOST_EU }}
|
||||
SSH_KEY=~/.ssh/id_rsa
|
||||
VERSION_TAG="latest"
|
||||
Deployed from $GITHUB_REF to $FQDN
|
||||
EOF
|
||||
- name: Update deployment status ❌
|
||||
if: failure()
|
||||
env:
|
||||
FQDN: ${{ inputs.target_subdomain && format('{0}.{1}', inputs.target_subdomain, inputs.target_domain) || inputs.target_domain }}
|
||||
run: |
|
||||
cat <<EOF >> $GITHUB_STEP_SUMMARY
|
||||
### Failure! :fire:
|
||||
|
||||
./deploy.sh $TARGET_ENV $TARGET_HOST $TARGET_SUBDOMAIN
|
||||
echo "Deployed to $TARGET_ENV environment on $TARGET_HOST host with subdomain $TARGET_SUBDOMAIN"
|
||||
Unable to deploy from $GITHUB_REF to $FQDN
|
||||
EOF
|
||||
|
||||
@@ -57,9 +57,6 @@ fi
|
||||
if [ -f .env.$ENV ]; then
|
||||
echo "Loading $ENV-specific configuration from .env.$ENV file..."
|
||||
export $(grep -v '^#' .env.$ENV | xargs)
|
||||
else
|
||||
echo "Error: Environment file .env.$ENV not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$HOST" == "staging" ]; then
|
||||
@@ -140,7 +137,6 @@ cat > $REMOTE_UPDATE_PATH/.env << 'EOL'
|
||||
GAME_ENV=$ENV
|
||||
ENV=$ENV
|
||||
HOST=$HOST
|
||||
SUBDOMAIN=$SUBDOMAIN
|
||||
DOCKER_IMAGE=$DOCKER_IMAGE
|
||||
DOCKER_TOKEN=$DOCKER_TOKEN
|
||||
ADMIN_TOKEN=$ADMIN_TOKEN
|
||||
@@ -165,4 +161,4 @@ fi
|
||||
print_header "DEPLOYMENT COMPLETED SUCCESSFULLY"
|
||||
echo "✅ New version deployed to ${ENV} environment in ${HOST} with subdomain ${SUBDOMAIN}!"
|
||||
echo "🌐 Check your server to verify the deployment."
|
||||
echo "======================================================="
|
||||
echo "======================================================="
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Япония и съседи",
|
||||
"betweentwoseas": "Между Две Морета",
|
||||
"knownworld": "Познат Свят",
|
||||
"faroeislands": "Фарьорски острови"
|
||||
"faroeislands": "Фарьорски острови",
|
||||
"europeclassic": "Европа (класическа)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Присъединяване към частна игра",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Japan and Neighbors",
|
||||
"betweentwoseas": "Between Two Seas",
|
||||
"knownworld": "Known World",
|
||||
"faroeislands": "Faroe Islands"
|
||||
"faroeislands": "Faroe Islands",
|
||||
"europeclassic": "Europe (classic)"
|
||||
},
|
||||
"map_categories": {
|
||||
"continental": "Continental",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Japanio kaj najbaroj",
|
||||
"betweentwoseas": "Inter du maroj",
|
||||
"knownworld": "Konata Mondo",
|
||||
"faroeislands": "Ferooj"
|
||||
"faroeislands": "Ferooj",
|
||||
"europeclassic": "Eŭropo (klasika)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Aliĝi al privata salono",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Japon et pays voisins",
|
||||
"betweentwoseas": "Entre deux mers",
|
||||
"knownworld": "Monde connu",
|
||||
"faroeislands": "Îles Féroé"
|
||||
"faroeislands": "Îles Féroé",
|
||||
"europeclassic": "Europe (classique)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Rejoindre un salon privé",
|
||||
|
||||
@@ -115,13 +115,14 @@
|
||||
"britannia": "ブリタニア",
|
||||
"gatewaytotheatlantic": "西ヨーロッパ",
|
||||
"australia": "オーストラリア",
|
||||
"random": "アイスランド",
|
||||
"random": "ランダム",
|
||||
"iceland": "アイスランド",
|
||||
"pangaea": "パンゲア",
|
||||
"japan": "日本とその隣国",
|
||||
"betweentwoseas": "2つの海の間",
|
||||
"knownworld": "知られてる世界",
|
||||
"faroeislands": "フェロー諸島"
|
||||
"faroeislands": "フェロー諸島",
|
||||
"europeclassic": "ヨーロッパ (クラシック)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "ランダム",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Japan en buren",
|
||||
"betweentwoseas": "Tussen twee zeeën",
|
||||
"knownworld": "Bekende Wereld",
|
||||
"faroeislands": "Faeröer eilanden"
|
||||
"faroeislands": "Faeröer eilanden",
|
||||
"europeclassic": "Europa (klassiek)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Privélobby toetreden",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Japonia i sąsiedzi",
|
||||
"betweentwoseas": "Między dwoma morzami",
|
||||
"knownworld": "Znany Świat",
|
||||
"faroeislands": "Wyspy Owcze"
|
||||
"faroeislands": "Wyspy Owcze",
|
||||
"europeclassic": ""
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Dołącz do prywatnego Lobby",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Япония и соседи",
|
||||
"betweentwoseas": "Между двух морей",
|
||||
"knownworld": "Известный мир",
|
||||
"faroeislands": "Фарерские острова"
|
||||
"faroeislands": "Фарерские острова",
|
||||
"europeclassic": "Европа (классическая)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Присоединиться к приватному лобби",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Japan i susjedi",
|
||||
"betweentwoseas": "Između dva mora",
|
||||
"knownworld": "Poznati svijet",
|
||||
"faroeislands": "Farska ostrva"
|
||||
"faroeislands": "Farska ostrva",
|
||||
"europeclassic": "Evropa (klasika)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Pridruži se privatnoj čekaonici",
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"japan": "Японія та сусіди",
|
||||
"betweentwoseas": "Поміж двох морів",
|
||||
"knownworld": "Відомий світ",
|
||||
"faroeislands": "Фарерські острови"
|
||||
"faroeislands": "Фарерські острови",
|
||||
"europeclassic": "Європа (класична)"
|
||||
},
|
||||
"private_lobby": {
|
||||
"title": "Приєднатися до приватного лобі",
|
||||
|
||||
@@ -194,6 +194,11 @@ label.option-card:hover {
|
||||
margin: 8px 0px 0px 0px;
|
||||
}
|
||||
|
||||
#lobbyIdInput {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lobby-id-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -671,7 +671,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
structureMinDist(): number {
|
||||
return 18;
|
||||
return 12;
|
||||
}
|
||||
|
||||
shellLifetime(): number {
|
||||
|
||||
@@ -21,13 +21,6 @@ export class AttackExecution implements Execution {
|
||||
private active: boolean = true;
|
||||
private toConquer: PriorityQueue<TileContainer> =
|
||||
new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
|
||||
if (a.priority == b.priority) {
|
||||
if (a.tick == b.tick) {
|
||||
return 0;
|
||||
// return this.random.nextInt(-1, 1)
|
||||
}
|
||||
return a.tick - b.tick;
|
||||
}
|
||||
return a.priority - b.priority;
|
||||
});
|
||||
private random = new PseudoRandom(123);
|
||||
@@ -227,8 +220,6 @@ export class AttackExecution implements Execution {
|
||||
this.target,
|
||||
this.border.size + this.random.nextInt(0, 5),
|
||||
);
|
||||
// consolex.log(`num tiles per tick: ${numTilesPerTick}`)
|
||||
// consolex.log(`num execs: ${this.mg.executions().length}`)
|
||||
|
||||
while (numTilesPerTick > 0) {
|
||||
if (this.attack.troops() < 1) {
|
||||
@@ -279,13 +270,9 @@ export class AttackExecution implements Execution {
|
||||
continue;
|
||||
}
|
||||
this.border.add(neighbor);
|
||||
let numOwnedByMe = this.mg
|
||||
const numOwnedByMe = this.mg
|
||||
.neighbors(neighbor)
|
||||
.filter((t) => this.mg.owner(t) == this._owner).length;
|
||||
const dist = 0;
|
||||
if (numOwnedByMe > 2) {
|
||||
numOwnedByMe = 10;
|
||||
}
|
||||
let mag = 0;
|
||||
switch (this.mg.terrainType(tile)) {
|
||||
case TerrainType.Plains:
|
||||
@@ -301,8 +288,9 @@ export class AttackExecution implements Execution {
|
||||
this.toConquer.enqueue(
|
||||
new TileContainer(
|
||||
neighbor,
|
||||
dist / 100 + this.random.nextInt(0, 2) - numOwnedByMe + mag,
|
||||
this.mg.ticks(),
|
||||
(this.random.nextInt(0, 7) + 10) *
|
||||
(1 - numOwnedByMe * 0.5 + mag / 2) +
|
||||
this.mg.ticks(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -355,6 +343,5 @@ class TileContainer {
|
||||
constructor(
|
||||
public readonly tile: TileRef,
|
||||
public readonly priority: number,
|
||||
public readonly tick: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,21 @@ import { BotBehavior } from "./utils/BotBehavior";
|
||||
export class BotExecution implements Execution {
|
||||
private active = true;
|
||||
private random: PseudoRandom;
|
||||
private attackRate: number;
|
||||
private mg: Game;
|
||||
private neighborsTerraNullius = true;
|
||||
|
||||
private behavior: BotBehavior | null = null;
|
||||
private attackRate: number;
|
||||
private attackTick: number;
|
||||
private triggerRatio: number;
|
||||
private reserveRatio: number;
|
||||
|
||||
constructor(private bot: Player) {
|
||||
this.random = new PseudoRandom(simpleHash(bot.id()));
|
||||
this.attackRate = this.random.nextInt(10, 50);
|
||||
this.attackRate = this.random.nextInt(40, 80);
|
||||
this.attackTick = this.random.nextInt(0, this.attackRate);
|
||||
this.triggerRatio = this.random.nextInt(60, 90) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 60) / 100;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
@@ -27,17 +33,21 @@ export class BotExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % this.attackRate != this.attackTick) return;
|
||||
|
||||
if (!this.bot.isAlive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticks % this.attackRate != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.behavior === null) {
|
||||
this.behavior = new BotBehavior(this.random, this.mg, this.bot, 1 / 20);
|
||||
this.behavior = new BotBehavior(
|
||||
this.random,
|
||||
this.mg,
|
||||
this.bot,
|
||||
this.triggerRatio,
|
||||
this.reserveRatio,
|
||||
);
|
||||
}
|
||||
|
||||
this.behavior.handleAllianceRequests();
|
||||
@@ -65,15 +75,14 @@ export class BotExecution implements Execution {
|
||||
this.neighborsTerraNullius = false;
|
||||
}
|
||||
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.checkIncomingAttacks();
|
||||
const enemy = this.behavior.selectRandomEnemy();
|
||||
if (!enemy) return;
|
||||
if (!this.bot.sharesBorderWith(enemy)) return;
|
||||
this.behavior.sendAttack(enemy);
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.bot;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ export class DefensePostExecution implements Execution {
|
||||
this.target = null;
|
||||
}
|
||||
|
||||
// TODO: Reconsider how/if defense posts target ships.
|
||||
return;
|
||||
|
||||
const ships = this.mg
|
||||
.nearbyUnits(
|
||||
this.post.tile(),
|
||||
|
||||
@@ -35,6 +35,11 @@ export class FakeHumanExecution implements Execution {
|
||||
private mg: Game;
|
||||
private player: Player = null;
|
||||
|
||||
private attackRate: number;
|
||||
private attackTick: number;
|
||||
private triggerRatio: number;
|
||||
private reserveRatio: number;
|
||||
|
||||
private lastEmojiSent = new Map<Player, Tick>();
|
||||
private lastNukeSent: [Tick, TileRef][] = [];
|
||||
private embargoMalusApplied = new Set<PlayerID>();
|
||||
@@ -46,6 +51,10 @@ export class FakeHumanExecution implements Execution {
|
||||
this.random = new PseudoRandom(
|
||||
simpleHash(playerInfo.id) + simpleHash(gameID),
|
||||
);
|
||||
this.attackRate = this.random.nextInt(40, 80);
|
||||
this.attackTick = this.random.nextInt(0, this.attackRate);
|
||||
this.triggerRatio = this.random.nextInt(60, 90) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 60) / 100;
|
||||
}
|
||||
|
||||
init(mg: Game) {
|
||||
@@ -96,17 +105,18 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % this.attackRate != this.attackTick) return;
|
||||
|
||||
if (this.mg.inSpawnPhase()) {
|
||||
if (ticks % this.random.nextInt(5, 30) == 0) {
|
||||
const rl = this.randomLand();
|
||||
if (rl == null) {
|
||||
consolex.warn(`cannot spawn ${this.playerInfo.name}`);
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(new SpawnExecution(this.playerInfo, rl));
|
||||
const rl = this.randomLand();
|
||||
if (rl == null) {
|
||||
consolex.warn(`cannot spawn ${this.playerInfo.name}`);
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(new SpawnExecution(this.playerInfo, rl));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player == null) {
|
||||
this.player = this.mg.players().find((p) => p.id() == this.playerInfo.id);
|
||||
if (this.player == null) {
|
||||
@@ -121,7 +131,13 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
if (this.behavior === null) {
|
||||
// Player is unavailable during init()
|
||||
this.behavior = new BotBehavior(this.random, this.mg, this.player, 1 / 5);
|
||||
this.behavior = new BotBehavior(
|
||||
this.random,
|
||||
this.mg,
|
||||
this.player,
|
||||
this.triggerRatio,
|
||||
this.reserveRatio,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.firstMove) {
|
||||
@@ -130,10 +146,6 @@ export class FakeHumanExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticks % this.random.nextInt(40, 80) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.player.troops() > 100_000 &&
|
||||
this.player.targetTroopRatio() > 0.7
|
||||
@@ -146,7 +158,10 @@ export class FakeHumanExecution implements Execution {
|
||||
this.handleEnemies();
|
||||
this.handleUnits();
|
||||
this.handleEmbargoesToHostileNations();
|
||||
this.maybeAttack();
|
||||
}
|
||||
|
||||
private maybeAttack() {
|
||||
const enemyborder = Array.from(this.player.borderTiles())
|
||||
.flatMap((t) => this.mg.neighbors(t))
|
||||
.filter(
|
||||
@@ -174,9 +189,9 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
const enemies = enemiesWithTN
|
||||
.filter((o) => o.isPlayer())
|
||||
.map((o) => o as Player)
|
||||
.sort((a, b) => a.troops() - b.troops());
|
||||
|
||||
// 5% chance to send a random alliance request
|
||||
if (this.random.chance(20)) {
|
||||
const toAlly = this.random.randElement(enemies);
|
||||
if (this.player.canSendAllianceRequest(toAlly)) {
|
||||
@@ -211,7 +226,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
shouldDiscourageAttack(other: Player) {
|
||||
private shouldDiscourageAttack(other: Player) {
|
||||
if (other.isTraitor()) {
|
||||
return false;
|
||||
}
|
||||
@@ -227,6 +242,8 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
handleEnemies() {
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.checkIncomingAttacks();
|
||||
this.behavior.assistAllies();
|
||||
const enemy = this.behavior.selectEnemy();
|
||||
if (!enemy) return;
|
||||
@@ -257,8 +274,7 @@ export class FakeHumanExecution implements Execution {
|
||||
const silos = this.player.units(UnitType.MissileSilo);
|
||||
if (
|
||||
silos.length == 0 ||
|
||||
this.player.gold() <
|
||||
this.mg.config().unitInfo(UnitType.AtomBomb).cost(this.player) ||
|
||||
this.player.gold() < this.cost(UnitType.AtomBomb) ||
|
||||
other.type() == PlayerType.Bot ||
|
||||
this.player.isOnSameTeam(other)
|
||||
) {
|
||||
@@ -397,36 +413,21 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.maybeSpawnStructure(
|
||||
UnitType.City,
|
||||
2,
|
||||
(t) => new ConstructionExecution(this.player.id(), t, UnitType.City),
|
||||
);
|
||||
this.maybeSpawnStructure(UnitType.City, 2);
|
||||
if (this.maybeSpawnWarship()) {
|
||||
return;
|
||||
}
|
||||
if (!this.mg.config().disableNukes()) {
|
||||
this.maybeSpawnStructure(
|
||||
UnitType.MissileSilo,
|
||||
1,
|
||||
(t) =>
|
||||
new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo),
|
||||
);
|
||||
this.maybeSpawnStructure(UnitType.MissileSilo, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private maybeSpawnStructure(
|
||||
type: UnitType,
|
||||
maxNum: number,
|
||||
build: (tile: TileRef) => Execution,
|
||||
) {
|
||||
private maybeSpawnStructure(type: UnitType, maxNum: number) {
|
||||
const units = this.player.units(type);
|
||||
if (units.length >= maxNum) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.player.gold() < this.mg.config().unitInfo(type).cost(this.player)
|
||||
) {
|
||||
if (this.player.gold() < this.cost(type)) {
|
||||
return;
|
||||
}
|
||||
const tile = this.randTerritoryTile(this.player);
|
||||
@@ -437,7 +438,9 @@ export class FakeHumanExecution implements Execution {
|
||||
if (canBuild == false) {
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(build(tile));
|
||||
this.mg.addExecution(
|
||||
new ConstructionExecution(this.player.id(), tile, type),
|
||||
);
|
||||
}
|
||||
|
||||
private maybeSpawnWarship(): boolean {
|
||||
|
||||
@@ -17,9 +17,6 @@ export class SAMLauncherExecution implements Execution {
|
||||
private mg: Game;
|
||||
private active: boolean = true;
|
||||
|
||||
private target: Unit = null;
|
||||
private warheadTargets: Unit[] = [];
|
||||
|
||||
private searchRangeRadius = 80;
|
||||
// As MIRV go very fast we have to detect them very early but we only
|
||||
// shoot the one targeting very close (MIRVWarheadProtectionRadius)
|
||||
@@ -119,7 +116,7 @@ export class SAMLauncherExecution implements Execution {
|
||||
this.pseudoRandom = new PseudoRandom(this.sam.id());
|
||||
}
|
||||
|
||||
this.warheadTargets = this.mg
|
||||
const mirvWarheadTargets = this.mg
|
||||
.nearbyUnits(
|
||||
this.sam.tile(),
|
||||
this.MIRVWarheadSearchRadius,
|
||||
@@ -136,8 +133,9 @@ export class SAMLauncherExecution implements Execution {
|
||||
this.MIRVWarheadProtectionRadius,
|
||||
);
|
||||
|
||||
if (this.warheadTargets.length == 0) {
|
||||
this.target = this.getSingleTarget();
|
||||
let target: Unit | null = null;
|
||||
if (mirvWarheadTargets.length == 0) {
|
||||
target = this.getSingleTarget();
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -147,16 +145,14 @@ export class SAMLauncherExecution implements Execution {
|
||||
this.sam.setCooldown(false);
|
||||
}
|
||||
|
||||
const isSingleTarget = this.target && !this.target.targetedBySAM();
|
||||
const isSingleTarget = target && !target.targetedBySAM();
|
||||
if (
|
||||
(isSingleTarget || this.warheadTargets.length > 0) &&
|
||||
(isSingleTarget || mirvWarheadTargets.length > 0) &&
|
||||
!this.sam.isCooldown()
|
||||
) {
|
||||
this.sam.setCooldown(true);
|
||||
const type =
|
||||
this.warheadTargets.length > 0
|
||||
? UnitType.MIRVWarhead
|
||||
: this.target.type();
|
||||
mirvWarheadTargets.length > 0 ? UnitType.MIRVWarhead : target.type();
|
||||
const random = this.pseudoRandom.next();
|
||||
const hit = this.isHit(type, random);
|
||||
if (!hit) {
|
||||
@@ -166,26 +162,25 @@ export class SAMLauncherExecution implements Execution {
|
||||
this.sam.owner().id(),
|
||||
);
|
||||
} else {
|
||||
if (this.warheadTargets.length > 0) {
|
||||
if (mirvWarheadTargets.length > 0) {
|
||||
// Message
|
||||
this.mg.displayMessage(
|
||||
`${this.warheadTargets.length} MIRV warheads intercepted`,
|
||||
`${mirvWarheadTargets.length} MIRV warheads intercepted`,
|
||||
MessageType.SUCCESS,
|
||||
this.sam.owner().id(),
|
||||
);
|
||||
// Delete warheads
|
||||
this.warheadTargets.forEach((u) => u.delete());
|
||||
mirvWarheadTargets.forEach((u) => u.delete());
|
||||
} else {
|
||||
this.target.setTargetedBySAM(true);
|
||||
target.setTargetedBySAM(true);
|
||||
this.mg.addExecution(
|
||||
new SAMMissileExecution(
|
||||
this.sam.tile(),
|
||||
this.sam.owner(),
|
||||
this.sam,
|
||||
this.target,
|
||||
target,
|
||||
),
|
||||
);
|
||||
this.warheadTargets = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ export class BotBehavior {
|
||||
private random: PseudoRandom,
|
||||
private game: Game,
|
||||
private player: Player,
|
||||
private attackRatio: number,
|
||||
private triggerRatio: number,
|
||||
private reserveRatio: number,
|
||||
) {}
|
||||
|
||||
handleAllianceRequests() {
|
||||
@@ -39,6 +40,24 @@ export class BotBehavior {
|
||||
);
|
||||
}
|
||||
|
||||
forgetOldEnemies() {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
this.enemy = null;
|
||||
}
|
||||
}
|
||||
|
||||
checkIncomingAttacks() {
|
||||
// Switch enemies if we're under attack
|
||||
const incomingAttacks = this.player.incomingAttacks();
|
||||
if (incomingAttacks.length > 0) {
|
||||
this.enemy = incomingAttacks
|
||||
.sort((a, b) => b.troops() - a.troops())[0]
|
||||
.attacker();
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
}
|
||||
|
||||
assistAllies() {
|
||||
outer: for (const ally of this.player.allies()) {
|
||||
if (ally.targets().length === 0) continue;
|
||||
@@ -66,9 +85,11 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
selectEnemy(): Player | null {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
this.enemy = null;
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
if (ratio < this.triggerRatio) return null;
|
||||
}
|
||||
|
||||
// Prefer neighboring bots
|
||||
@@ -100,24 +121,54 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
selectRandomEnemy(): Player | TerraNullius | null {
|
||||
const neighbors = this.player.neighbors();
|
||||
for (const neighbor of this.random.shuffleArray(neighbors)) {
|
||||
if (neighbor.isPlayer()) {
|
||||
if (this.player.isFriendly(neighbor)) continue;
|
||||
if (neighbor.type() == PlayerType.FakeHuman) {
|
||||
if (this.random.chance(2)) {
|
||||
continue;
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
if (ratio < this.triggerRatio) return null;
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const neighbors = this.player.neighbors();
|
||||
for (const neighbor of this.random.shuffleArray(neighbors)) {
|
||||
if (neighbor.isPlayer()) {
|
||||
if (this.player.isFriendly(neighbor)) continue;
|
||||
if (neighbor.type() == PlayerType.FakeHuman) {
|
||||
if (this.random.chance(2)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.enemy = neighbor;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
|
||||
// Select a traitor as an enemy
|
||||
const traitors = this.player
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
|
||||
if (traitors.length > 0) {
|
||||
const toAttack = this.random.randElement(traitors);
|
||||
const odds = this.player.isFriendly(toAttack) ? 6 : 3;
|
||||
if (this.random.chance(odds)) {
|
||||
this.enemy = toAttack;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
}
|
||||
return neighbor;
|
||||
}
|
||||
return null;
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
if (this.enemy && this.player.isFriendly(this.enemy)) {
|
||||
this.enemy = null;
|
||||
}
|
||||
return this.enemy;
|
||||
}
|
||||
|
||||
sendAttack(target: Player | TerraNullius) {
|
||||
if (target.isPlayer() && this.player.isOnSameTeam(target)) return;
|
||||
const troops = this.player.troops() * this.attackRatio;
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const maxTroops = maxPop * this.player.targetTroopRatio();
|
||||
const targetTroops = maxTroops * this.reserveRatio;
|
||||
const troops = this.player.troops() - targetTroops;
|
||||
if (troops < 1) return;
|
||||
this.game.addExecution(
|
||||
new AttackExecution(
|
||||
|
||||
@@ -85,7 +85,6 @@ export class GameImpl implements Game {
|
||||
nationMap: NationMap,
|
||||
private _config: Config,
|
||||
) {
|
||||
this.addHumans();
|
||||
this._terraNullius = new TerraNulliusImpl();
|
||||
this._width = _map.width();
|
||||
this._height = _map.height();
|
||||
@@ -112,6 +111,7 @@ export class GameImpl implements Game {
|
||||
if (numPlayerTeams >= 8)
|
||||
throw new Error(`Too many teams: ${numPlayerTeams}`);
|
||||
}
|
||||
this.addHumans();
|
||||
}
|
||||
|
||||
private addHumans() {
|
||||
|
||||
@@ -469,12 +469,12 @@ export class PlayerImpl implements Player {
|
||||
this.mg.target(this, other);
|
||||
}
|
||||
|
||||
targets(): PlayerImpl[] {
|
||||
targets(): Player[] {
|
||||
return this.targets_
|
||||
.filter(
|
||||
(t) => this.mg.ticks() - t.tick < this.mg.config().targetDuration(),
|
||||
)
|
||||
.map((t) => t.target as PlayerImpl);
|
||||
.map((t) => t.target);
|
||||
}
|
||||
|
||||
transitiveTargets(): Player[] {
|
||||
@@ -809,7 +809,6 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
// only get missilesilos that are not on cooldown
|
||||
const spawns = this.units(UnitType.MissileSilo)
|
||||
.map((u) => u as Unit)
|
||||
.filter((silo) => {
|
||||
return !silo.isCooldown();
|
||||
})
|
||||
|
||||
@@ -88,9 +88,9 @@ export class UnitImpl implements Unit {
|
||||
if (tile == null) {
|
||||
throw new Error("tile cannot be null");
|
||||
}
|
||||
this.mg.removeUnit(this);
|
||||
this._lastTile = this._tile;
|
||||
this._tile = tile;
|
||||
this.mg.removeUnit(this);
|
||||
this.mg.addUnit(this);
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user