Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ Traditional Chinese, Simplified Chinese and English, switchable inside the app.
| System | Summary |
| --- | --- |
| Map | Pointy-top hexes, axial maths, odd-r storage. The world map is 120×76 and wraps east–west. It is deliberately stretched — Europe and East Asia wide, the oceans narrow — but the geography stays true: which countries share a border and which are separated by sea is checked every time the map is generated. |
| Terrain | 13 types, each with a movement cost, a defence bonus and an income. Vehicles cannot enter mountains, jungle or swamp. |
| Terrain | 13 types, each with a movement cost, an income, and a penalty on tanks and guns attacking into it. Vehicles cannot enter mountains, jungle or swamp. |
| Territory | Ownership is per **province**, not per hex; each province is one city and its land. A city falls when its defence is worn down and a land unit stands in it, and the whole province changes hands with it. Hong Kong, Gibraltar and Belfast are single-hex strongholds. |
| Units | 17 kinds: 11 land, 6 sea. Each has four attack values — against infantry, armour, ships and aircraft — so the counter matters more than the raw number. Units are built as formations of one to four. |
| Air power | Not units on the board but missions you pay for: fighter strikes, bomber strikes (which can also hit an empty city's walls) and airdrops, flown from cities and carriers (bombers need a large city). Anti-air, cruisers and destroyers blunt strikes near them. |
| Combat | Ratio-based (`55·A/(A+D)`), bounded by construction, ±8% deterministic variance. Artillery fires from range and takes no return fire; it is nearly helpless in melee. A unit inside a city takes half the damage, and the city's walls take a further 70% of the blow. |
| Combat | Each attack rolls between the unit's minimum and maximum attack; each point of defence adds 1.6% to the divisor (`dmg = atk·1000/(def·16+1000)`). HP is absolute and grows with formation and level; below half HP a unit hits at half strength. Survivors always return fire at full strength. Artillery fires from range and takes no return fire, and cannot fire back in melee. A unit inside a city with walls takes half the damage, and the walls take their own share. Rules follow `docs/original-behavior.md`. |
| Zones of control | Moving next to an enemy ends your move. Armour ignores this once per move, which is what makes it the tool for opening a breach. |
| Supply | Spreads from your own cities along your own territory, priced in movement cost — so mountains break a supply line faster than plains do. Out of supply means losing strength, then dying. Supply trucks and headquarters carry a small supply bubble with them, even into enemy land. Ships carry their own stores: away from port they weaken to four fifths of their strength but never starve. |
| Experience | Units level 1→5 from damage dealt and kills. |
Expand Down
4 changes: 2 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ Android 上的六角格回合制大戰略遊戲。自由軟體,沒有廣告、
| 系統 | 說明 |
| --- | --- |
| 地圖 | 尖頂六角格,軸向座標做數學、奇數列偏移做儲存。世界地圖 120×76,東西環繞。地圖刻意變形 —— 歐洲與東亞放大、大洋壓扁 —— 但地理關係是真的:哪些國家接壤、哪些之間隔著海,每次產生地圖時都會檢查。 |
| 地形 | 13 種,各有移動成本、防禦加成與產值。輪車與履帶進不了山地、叢林與沼澤。 |
| 地形 | 13 種,各有移動成本、產值,以及對打進來的戰車與火炮的傷害懲罰。輪車與履帶進不了山地、叢林與沼澤。 |
| 領土 | 歸屬算在**省**上而不是格上,一省就是一座城與它的土地。城防被打光、陸軍站進城裡,這座城就陷落,整省跟著易主。香港、直布羅陀、貝爾法斯特是只佔一格的據點。 |
| 兵種 | 17 種:陸軍 11、海軍 6。每種都有四個獨立的攻擊值 —— 對步兵、對裝甲、對艦艇、對空 —— 所以「派誰去」比「數字多大」更重要。部隊建造時就決定編制,一到四個。 |
| 空軍 | 不是棋盤上的棋子,而是花錢買的任務:戰鬥機打擊、轟炸機打擊(也能轟炸無人城市的城防)與空降,從城市與航艦起飛(轟炸機要大城市)。附近的防空、巡洋艦、驅逐艦會削弱打擊。 |
| 戰鬥 | 攻防比公式(`55·A/(A+D)`),天然有界,加上 ±8% 的決定性亂數。火炮遠射不被反擊,但被貼身幾乎無法自衛。城裡的部隊只受一半傷害,城防另外承受這一擊的 70%。 |
| 戰鬥 | 每次攻擊在最小與最大攻擊之間擲骰;每 1 點防禦讓分母多 1.6%(`傷害 = 攻·1000/(防·16+1000)`)。HP 是絕對值,隨編制與等級增加;不到半血攻擊減半。活下來的守方一定全力反擊。火炮遠射不被反擊,被貼身也還不了手。城防還在時城裡的部隊只受一半傷害,城防另外承受一份。規則見 `docs/original-behavior.md`。 |
| 控制區 | 走到敵人旁邊就必須停下。裝甲部隊每次移動可以無視一次,這正是它作為突破口的價值。 |
| 補給 | 從自己的城市沿自己的領土擴散,用移動成本計價 —— 所以越過山脈的攻勢會比沿平原推進更快斷補。斷補給會先掉戰力,最後餓死。補給車與司令部自帶一個小型補給圈,在敵境一樣有效。軍艦自帶物資:離港會變弱到八成戰力,但不會餓死。 |
| 經驗 | 部隊靠打出的傷害與擊殺從 1 級升到 5 級。 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,9 @@ class AiPlayer(private val session: Session, private val nationId: Int) {
AirOps.collectTargets(session, nationId, mission, airTargets)
for (tile in airTargets) {
val defender = AirOps.strikeTarget(session, nationId, mission, tile) ?: continue
val share = defender.scaledDamage(AirOps.previewDamage(session, nationId, mission, defender))
var value = share * defender.kind.cost * defender.size / 100f
if (share >= defender.hp) value += defender.kind.cost * defender.size * 0.5f
val dealt = AirOps.previewDamage(session, nationId, mission, defender)
var value = worth(defender, dealt)
if (dealt >= defender.hp) value += defender.kind.cost * defender.size * 0.5f
val ratio = value / mission.totalCost
if (ratio > bestRatio) {
bestRatio = ratio
Expand Down Expand Up @@ -290,7 +290,13 @@ class AiPlayer(private val session: Session, private val nationId: Int) {
// 因為拆城防不會直接減少對方的戰力,只是為佔領開路。
val pid = Orders.cityTargetAt(session, unit, tile)
if (pid < 0) continue
val score = session.cityHp[pid].coerceAtMost(30) * 0.4f
val wall = Combat.previewCityStrike(
unit,
nation.techBonus(unit.kind.branch),
session.commandAura(unit),
session.moraleOf(unit)
)
val score = wall.coerceAtMost(session.cityHp[pid]).coerceAtMost(CITY_STRIKE_SCORE_CAP) * 0.4f
if (score > bestScore) {
bestScore = score
bestTile = tile
Expand All @@ -299,20 +305,15 @@ class AiPlayer(private val session: Session, private val nationId: Int) {
}
val ctx = Orders.buildContext(session, unit, defender)
val dealt = Combat.previewDamage(ctx)
val taken = if (Combat.canRetaliate(ctx)) {
Orders.previewDamage(session, defender, unit) * 6 / 10
} else 0
// 反擊跟主動攻擊同一條公式,而且看的是挨打之後的殘血。
val taken = Combat.previewRetaliation(ctx, dealt)

// 價值換算成錢:打掉一支貴的部隊比打掉一支便宜的值錢,
// 而自己的損失也用同一把尺量,AI 才不會拿戰列艦去換運輸船。
// 預測傷害是原始值,要先換算成對方實際會掉的成數再比。一個四編制的
// 部隊值四支的錢,也要四支的傷害才打得死。
val dealtShare = defender.scaledDamage(dealt)
val takenShare = unit.scaledDamage(taken)
var score = dealtShare * defender.kind.cost * defender.size / 100f -
takenShare * unit.kind.cost * unit.size / 100f
if (dealtShare >= defender.hp) score += defender.kind.cost * defender.size * 0.5f
if (takenShare >= unit.hp) score -= unit.kind.cost * unit.size * 0.8f
// 一個四編制的部隊值四支的錢,也要夠多的傷害才打得死。
var score = worth(defender, dealt) - worth(unit, taken)
if (dealt >= defender.hp) score += defender.kind.cost * defender.size * 0.5f
if (taken >= unit.hp) score -= unit.kind.cost * unit.size * 0.8f
// 守著城市的敵人優先清掉,那是勝利條件所在。
if (session.map.provinceAt(defender.tile)?.capitalTile == defender.tile) score *= 1.3f

Expand All @@ -331,6 +332,13 @@ class AiPlayer(private val session: Session, private val nationId: Int) {
* 目標選擇。順序就是優先級:
* 先救自己快掉的城,再打對面最近的城,都沒有的話往最近的敵人靠。
*/
/** 打掉 [damage] 點 HP 值多少錢:依打掉的比例換算成這支部隊的造價。 */
private fun worth(unit: ArmyUnit, damage: Int): Float {
if (unit.maxHp <= 0) return 0f
val share = damage.coerceAtMost(unit.hp).toFloat() / unit.maxHp
return share * unit.kind.cost * unit.size
}

private fun chooseGoal(unit: ArmyUnit): Int {
if (profile == AiProfile.TURTLE) {
threatenedOwnCity(unit)?.let { return it }
Expand Down Expand Up @@ -411,8 +419,9 @@ class AiPlayer(private val session: Session, private val nationId: Int) {

for (tile in reachable) {
var score = (currentDistance - map.distance(tile, goal)) * 10f
// 同樣的推進距離下,挑防禦地形好的那一格。
score += map.terrainAt(tile).defenceBonus * 0.25f
// 同樣的推進距離下,挑戰車與火炮難打的地形。
val terrain = map.terrainAt(tile)
score += (terrain.armourPenalty + terrain.artilleryPenalty) * 0.25f
// 別走出補給範圍。軍艦自帶物資,離港只是慢慢變弱,罰得輕得多。
if (!session.isSupplied(tile)) {
score -= if (unit.kind.domain == Domain.SEA) 3f else 12f
Expand Down Expand Up @@ -447,6 +456,9 @@ class AiPlayer(private val session: Session, private val nationId: Int) {
const val AIR_RESERVE = 250
const val MAX_SORTIES_PER_TURN = 3

/** 拆城防的分數上限(以一擊打掉的城防點數計),別讓 AI 光顧著拆牆。 */
const val CITY_STRIKE_SCORE_CAP = 30

/** 預期戰果至少要值任務價錢的這個比例才飛。 */
const val MIN_SORTIE_VALUE_RATIO = 0.75f
const val MAX_GOAL_DISTANCE = 28
Expand Down
Loading
Loading