@@ -4,6 +4,7 @@ import { buildSolarSystemMap, SCENARIOS, findBaseHex, findBaseHexes } from '../m
44import { SHIP_STATS , ORDNANCE_MASS } from '../constants' ;
55import { hexKey , hexEqual , hexDistance } from '../hex' ;
66import { resolveBaseDefense } from '../combat' ;
7+ import { updateCheckpoints , checkImmediateVictory } from '../engine-victory' ;
78import type { GameState , ScenarioDefinition , SolarSystemMap , AstrogationOrder , OrdnanceLaunch , Ordnance , Ship } from '../types' ;
89
910let map : SolarSystemMap ;
@@ -2480,3 +2481,147 @@ describe('fleet building (MegaCredit economy)', () => {
24802481 }
24812482 } ) ;
24822483} ) ;
2484+
2485+ describe ( 'Grand Tour' , ( ) => {
2486+ let tourState : GameState ;
2487+
2488+ beforeEach ( ( ) => {
2489+ tourState = createGame ( SCENARIOS . grandTour , map , 'TOUR1' , findBaseHex ) ;
2490+ } ) ;
2491+
2492+ it ( 'initializes checkpoint tracking' , ( ) => {
2493+ expect ( tourState . scenarioRules . checkpointBodies ) . toEqual (
2494+ [ 'Sol' , 'Mercury' , 'Venus' , 'Terra' , 'Mars' , 'Jupiter' , 'Io' , 'Callisto' ] ,
2495+ ) ;
2496+ expect ( tourState . scenarioRules . combatDisabled ) . toBe ( true ) ;
2497+ expect ( tourState . players [ 0 ] . visitedBodies ) . toBeDefined ( ) ;
2498+ expect ( tourState . players [ 0 ] . totalFuelSpent ) . toBe ( 0 ) ;
2499+ expect ( tourState . players [ 1 ] . visitedBodies ) . toBeDefined ( ) ;
2500+ expect ( tourState . players [ 1 ] . totalFuelSpent ) . toBe ( 0 ) ;
2501+ } ) ;
2502+
2503+ it ( 'pre-marks starting body as visited' , ( ) => {
2504+ // Player 0 starts at Terra, Player 1 at Mars
2505+ expect ( tourState . players [ 0 ] . visitedBodies ) . toContain ( 'Terra' ) ;
2506+ expect ( tourState . players [ 1 ] . visitedBodies ) . toContain ( 'Mars' ) ;
2507+ } ) ;
2508+
2509+ it ( 'gives both players shared bases for fuel' , ( ) => {
2510+ // Both players should have bases on Terra, Venus, Mars, and Callisto
2511+ const terraBaseKeys = findBaseHexes ( map , 'Terra' ) . map ( h => hexKey ( h ) ) ;
2512+ const venusBaseKeys = findBaseHexes ( map , 'Venus' ) . map ( h => hexKey ( h ) ) ;
2513+ const marsBaseKeys = findBaseHexes ( map , 'Mars' ) . map ( h => hexKey ( h ) ) ;
2514+ const callistoBaseKeys = findBaseHexes ( map , 'Callisto' ) . map ( h => hexKey ( h ) ) ;
2515+
2516+ for ( const key of [ ...terraBaseKeys , ...venusBaseKeys , ...marsBaseKeys , ...callistoBaseKeys ] ) {
2517+ expect ( tourState . players [ 0 ] . bases ) . toContain ( key ) ;
2518+ expect ( tourState . players [ 1 ] . bases ) . toContain ( key ) ;
2519+ }
2520+ } ) ;
2521+
2522+ it ( 'updates checkpoints when path crosses gravity hexes' , ( ) => {
2523+ // Find a Mercury gravity hex
2524+ const mercuryGravityHex = [ ...map . hexes . entries ( ) ]
2525+ . find ( ( [ , hex ] ) => hex . gravity ?. bodyName === 'Mercury' ) ;
2526+ expect ( mercuryGravityHex ) . toBeDefined ( ) ;
2527+
2528+ const [ keyStr ] = mercuryGravityHex ! ;
2529+ const [ q , r ] = keyStr . split ( ',' ) . map ( Number ) ;
2530+
2531+ updateCheckpoints ( tourState , 0 , [ { q, r } ] , map ) ;
2532+ expect ( tourState . players [ 0 ] . visitedBodies ) . toContain ( 'Mercury' ) ;
2533+ // Player 1 should be unaffected
2534+ expect ( tourState . players [ 1 ] . visitedBodies ) . not . toContain ( 'Mercury' ) ;
2535+ } ) ;
2536+
2537+ it ( 'does not duplicate visited bodies' , ( ) => {
2538+ const mercuryGravityHex = [ ...map . hexes . entries ( ) ]
2539+ . find ( ( [ , hex ] ) => hex . gravity ?. bodyName === 'Mercury' ) ;
2540+ const [ keyStr ] = mercuryGravityHex ! ;
2541+ const [ q , r ] = keyStr . split ( ',' ) . map ( Number ) ;
2542+
2543+ updateCheckpoints ( tourState , 0 , [ { q, r } ] , map ) ;
2544+ updateCheckpoints ( tourState , 0 , [ { q, r } ] , map ) ;
2545+ const count = tourState . players [ 0 ] . visitedBodies ! . filter ( b => b === 'Mercury' ) . length ;
2546+ expect ( count ) . toBe ( 1 ) ;
2547+ } ) ;
2548+
2549+ it ( 'wins when all checkpoints visited and landed at home' , ( ) => {
2550+ // Mark all checkpoints as visited for player 0
2551+ tourState . players [ 0 ] . visitedBodies = [ ...tourState . scenarioRules . checkpointBodies ! ] ;
2552+
2553+ // Land the ship at a Terra base
2554+ const ship = tourState . ships . find ( s => s . owner === 0 ) ! ;
2555+ const terraBase = findBaseHexes ( map , 'Terra' ) [ 0 ] ;
2556+ ship . position = terraBase ;
2557+ ship . landed = true ;
2558+ ship . destroyed = false ;
2559+
2560+ // Run astrogation with no burns (ship is already landed)
2561+ // Directly test checkImmediateVictory by importing it
2562+ checkImmediateVictory ( tourState , map ) ;
2563+
2564+ expect ( tourState . winner ) . toBe ( 0 ) ;
2565+ expect ( tourState . winReason ) . toContain ( 'Grand Tour complete' ) ;
2566+ } ) ;
2567+
2568+ it ( 'does not win with incomplete checkpoints' , ( ) => {
2569+ // Mark only some checkpoints as visited
2570+ tourState . players [ 0 ] . visitedBodies = [ 'Terra' , 'Mercury' , 'Venus' ] ;
2571+
2572+ const ship = tourState . ships . find ( s => s . owner === 0 ) ! ;
2573+ const terraBase = findBaseHexes ( map , 'Terra' ) [ 0 ] ;
2574+ ship . position = terraBase ;
2575+ ship . landed = true ;
2576+ ship . destroyed = false ;
2577+
2578+ checkImmediateVictory ( tourState , map ) ;
2579+
2580+ expect ( tourState . winner ) . toBeNull ( ) ;
2581+ } ) ;
2582+
2583+ it ( 'does not win at wrong home body' , ( ) => {
2584+ // All checkpoints visited but landed at Mars (not home for player 0)
2585+ tourState . players [ 0 ] . visitedBodies = [ ...tourState . scenarioRules . checkpointBodies ! ] ;
2586+
2587+ const ship = tourState . ships . find ( s => s . owner === 0 ) ! ;
2588+ const marsBase = findBaseHexes ( map , 'Mars' ) [ 0 ] ;
2589+ ship . position = marsBase ;
2590+ ship . landed = true ;
2591+ ship . destroyed = false ;
2592+
2593+ checkImmediateVictory ( tourState , map ) ;
2594+
2595+ expect ( tourState . winner ) . toBeNull ( ) ;
2596+ } ) ;
2597+
2598+ it ( 'skips combat phase when combatDisabled' , ( ) => {
2599+ // Move player 0's ship next to player 1's ship
2600+ const ship0 = tourState . ships . find ( s => s . owner === 0 ) ! ;
2601+ const ship1 = tourState . ships . find ( s => s . owner === 1 ) ! ;
2602+ ship0 . position = { q : 0 , r : 0 } ;
2603+ ship0 . landed = false ;
2604+ ship0 . velocity = { dq : 0 , dr : 0 } ;
2605+ ship1 . position = { q : 1 , r : 0 } ;
2606+ ship1 . landed = false ;
2607+ ship1 . velocity = { dq : 0 , dr : 0 } ;
2608+
2609+ // Process a turn — should skip combat
2610+ const result = processAstrogation ( tourState , 0 , [ { shipId : ship0 . id , burn : null } ] , map ) ;
2611+ // After movement, phase should not be 'combat'
2612+ expect ( tourState . phase ) . not . toBe ( 'combat' ) ;
2613+ } ) ;
2614+
2615+ it ( 'tracks fuel consumption' , ( ) => {
2616+ const ship = tourState . ships . find ( s => s . owner === 0 ) ! ;
2617+ ship . landed = false ;
2618+ ship . velocity = { dq : 0 , dr : 0 } ;
2619+ tourState . phase = 'astrogation' ;
2620+ tourState . activePlayer = 0 ;
2621+
2622+ const initialFuel = tourState . players [ 0 ] . totalFuelSpent ;
2623+ resolveAstrogationMovement ( tourState , 0 , [ { shipId : ship . id , burn : 0 } ] ) ;
2624+
2625+ expect ( tourState . players [ 0 ] . totalFuelSpent ) . toBe ( initialFuel ! + 1 ) ;
2626+ } ) ;
2627+ } ) ;
0 commit comments