-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Fix/double tap full send #4397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/double tap full send #4397
Changes from all commits
f4c07cb
799ee08
18ffe46
4275211
100aa73
90f2375
2ef3c05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -359,7 +359,10 @@ export const AllianceExtensionIntentSchema = z.object({ | |
| export const AttackIntentSchema = z.object({ | ||
| type: z.literal("attack"), | ||
| targetID: ID.nullable(), | ||
| troops: z.number().nonnegative().nullable(), | ||
| troopCount: z.number().nonnegative(), | ||
| troopRatio: z.number().gt(0).max(1), | ||
| // maxTroopSent present only for retaliation. | ||
| maxTroopSent: z.number().nonnegative().optional(), | ||
|
Comment on lines
+362
to
+365
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor 🧩 Analysis chain🏁 Script executed: # First, find and inspect the relevant file
fd "Schemas.ts" src/core/
# Then read the specific lines mentioned in the review
sed -n '360,380p' src/core/Schemas.tsRepository: openfrontio/OpenFrontIO Length of output: 881 🏁 Script executed: # Check if there are other numeric troop/unit count fields in the schemas file
rg "troopCount|maxTroopSent|soldierCount|unitCount" src/core/Schemas.ts -n -A 1 -B 1
# Also search for similar patterns in other intent schemas to see if there's a pattern
rg "z\.number\(\)\.nonnegative" src/core/Schemas.ts -nRepository: openfrontio/OpenFrontIO Length of output: 851 🏁 Script executed: # Let's search for any existing use of .int() in the schemas to see if it's used elsewhere
rg "\.int\(\)" src/core/Schemas.ts -n
# Also check the imports to understand what version of Zod is being used
head -30 src/core/Schemas.tsRepository: openfrontio/OpenFrontIO Length of output: 1311 🏁 Script executed: # Check the context around the troop intent schemas to understand their purpose
sed -n '355,380p' src/core/Schemas.ts
# Also search for where troopCount is used to see if it's ever expected to be fractional
rg "troopCount" src/core/ -t ts --no-heading -B 2 -A 2 | head -50Repository: openfrontio/OpenFrontIO Length of output: 3645 🏁 Script executed: # Let's check how these values are actually used in the execution to confirm they should be integers
sed -n '1,50p' src/core/execution/ExecutionManager.tsRepository: openfrontio/OpenFrontIO Length of output: 2469 🏁 Script executed: # Let's verify the actual calculation to see if fractional troop counts would make sense
sed -n '100,120p' src/core/execution/ExecutionManager.ts | grep -A 30 "troopCount"
# Also check if there are any tests for these intent schemas
fd ".*test.*" src/core/ -t f | head -20Repository: openfrontio/OpenFrontIO Length of output: 578 🏁 Script executed: # Let's look at the full context of how the intent is processed to understand data flow
sed -n '140,180p' src/core/execution/ExecutionManager.tsRepository: openfrontio/OpenFrontIO Length of output: 1611 🏁 Script executed: # Let's find the attack case handler
sed -n '100,150p' src/core/execution/ExecutionManager.ts | grep -B 20 -A 20 "case \"attack\""Repository: openfrontio/OpenFrontIO Length of output: 940 🏁 Script executed: # Check if there are any other usages of Math.floor on troopCount calculations
rg "Math\.floor.*troopCount|troopCount.*Math\.floor" src/core/ -t tsRepository: openfrontio/OpenFrontIO Length of output: 281 🏁 Script executed: # Let's check if there's any documentation or comments about why troopCount might be fractional
rg "troopCount|troop.*count" src/core/Schemas.ts -B 3 -A 3
# Also search for any boat attack intent usage
rg "BoatAttackIntent" src/core/ -t ts -B 2 -A 2 | head -40Repository: openfrontio/OpenFrontIO Length of output: 1747 Validate troop counts as safe integers in attack intents. Troop counts are discrete units, not fractional quantities. Proposed schema changes troopCount: z.number().nonnegative(),
+ troopCount: z.number().int().nonnegative(),
troopRatio: z.number().gt(0).max(1),
// maxTroopSent present only for retaliation.
maxTroopSent: z.number().nonnegative().optional(),
+ maxTroopSent: z.number().int().nonnegative().optional(),Apply the same change to BoatAttackIntentSchema (line 375): troopCount: z.number().nonnegative(),
+ troopCount: z.number().int().nonnegative(),🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| export const SpawnIntentSchema = z.object({ | ||
|
|
@@ -369,7 +372,8 @@ export const SpawnIntentSchema = z.object({ | |
|
|
||
| export const BoatAttackIntentSchema = z.object({ | ||
| type: z.literal("boat"), | ||
| troops: z.number().nonnegative(), | ||
| troopCount: z.number().nonnegative(), | ||
| troopRatio: z.number().gt(0).max(1), | ||
| dst: z.number(), | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,11 +42,57 @@ export class Executor { | |
| this.random = new PseudoRandom(simpleHash(gameID) + 1); | ||
| } | ||
|
|
||
| private computeRatio( | ||
| remainingTroopRatio: number, | ||
| totalRatioUsage: number, | ||
| ): number { | ||
| return (1 - remainingTroopRatio) / totalRatioUsage; | ||
| } | ||
|
Comment on lines
+45
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Floating-point math in deterministic core.
As per coding guidelines: "Ensure deterministic behavior in Also applies to: 106-111, 126-130 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| createExecs(turn: Turn): Execution[] { | ||
| return turn.intents.map((i) => this.createExec(i)); | ||
| // In the rare case a client sends multiple troopRatio-orders, | ||
| // we need to "merge" their orders instead of executing them in parallel. | ||
| // (two 60% attacks should be one 84% attack, not one 120% attack) | ||
| // But, they may be of different types/on different targets | ||
| // (hence we do two (84/120)*60% = 42% attacks). | ||
| const remainingTroopRatio_perClientID = new Map<ClientID, number>(); | ||
| const totalRatioUsage_perClientID = new Map<ClientID, number>(); | ||
| for (const intent of turn.intents) { | ||
| switch (intent.type) { | ||
| case "boat": | ||
| case "attack": { | ||
| remainingTroopRatio_perClientID.set( | ||
| intent.clientID, | ||
| (remainingTroopRatio_perClientID.get(intent.clientID) ?? 1) * | ||
| (1 - intent.troopRatio), | ||
| ); | ||
| totalRatioUsage_perClientID.set( | ||
| intent.clientID, | ||
| (totalRatioUsage_perClientID.get(intent.clientID) ?? 0) + | ||
| intent.troopRatio, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return turn.intents.map((intent) => { | ||
| switch (intent.type) { | ||
| case "boat": | ||
| case "attack": | ||
| return this.createExec( | ||
| intent, | ||
| this.computeRatio( | ||
| remainingTroopRatio_perClientID.get(intent.clientID)!, | ||
| totalRatioUsage_perClientID.get(intent.clientID)!, | ||
| ), | ||
| ); | ||
| default: | ||
| return this.createExec(intent, undefined); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| createExec(intent: StampedIntent): Execution { | ||
| createExec(intent: StampedIntent, troopRatioFactor?: number): Execution { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find all callers of createExec to confirm attack/boat callers always pass a factor.
rg -nP --type=ts -C3 '\.createExec\s*\('Repository: openfrontio/OpenFrontIO Length of output: 161 🏁 Script executed: # First, let's verify the file exists and get its structure
ls -la src/core/execution/ExecutionManager.ts
# Use simpler rg syntax to find createExec calls (avoid -P flag)
rg --type ts 'createExec' src/core/execution/ -A2Repository: openfrontio/OpenFrontIO Length of output: 1223 🏁 Script executed: # Also search the entire codebase for createExec calls
rg --type ts 'createExec' -C 2Repository: openfrontio/OpenFrontIO Length of output: 161 🏁 Script executed: # Read the ExecutionManager.ts file to see the complete context
cat -n src/core/execution/ExecutionManager.tsRepository: openfrontio/OpenFrontIO Length of output: 8918 🏁 Script executed: # Search more broadly for createExec calls across the entire repo
find . -name "*.ts" -type f -exec grep -l "createExec" {} \;Repository: openfrontio/OpenFrontIO Length of output: 275 🏁 Script executed: # Check GameRunner.ts for how it calls createExec
cat -n src/core/GameRunner.ts | grep -A 5 -B 5 "createExec"Repository: openfrontio/OpenFrontIO Length of output: 501 🏁 Script executed: # Also check the test file to understand expected usage patterns
cat -n tests/core/execution/ExecutionManager.test.tsRepository: openfrontio/OpenFrontIO Length of output: 5696 Guard against direct calls to The method is public and Either add a runtime guard (throw error if 🤖 Prompt for AI Agents |
||
| const player = this.mg.playerByClientID(intent.clientID); | ||
| if (!player) { | ||
| console.warn(`player with clientID ${intent.clientID} not found`); | ||
|
|
@@ -57,7 +103,12 @@ export class Executor { | |
| switch (intent.type) { | ||
| case "attack": { | ||
| return new AttackExecution( | ||
| intent.troops, | ||
| Math.floor( | ||
| Math.min( | ||
| troopRatioFactor! * intent.troopRatio * intent.troopCount, | ||
| intent.maxTroopSent ?? intent.troopCount, | ||
| ), | ||
| ), | ||
| player, | ||
| intent.targetID, | ||
| null, | ||
|
|
@@ -72,7 +123,11 @@ export class Executor { | |
| case "spawn": | ||
| return new SpawnExecution(this.gameID, player.info(), intent.tile); | ||
| case "boat": | ||
| return new TransportShipExecution(player, intent.dst, intent.troops); | ||
| return new TransportShipExecution( | ||
| player, | ||
| intent.dst, | ||
| Math.floor(troopRatioFactor! * intent.troopRatio * intent.troopCount), | ||
| ); | ||
| case "allianceRequest": | ||
| return new AllianceRequestExecution(player, intent.recipient); | ||
| case "allianceReject": | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Forward the retaliation cap in the attack intent.
The fourth constructor argument is never serialized, so retaliate actions drop the
maxTroopSentcap and can send more troops than the incoming attack allows. Match the schema field and only include it when present.Proposed fix
this.sendIntent({ type: "attack", targetID: event.targetID, troopRatio: event.troopRatio, troopCount: event.troopCount, + ...(event.maxTroopSent === null + ? {} + : { maxTroopSent: event.maxTroopSent }), });Also applies to: 486-492
🤖 Prompt for AI Agents