From 26089ac0c3c069bd954a2afb2847f6aa6692ecd3 Mon Sep 17 00:00:00 2001
From: Tor Andersson <tor@ccxvii.net>
Date: Fri, 18 Feb 2022 17:23:14 +0100
Subject: Indians & Leaders Go Home after late season.

---
 rules.js | 283 ++++++++++++++++++++++++++++++++++++++++++++-------------------
 1 file changed, 197 insertions(+), 86 deletions(-)

diff --git a/rules.js b/rules.js
index 5bf2b17..5968995 100644
--- a/rules.js
+++ b/rules.js
@@ -1,35 +1,38 @@
 "use strict";
 
 // RULES
-// does defender win assault if attacker is eliminated?
+// Do defenders win an assault if the attacker is eliminated?
+// Can leaders and coureurs follow indians home to Pays d'en Haut?
+// May indians stacked with a leader at their home settlement follow the leader home to a fortification?
 // France/Britain or "The French"/"The British"
 
+// Log messages - past tense.
+
 // WONTFIX
-// TODO: select leader for defense instead of automatically picking
+// TODO: select leader for defense instead of automatically picking the best
 // TODO: remove old 7 command leader(s) immediately as they're drawn, before placing reinforcements
 
 // CLEANUPS
-// TODO: rename node/space -> location/space or raw_space/space or box/space?
 // TODO: replace piece[p].type lookups with index range checks
-// TODO: move core of is_friendly/enemy to is_british/french and branch in is_friendly/enemy
 // TODO: make 'inside' negative location instead of separate array
 // TODO: abbreviate per-player (game.British.xxx) property name (game.b.xxx)
-// TODO: add/remove 'next' steps at end of states
-// TODO: manual selection of reduced/placed units in events
-// TODO: show british leader pool
-// TODO: show badge on leader boxes if they're eliminated or in pool
-// TODO: show discard/removed card list in UI
+// TODO: move core of is_friendly/enemy to is_british/french and branch in is_friendly/enemy
 // TODO: for_each_exit -> flat list of all exits
-// TODO: special "naval_move" action for different highlight to do naval moves
-
-// TODO: check when unstack happens
-// TODO: auto-select retreat destination if only one available
+// UI: show discard/removed card list in UI
+// UI: show pool leaders in their own box
+// UI: show dead leaders as grayed out in own box
 
 // MAJOR
 // TODO: check activation limits when dropping subcommanders
-// TODO: 10.413 leaders and coureurs may follow indians home
 // TODO: leaders alone - retreat from reinforcement placements
 // TODO: find closest path to non-infiltration space for allowing infiltration
+// TODO: manual selection of reduced/placed units in events
+// TODO: raiders go home
+
+// RETREAT BEHAVIOR
+// a) auto-select retreat destination if only one available like for attack retreats?
+// b) auto-send retreating units to destination if only one available?
+// b) click to retreat/eliminate all attackers individually?
 
 const { spaces, pieces, cards } = require("./data");
 
@@ -1031,13 +1034,6 @@ function set_piece_outside(p) {
 	remove_from_array(game.pieces.inside, p);
 }
 
