@@ -172,13 +172,19 @@ private async void TimerOnElapsed(object sender, ElapsedEventArgs elapsedEventAr
172172 }
173173
174174
175- // ===================== SQUAD FORMATION (PIERCING) =====================
175+ // ===================== SQUAD FORMATION =====================
176+ // Phase 1: planets with >= TeamSize attackers form one squad with ALL their attackers.
177+ // Phase 2: repeatedly complete a squad for the highest-ranked straggler whose planet
178+ // the pool can still fill, pulling other stragglers to him as fillers. Skip
179+ // leaders whose planet is too big for the pool; stop when none qualify.
180+ // Phase 3: any remaining stragglers join the strongest existing squad (drawn by its
181+ // highest-ranked member). If no squad was formed at all, they are dropped.
182+ // Result: at most one squad per planet.
176183
177184 private void RunSquadFormation ( )
178185 {
179186 FormedSquads . Clear ( ) ;
180187
181- // collect all attackers still connected, grouped by planet
182188 var playerPlanet = new Dictionary < string , AttackOption > ( ) ; // player -> their chosen option
183189 foreach ( var opt in AttackOptions )
184190 {
@@ -213,71 +219,62 @@ private void RunSquadFormation()
213219 }
214220 }
215221
216- var pool = new HashSet < string > ( playerPlanet . Keys ) ;
217-
218- // Pass 1: while any planet has >= TeamSize players, form squads from top WHR
219- bool formed ;
220- do
222+ // Phase 1: self-sufficient planets form one squad with all their attackers
223+ foreach ( var opt in AttackOptions )
221224 {
222- formed = false ;
223- foreach ( var opt in AttackOptions )
225+ var available = opt . Attackers . Where ( playerPlanet . ContainsKey ) . ToList ( ) ;
226+ if ( available . Count >= opt . TeamSize )
224227 {
225- var available = opt . Attackers . Where ( pool . Contains ) . OrderByDescending ( x => playerWhr . Get ( x ) ) . ToList ( ) ;
226- while ( available . Count >= opt . TeamSize )
227- {
228- var squad = CreateSquadFromOption ( opt ) ;
229- squad . Attackers = available . Take ( opt . TeamSize ) . ToList ( ) ;
230- FormedSquads . Add ( squad ) ;
231- foreach ( var p in squad . Attackers ) pool . Remove ( p ) ;
232- available = available . Skip ( opt . TeamSize ) . ToList ( ) ;
233- formed = true ;
234- }
228+ var squad = CreateSquadFromOption ( opt ) ;
229+ squad . Attackers = available ;
230+ squad . TeamSize = available . Count ;
231+ FormedSquads . Add ( squad ) ;
232+ foreach ( var p in available ) playerPlanet . Remove ( p ) ;
235233 }
236- } while ( formed ) ; // repeat in case removing players from one planet frees up nothing, but be safe
234+ }
237235
238- // Pass 2: piercing — top PW-Rank player pulls others to their planet
239- while ( pool . Count > 0 )
236+ // Phase 2: piercing — pick the top-ranked straggler whose planet's TeamSize
237+ // the pool can still satisfy; skip leaders whose planet is too big.
238+ while ( playerPlanet . Count > 0 )
240239 {
241- // find top PW-Rank player (lowest DisplayOrder, tiebreak by WHR desc)
242- var leader = pool
240+ var leader = playerPlanet . Keys
241+ . Where ( x => playerPlanet [ x ] . TeamSize <= playerPlanet . Count )
243242 . OrderBy ( x => playerRoleOrder . GetOrDefault ( x , int . MaxValue ) )
244243 . ThenByDescending ( x => playerWhr . Get ( x ) )
245- . First ( ) ;
244+ . FirstOrDefault ( ) ;
245+
246+ if ( leader == null ) break ; // no straggler's planet fits the remaining pool
246247
247248 var leaderOption = playerPlanet [ leader ] ;
248- if ( pool . Count < leaderOption . TeamSize )
249- break ; // not enough players for any squad
250249
251- var fillers = pool
250+ var fillers = playerPlanet . Keys
252251 . Where ( x => x != leader )
253252 . OrderByDescending ( x => playerWhr . Get ( x ) )
254253 . Take ( leaderOption . TeamSize - 1 )
255254 . ToList ( ) ;
256255
257- if ( fillers . Count < leaderOption . TeamSize - 1 )
258- break ; // not enough
259-
260256 var squad = CreateSquadFromOption ( leaderOption ) ;
261- squad . Attackers = new List < string > { leader } ;
257+ squad . Attackers . Add ( leader ) ;
262258 squad . Attackers . AddRange ( fillers ) ;
259+ squad . TeamSize = squad . Attackers . Count ;
263260 FormedSquads . Add ( squad ) ;
264261
265- pool . Remove ( leader ) ;
266- foreach ( var p in fillers ) pool . Remove ( p ) ;
262+ playerPlanet . Remove ( leader ) ;
263+ foreach ( var p in fillers ) playerPlanet . Remove ( p ) ;
267264 }
268265
269- // Pass 3: absorb leftovers into an existing squad on their original planet ,
270- // so all attackers join when a planet had more people than TeamSize .
271- foreach ( var name in pool . ToList ( ) )
266+ // Phase 3: remaining stragglers all merge into the strongest existing squad ,
267+ // i.e. the one whose best member has the top PW-Rank (tiebreak by WHR) .
268+ if ( playerPlanet . Count > 0 && FormedSquads . Count > 0 )
272269 {
273- var originalPlanetId = playerPlanet [ name ] . PlanetID ;
274- var squad = FormedSquads . FirstOrDefault ( s => s . PlanetID == originalPlanetId ) ;
275- if ( squad != null )
276- {
277- squad . Attackers . Add ( name ) ;
278- squad . TeamSize = squad . Attackers . Count ;
279- pool . Remove ( name ) ;
280- }
270+ var strongest = FormedSquads
271+ . OrderBy ( s => s . Attackers . Min ( a => playerRoleOrder . GetOrDefault ( a , int . MaxValue ) ) )
272+ . ThenByDescending ( s => s . Attackers . Max ( a => playerWhr . Get ( a ) ) )
273+ . First ( ) ;
274+
275+ strongest . Attackers . AddRange ( playerPlanet . Keys ) ;
276+ strongest . TeamSize = strongest . Attackers . Count ;
277+ playerPlanet . Clear ( ) ;
281278 }
282279
283280 AttackOptions . Clear ( ) ;
@@ -326,38 +323,29 @@ private void RunDefenderAssignment()
326323 }
327324 }
328325
329- // per-planet : assign defenders, overflow to pool
326+ // per-squad : assign top-WHR volunteers; overflow spills to floating pool
330327 var floatingPool = new List < string > ( ) ;
331- var assignedDefenders = new Dictionary < int , List < string > > ( ) ; // planetID -> assigned defender names
332- var attackedPlanetIds = FormedSquads . Select ( s => s . PlanetID ) . Distinct ( ) . ToList ( ) ;
333-
334- foreach ( var planetId in attackedPlanetIds )
328+ foreach ( var squad in FormedSquads )
335329 {
336- var totalSlotsNeeded = FormedSquads . Where ( s => s . PlanetID == planetId ) . Sum ( s => s . TeamSize ) ;
337- var volunteers = ( DefenderVotes . ContainsKey ( planetId ) ? DefenderVotes [ planetId ] : new List < string > ( ) )
330+ var volunteers = ( DefenderVotes . ContainsKey ( squad . PlanetID ) ? DefenderVotes [ squad . PlanetID ] : new List < string > ( ) )
338331 . Where ( x => server . ConnectedUsers . ContainsKey ( x ) && defenderWhr . ContainsKey ( x ) )
339332 . OrderByDescending ( x => defenderWhr [ x ] )
340333 . ToList ( ) ;
341334
342- if ( volunteers . Count > totalSlotsNeeded )
335+ if ( volunteers . Count > squad . TeamSize )
343336 {
344- assignedDefenders [ planetId ] = volunteers . Take ( totalSlotsNeeded ) . ToList ( ) ;
345- floatingPool . AddRange ( volunteers . Skip ( totalSlotsNeeded ) ) ;
337+ squad . Defenders = volunteers . Take ( squad . TeamSize ) . ToList ( ) ;
338+ floatingPool . AddRange ( volunteers . Skip ( squad . TeamSize ) ) ;
346339 }
347340 else
348341 {
349- assignedDefenders [ planetId ] = volunteers ;
342+ squad . Defenders = volunteers ;
350343 }
351344 }
352345
353- // floating pool fills unfilled slots on other planets (WHR order, respecting faction eligibility)
346+ // floating pool fills deficits on other squads (WHR order, respecting faction eligibility)
354347 floatingPool = floatingPool . OrderByDescending ( x => defenderWhr . Get ( x ) ) . ToList ( ) ;
355348
356- // cache defending factions per planet and defender faction IDs
357- var planetDefendingFactions = new Dictionary < int , List < Faction > > ( ) ;
358- foreach ( var pid in attackedPlanetIds )
359- planetDefendingFactions [ pid ] = GetDefendingFactions ( FormedSquads . First ( s => s . PlanetID == pid ) ) ;
360-
361349 var defenderFactionId = new Dictionary < string , int ? > ( ) ;
362350 using ( var db = new ZkDataContext ( ) )
363351 {
@@ -368,45 +356,18 @@ private void RunDefenderAssignment()
368356 }
369357 }
370358
371- foreach ( var planetId in attackedPlanetIds )
359+ foreach ( var squad in FormedSquads )
372360 {
373- var totalSlotsNeeded = FormedSquads . Where ( s => s . PlanetID == planetId ) . Sum ( s => s . TeamSize ) ;
374- var assigned = assignedDefenders [ planetId ] ;
375- var deficit = totalSlotsNeeded - assigned . Count ;
361+ var deficit = squad . TeamSize - squad . Defenders . Count ;
376362 if ( deficit > 0 && floatingPool . Count > 0 )
377363 {
378- var allowedFactionIds = planetDefendingFactions [ planetId ] . Select ( f => f . FactionID ) . ToHashSet ( ) ;
364+ var allowedFactionIds = GetDefendingFactions ( squad ) . Select ( f => f . FactionID ) . ToHashSet ( ) ;
379365 var eligible = floatingPool . Where ( x => defenderFactionId . ContainsKey ( x ) && defenderFactionId [ x ] . HasValue && allowedFactionIds . Contains ( defenderFactionId [ x ] . Value ) ) . ToList ( ) ;
380366 var toAdd = eligible . Take ( deficit ) . ToList ( ) ;
381- assigned . AddRange ( toAdd ) ;
367+ squad . Defenders . AddRange ( toAdd ) ;
382368 foreach ( var p in toAdd ) floatingPool . Remove ( p ) ;
383369 }
384370 }
385-
386- // slice defenders into squads: sort squads by avg attacker WHR desc, assign best defenders to best attackers
387- foreach ( var planetId in attackedPlanetIds )
388- {
389- var squadsForPlanet = FormedSquads
390- . Where ( s => s . PlanetID == planetId )
391- . OrderByDescending ( s => s . Attackers . Average ( a => GetPlayerWhr ( a ) ) ) // sort by attacker strength
392- . ToList ( ) ;
393-
394- var defenders = assignedDefenders . ContainsKey ( planetId )
395- ? assignedDefenders [ planetId ] . OrderByDescending ( x => defenderWhr . Get ( x ) ) . ToList ( )
396- : new List < string > ( ) ;
397-
398- int idx = 0 ;
399- foreach ( var squad in squadsForPlanet )
400- {
401- var count = Math . Min ( squad . TeamSize , defenders . Count - idx ) ;
402- if ( count > 0 )
403- {
404- squad . Defenders = defenders . Skip ( idx ) . Take ( count ) . ToList ( ) ;
405- idx += count ;
406- }
407- // else: no defenders at all for this squad (concede)
408- }
409- }
410371 }
411372
412373 private double GetPlayerWhr ( string name )
@@ -423,38 +384,30 @@ private async Task LaunchAllBattles()
423384 {
424385 var attackerNamesToChargeSpend = new List < string > ( ) ;
425386
426- // merge squads on the same planet into one battle per planet
427- foreach ( var planetId in FormedSquads . Select ( s => s . PlanetID ) . Distinct ( ) . ToList ( ) )
387+ foreach ( var squad in FormedSquads )
428388 {
429- var squads = FormedSquads . Where ( s => s . PlanetID == planetId ) . ToList ( ) ;
430- var first = squads . First ( ) ;
431-
432- // merge all squads into one AttackOption
433- var merged = CreateSquadFromOption ( first ) ;
434- foreach ( var squad in squads )
435- {
436- merged . Attackers . AddRange ( squad . Attackers . Where ( x => server . ConnectedUsers . ContainsKey ( x ) ) ) ;
437- merged . Defenders . AddRange ( squad . Defenders . Where ( x => server . ConnectedUsers . ContainsKey ( x ) ) ) ;
438- }
389+ // drop anyone who disconnected between squad formation and launch
390+ squad . Attackers = squad . Attackers . Where ( x => server . ConnectedUsers . ContainsKey ( x ) ) . ToList ( ) ;
391+ squad . Defenders = squad . Defenders . Where ( x => server . ConnectedUsers . ContainsKey ( x ) ) . ToList ( ) ;
439392
440- if ( merged . Attackers . Count > 0 ) attackerNamesToChargeSpend . AddRange ( merged . Attackers ) ;
393+ if ( squad . Attackers . Count > 0 ) attackerNamesToChargeSpend . AddRange ( squad . Attackers ) ;
441394
442- if ( merged . Defenders . Count > 0 && merged . Attackers . Count > 0 )
395+ if ( squad . Defenders . Count > 0 && squad . Attackers . Count > 0 )
443396 {
444397 // battle (may be uneven)
445398 try
446399 {
447- merged . TeamSize = Math . Max ( merged . Attackers . Count , merged . Defenders . Count ) ;
448- var battle = new PlanetWarsServerBattle ( server , merged ) ;
400+ squad . TeamSize = Math . Max ( squad . Attackers . Count , squad . Defenders . Count ) ;
401+ var battle = new PlanetWarsServerBattle ( server , squad ) ;
449402 await server . AddBattle ( battle ) ;
450- RunningBattles [ battle . BattleID ] = merged ;
403+ RunningBattles [ battle . BattleID ] = squad ;
451404
452- foreach ( var usr in merged . Attackers . Union ( merged . Defenders ) )
405+ foreach ( var usr in squad . Attackers . Union ( squad . Defenders ) )
453406 await server . ForceJoinBattle ( usr , battle ) ;
454407
455408 if ( await battle . StartGame ( ) )
456409 {
457- var text = $ "Battle for planet { merged . Name } starts on zk://@join_player:{ merged . Attackers . FirstOrDefault ( ) } Roster: { string . Join ( "," , merged . Attackers ) } vs { string . Join ( "," , merged . Defenders ) } ";
410+ var text = $ "Battle for planet { squad . Name } starts on zk://@join_player:{ squad . Attackers . FirstOrDefault ( ) } Roster: { string . Join ( "," , squad . Attackers ) } vs { string . Join ( "," , squad . Defenders ) } ";
458411 foreach ( var fac in factions ) await server . GhostChanSay ( fac . Shortcut , text ) ;
459412 }
460413 else
@@ -468,10 +421,10 @@ private async Task LaunchAllBattles()
468421 Trace . TraceError ( "PlanetWars LaunchBattle error: {0}" , ex ) ;
469422 }
470423 }
471- else if ( merged . Attackers . Count > 0 )
424+ else if ( squad . Attackers . Count > 0 )
472425 {
473426 // concede - zero defenders
474- RecordPlanetwarsLoss ( merged ) ;
427+ RecordPlanetwarsLoss ( squad ) ;
475428 }
476429 // else: no attackers left, skip entirely
477430 }
0 commit comments