-function set_force_inside(force) {
-	for_each_piece_in_force(force, p => {
-		set_piece_inside(p);
-	});
-	unstack_force(force);
-}
-
 function is_piece_on_map(p) {
 	// TODO: militia boxes?
 	return piece_node(p) !== 0;
@@ -1256,6 +1252,13 @@ function count_enemy_units_in_space(space) {
 	return n;
 }
 
+function has_unbesieged_friendly_leader(space) {
+	for (let p = first_friendly_leader; p <= last_friendly_leader; ++p)
+		if (is_piece_in_space(p, space) && !is_piece_inside(p))
+			return true;
+	return false;
+}
+
 function has_unbesieged_enemy_leader(space) {
 	for (let p = first_enemy_leader; p <= last_enemy_leader; ++p)
 		if (is_piece_in_space(p, space) && !is_piece_inside(p))
@@ -2050,7 +2053,7 @@ function end_late_season() {
 	log("");
 	delete game.events.no_amphib;
 	delete game.events.blockhouses;
-	goto_indians_and_leaders_go_home();
+	goto_go_home_after_late_season();
 }
 
 function start_action_phase() {
@@ -2067,8 +2070,10 @@ function end_action_phase() {
 	game.count = 0;
 
 	// TODO: should not be needed! (but we may have forgotten some place where it should happen)
-	for (let p = first_friendly_leader; p <= last_friendly_leader; ++p)
-		unstack_force(p);
+	for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) {
+		if (count_pieces_in_force(p) > 0)
+			console.log("FORGOT TO UNSTACK", piece_name(p));
+	}
 
 	if (!enemy_player.passed && enemy_player.hand.length > 0) {
 		console.log("END ACTION PHASE - NEXT PLAYER");
@@ -4799,7 +4804,7 @@ states.retreat_defender_to = {
 		let who = game.battle.who;
 		if (from === to) {
 			log("retreats inside fortification");
-			set_force_inside(who);
+			set_piece_inside(who);
 		} else {
 			log("retreats to " + space_name(to));
 			move_piece_to(who, to);
@@ -4863,7 +4868,7 @@ states.retreat_lone_leader = {
 		let who = pick_unbesieged_leader(from);
 		if (from === to) {
 			log("retreats inside fortification");
-			set_force_inside(who);
+			set_piece_inside(who);
 		} else {
 			log("retreats to " + space_name(to));
 			move_piece_to(who, to);
@@ -5363,30 +5368,27 @@ states.raiders_go_home = {
 
 // LATE SEASON - INDIANS AND LEADERS GO HOME
 
-function goto_indians_and_leaders_go_home() {
+function goto_go_home_after_late_season() {
 	set_active(FRANCE);
-	game.state = 'indians_and_leaders_go_home';
-	game.go_home = {
-		indians: {}
-	};
-	resume_indians_and_leaders_go_home();
-	if (!game.go_home.who)
-		end_indians_and_leaders_go_home();
+	resume_go_home_after_late_season();
 }
 
-function resume_indians_and_leaders_go_home() {
-	let who = game.go_home.who = next_indian_and_leader_to_go_home();
-	if (who && is_leader(who))
-		game.go_home.closest = find_closest_friendly_unbesieged_fortification(piece_space(who));
+function resume_go_home_after_late_season() {
+	game.state = 'go_home_who';
+	game.go_home = {
+		reason: 'late_season',
+		who: 0,
+		from: 0,
+		to: 0,
+		follow: {},
+	};
 }
 
-function end_indians_and_leaders_go_home() {
+function end_go_home_after_late_season() {
 	clear_undo();
 	if (game.active === FRANCE) {
 		set_active(BRITAIN);
-		resume_indians_and_leaders_go_home();
-		if (!game.go_home.who)
-			end_indians_and_leaders_go_home();
+		resume_go_home_after_late_season();
 	} else {
 		set_active(FRANCE);
 		delete game.go_home;
@@ -5394,73 +5396,182 @@ function end_indians_and_leaders_go_home() {
 	}
 }
 
-function next_indian_and_leader_to_go_home() {
-	// Indians go home first
-	for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) {
-		if (is_indian_unit(p) && !is_piece_inside(p)) {
+states.go_home_who = {
+	prompt() {
+		let done = true;
+		for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) {
 			let s = piece_space(p);
-			if (s && s != indian_home_settlement(p) && !has_unbesieged_friendly_fortifications(s))
-				return p;
-		}
-	}
+			if (s && !is_piece_inside(p) && !has_friendly_fortifications(s)) {
 
-	// Then leaders who are left alone in the wilderness
-	for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) {
-		if (!is_piece_inside(p)) {
-			let s = piece_space(p);
-			if (s && is_wilderness_or_mountain(s) && !has_friendly_units(s) && !has_unbesieged_friendly_fortifications(s))
-				return p;
+				// Indians not at their settlement
+				if (is_indian_unit(p)) {
+					if (s != indian_home_settlement(p)) {
+						done = false;
+						gen_action_piece(p);
+					}
+				}
+
+				// Leaders who are left alone in the wilderness
+				if (is_leader(p)) {
+					if (is_wilderness_or_mountain(s) && !has_friendly_units(s)) {
+						done = false;
+						gen_action_piece(p);
+					}
+				}
+			}
 		}
-	}
+		if (done) {
+			view.prompt = "Indians and leaders go home \u2014 done.";
+			gen_action_next();
+		} else {
+			view.prompt = "Indians and leaders go home.";
+		}
+	},
+	piece(p) {
+		push_undo();
+		game.go_home.who = p;
+		game.go_home.from = piece_space(p);
+		game.state = 'go_home_to';
+	},
+	next() {
+		if (game.go_home.reason === 'late_season')
+			end_go_home_after_late_season();
+		else
+			end_go_home_after_raid();
+	},
 }
 
-states.indians_and_leaders_go_home = {
+states.go_home_to = {
 	prompt() {
 		let who = game.go_home.who;
+		let from = game.go_home.from;
 
-		if (who)
-			view.prompt = "Indians and leaders go home \u2014 " + piece_name(who) + ".";
-		else
-			view.prompt = "Indians and leaders go home \u2014 done.";
+		view.prompt = `Indians and leaders go home \u2014 ${piece_name(who)}.`;
 		view.who = who;
 
-		if (!who) {
-			gen_action('next');
-		} else if (is_indian_unit(who)) {
-			// 10.412: Cherokee have no home settlement (home=0)
+		let can_go_home = false;
+
+		if (is_indian_unit(who)) {
 			let home = indian_home_settlement(who);
-			if (home && has_friendly_allied_settlement(home) && !has_enemy_units(home))
+			// 10.412: Cherokee have no home settlement
+			if (home && has_friendly_allied_settlement(home) && !has_enemy_units(home)) {
+				can_go_home = true;
 				gen_action_space(home);
-			else
-				gen_action('eliminate');
+			}
+
+			if (has_unbesieged_friendly_leader(from)) {
+				can_go_home = true;
+				find_closest_friendly_unbesieged_fortification(from).forEach(gen_action_space);
+			} else if (game.go_home.follow && game.go_home.follow[from]) {
+				can_go_home = true;
+				game.go_home.follow[from].forEach(gen_action_space);
+			}
 		} else {
-			let from = piece_space(who);
-			game.go_home.closest.forEach(gen_action_space);
-			if (from in game.go_home.indians)
-				game.go_home.indians[from].forEach(gen_action_space);
+			// Leader alone in the wilderness
+			find_closest_friendly_unbesieged_fortification(from).forEach(gen_action_space);
 		}
+
+		if (!can_go_home)
+			gen_action('eliminate');
 	},
-	space(s) {
-		push_undo();
+	space(to) {
+		// TODO: push_undo() here?
 		let who = game.go_home.who;
+		let from = game.go_home.from;
+		log(`${piece_name(who)} went home to ${space_name(to)}.`);
+		move_piece_to(who, to);
 		if (is_indian_unit(who)) {
-			let from = piece_space(who);
-			if (!(s in game.go_home.indians))
-				game.go_home.indians[from] = [];
-			game.go_home.indians[from].push(s);
+			let home = indian_home_settlement(who);
+			if (to !== home) {
+				if (game.go_home.follow[from]) {
+					if (game.go_home.follow[from].includes(to)) {
+						game.count = 0;
+					} else {
+						game.go_home.follow[from].push(to);
+						game.count = 1;
+					}
+				} else {
+					game.go_home.follow[from] = [ to ];
+					game.count = 1;
+				}
+			}
+			if (game.count > 0 || can_follow_indians_home(from)) {
+				game.go_home.to = to;
+				game.state = 'go_home_with_indians';
+			} else {
+				game.go_home.who = 0;
+				game.state = 'go_home_who';
+			}
+		} else {
+			// Leader alone in the wilderness
+			game.go_home.who = 0;
+			game.state = 'go_home_who';
 		}
-		log(`${piece_name(who)} goes home to ${space_name(s)}.`);
-		move_piece_to(who, s);
-		resume_indians_and_leaders_go_home();
 	},
 	eliminate() {
-		push_undo();
 		eliminate_piece(game.go_home.who);
-		resume_indians_and_leaders_go_home();
+		game.go_home.who = 0;
+		game.state = 'go_home_who';
 	},
-	next() {
-		end_indians_and_leaders_go_home();
+}
+
+function can_follow_indians_home(from) {
+	for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) {
+		if (is_piece_in_space(p, from) && !is_piece_inside(p))
+			return true;
 	}
+	for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) {
+		if (is_coureurs_unit(p) && is_piece_in_space(p, from) && !is_piece_inside(p))
+			return true;
+	}
+	return false;
+}
+
+states.go_home_with_indians = {
+	prompt() {
+		let who = game.go_home.who;
+		let from = game.go_home.from;
+		let to = game.go_home.to;
+
+		view.prompt = `Indians and leaders go home \u2014 follow ${piece_name(who)} to ${space_name(to)}.`;
+		view.where = to;
+
+		for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) {
+			if (is_piece_in_space(p, from) && !is_piece_inside(p))
+				gen_action_piece(p);
+		}
+		for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) {
+			if (is_coureurs_unit(p) && is_piece_in_space(p, from) && !is_piece_inside(p))
+				gen_action_piece(p);
+		}
+
+		if (game.count === 0)
+			gen_action_next();
+	},
+	piece(p) {
+		push_undo();
+		let from = game.go_home.from;
+		let to = game.go_home.to;
+
+		log(`${piece_name(p)} followed to ${space_name(to)}.`);
+		move_piece_to(p, to);
+		if (game.count > 0 && is_leader(p))
+			game.count = 0;
+
+		if (!can_follow_indians_home(from))
+			end_go_home_with_indians();
+	},
+	next() {
+		push_undo();
+		end_go_home_with_indians();
+	},
+}
+
+function end_go_home_with_indians() {
+	game.go_home.who = 0;
+	game.go_home.from = 0;
+	game.go_home.to = 0;
+	game.state = 'go_home_who';
 }
 
 // LATE SEASON - REMOVE RAIDED MARKERS
-- 
cgit v1.2.3