summaryrefslogtreecommitdiff
path: root/rules.js
blob: 92ea536d82aa5d5a3b49d5bca50c3df4854827d1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
"use strict"

const FRENCH = "French"
const COALITION = "Coalition"

const P1 = FRENCH
const P2 = COALITION

exports.roles = [ P1, P2 ]

exports.scenarios = [ "June 16-18", "June 15-18" ]

const data = require("./data")

var game = null
var view = null
var states = {}

const OPEN = 0
const TOWN = 1
const STREAM = 2

const OLD_GUARD = 25
const GRAND_BATTERY = 28

function make_piece_list(f) {
	let list = []
	for (let p = 0; p < data.pieces.length; ++p)
		if (f(data.pieces[p]))
			list.push(p)
	return list
}

const p1_hqs = make_piece_list(p => p.side === P1 && p.type === "hq")
const p2_hqs = make_piece_list(p => p.side !== P1 && p.type === "hq")
const p1_cav = make_piece_list(p => p.side === P1 && p.type === "cav")
const p2_cav = make_piece_list(p => p.side !== P1 && p.type === "cav")
const p1_inf = make_piece_list(p => p.side === P1 && p.type === "inf")
const p2_inf = make_piece_list(p => p.side !== P1 && p.type === "inf")
const p1_det = make_piece_list(p => p.side === P1 && p.type === "det")
const p2_det = make_piece_list(p => p.side !== P1 && p.type === "det")
const p1_corps = make_piece_list(p => p.side === P1 && (p.type === "inf" || p.type === "cav"))
const p2_corps = make_piece_list(p => p.side !== P1 && (p.type === "inf" || p.type === "cav"))
const p1_units = make_piece_list(p => p.side === P1 && (p.type === "inf" || p.type === "cav" || p.type === "det"))
const p2_units = make_piece_list(p => p.side !== P1 && (p.type === "inf" || p.type === "cav" || p.type === "det"))

function friendly_hqs() { return (game.active === P1) ? p1_hqs : p2_hqs }
function enemy_hqs() { return (game.active !== P1) ? p1_hqs : p2_hqs }
function friendly_cavalry_corps() { return (game.active === P1) ? p1_cav : p2_cav }
function enemy_cavalry_corps() { return (game.active !== P1) ? p1_cav : p2_cavalry_corps }
function friendly_infantry_corps() { return (game.active === P1) ? p1_inf : p2_inf }
function enemy_infantry_corps() { return (game.active !== P1) ? p1_inf : p2_inf }
function friendly_detachments() { return (game.active === P1) ? p1_det : p2_det }
function enemy_detachments() { return (game.active !== P1) ? p1_det : p2_det }
function friendly_corps() { return (game.active === P1) ? p1_corps : p2_corps }
function enemy_corps() { return (game.active !== P1) ? p1_corps : p2_corps }
function friendly_units() { return (game.active === P1) ? p1_units : p2_units }
function enemy_units() { return (game.active !== P1) ? p1_units : p2_units }

function set_piece_hex(p, hex) {
	game.hex[p] = hex
}

function set_piece_mode(p, mode) {
	game.mode[p] = mode
}

function piece_hex(p) {
	return game.hex[p]
}

function piece_mode(p) {
	return game.mode[p]
}

// === ZONE OF CONTROL / INFLUENCE ===

var zoc_valid = false
var p1_zoc = new Array(data.map.rows * 100).fill(0)
var p1_zoi = new Array(data.map.rows * 100).fill(0)
var p2_zoc = new Array(data.map.rows * 100).fill(0)
var p2_zoi = new Array(data.map.rows * 100).fill(0)

function is_friendly_zoc(x) { return game.active === P1 ? p1_zoc[x] : p2_zoc[x] }
function is_friendly_zoi(x) { return game.active === P1 ? p1_zoi[x] : p2_zoi[x] }
function is_enemy_zoc(x) { return game.active !== P1 ? p1_zoc[x] : p2_zoc[x] }
function is_enemy_zoi(x) { return game.active !== P1 ? p1_zoi[x] : p2_zoi[x] }

function update_zoc_imp(zoc, zoi, units) {
	zoc.fill(0)
	zoi.fill(0)
	for (let p of units) {
		for_each_adjacent(piece_hex(p), x => {
			// TODO: river
			zoc[x - 1000] = 1
			for_each_adjacent(x, y => {
				// TODO: bridge
				zoi[y - 1000] = 1
			})
		})
	}
}

function update_zoc() {
	if (!zoc_valid) {
		zoc_valid = true
		update_zoc_imp(p1_zoc, p1_zoi, p1_units)
		update_zoc_imp(p2_zoc, p2_zoi, p2_units)
	}
}

function is_not_in_enemy_zoc_or_zoi(p) {
	let x = piece_hex(p)
	return !is_enemy_zoc(x) && !is_enemy_zoi(x)
}

function is_map_hex(row, col) {
	return row >= 10 && row <= 40 && col >= 0 && col <= 41
}

function calc_distance(a, b) {
	let ac = a % 100
	let bc = b % 100
	let ay = a / 100 | 0
	let by = b / 100 | 0
	let ax = ac - (ay >> 1)
	let bx = bc - (by >> 1)
	let az = -ax - ay
	let bz = -bx - by
	return max(abs(bx-ax), abs(by-ay), abs(bz-az))
}

function for_each_adjacent(hex, fn) {
	let row = hex / 10 | 0
	let col = hex % 10
	if (col < 41)
		fn(hex + 1)
	if (col > 0)
		fn(hex - 1)
	if (row & 1) {
		if (row < 40) {
			if (col < 41)
				fn(hex + 101)
			fn(hex + 100)
		}
		if (row > 10) {
			fn(hex - 100)
			if (col < 41)
				fn(hex - 99)
		}
	} else {
		if (row < 40) {
			fn(hex + 100)
			if (col > 0)
				fn(hex + 99)
		}
		if (row > 10) {
			if (col > 0)
				fn(hex - 101)
			fn(hex - 100)
		}
	}
}

function prompt(str) {
	view.prompt = str
}

// === === COMMAND PHASE === ===

function goto_command_phase() {
	log("Command Phase")
	log("")
	goto_hq_placement_step()
}

function goto_hq_placement_step() {
	game.active = P1
	game.state = "hq_placement_step"
}

function goto_blown_unit_return_step() {
	game.active = P1
	game.state = "blown_unit_return_step"
	game.count = 2
}

function end_blown_unit_return_step() {
	if (game.active === P1) {
		game.active = P2
		game.count = 2
	} else {
		goto_cavalry_corps_recovery_step()
	}
}

function goto_cavalry_corps_recovery_step() {
	game.active = P1
	game.state = "cavalry_corps_recovery_step"
	resume_cavalry_corps_recovery_step()
}

function resume_cavalry_corps_recovery_step() {
	update_zoc()
	for (let p of friendly_cavalry_corps())
		if (is_not_in_enemy_zoc_or_zoi(p))
			return
	end_cavalry_corps_recovery_step()
}

function end_cavalry_corps_recovery_step() {
	if (game.active === P1) {
		game.active = P2
		resume_cavalry_corps_recovery_step()
	} else {
		goto_detachment_placement_step()
	}
}

function goto_detachment_placement_step() {
	game.active = P1
	game.state = "detachment_placement_step"
	game.count = 0
}

function end_detachment_placement_step() {
	if (game.active === P1) {
		game.active = P2
		game.count = 0
	} else {
		goto_detachment_recall_step()
	}
}

function goto_detachment_recall_step() {
	game.active = P1
	game.state = "detachment_recall_step"
}

function end_detachment_recall_step() {
	if (game.active === P1) {
		game.active = P2
	} else {
		goto_british_line_of_communication_angst()
	}
}

function goto_british_line_of_communication_angst() {
	game.active = P2
	game.state = "british_line_of_communication_angst"
}

/*

command phase:

	remove hq
	place hq
	return up to 2 blown corps
	flip exhausted cav to fresh (move to organization?)
	place 1 detachment per hq
	recall all, some, or no detachments
	angst: substitute Hill unit

organization
	advance formation: flip infantry corps to advance
	battle formation: flip infantry corps to battle
	alternate withdrawal: retreat or pass (3 remain)

movement
	alternate corps movement: move corps or pass

attack
	alternate corps to attack in zoc or pass

end phase
	if last turn - victory
	recall french grand battery
	new turn

*/


// === A: HQ PLACEMENT STEP ===


states.hq_placement_step = {
	prompt() {
		prompt("HQ Placement")
	},
}

states.setup = {
	prompt() {
	},
}

states.edit_town = {
	prompt() {
		view.roads = data.map.roads
	},
}

// === SETUP ===

function setup_piece(side, name, hex, mode = 0) {
	let id = data.pieces.findIndex(pc => pc.side === side && pc.name === name)
	if (id < 0)
		throw new Error("INVALID PIECE NAME: " + name)
	set_piece_hex(id, hex)
	set_piece_mode(id, mode)
}

exports.setup = function (seed, scenario, options) {
	game = {
		seed,
		scenario,
		undo: [],
		log: [],
		active: P1,
		state: null,
		turn: 3,
		pieces: new Array(piece_count).fill(0),
		mode: new Array(piece_count).fill(0),
		remain: 0,
	}

	setup("French", "Napoleon HQ", 1217)
	setup("French", "Guard Corps (Drouot)", 1217)
	setup("French", "Grouchy HQ", 1621)
	setup("French", "Ney HQ", 2218)
	setup("French", "II Corps (Reille)", 2218)
	setup("French", "I Corps (d'Erlon)", 1617)
	setup("French", "III Corps (Vandamme)", 1721)
	setup("French", "IV Corps (Gerard)", 1221)
	setup("French", "VI Corps (Lobau)", 1117)
	setup("French", "Guard Cav Corps (Guyot)", 2317)
	setup("French", "Res Cav Corps (Grouchy)", 1822)
	setup("French", "I Detachment (Jacquinot)", 1314)

	setup("Anglo", "Wellington HQ", 2818, 1)
	setup("Anglo", "Reserve Corps (Wellington)", 3715)
	setup("Anglo", "I Corps (Orange)", 3002)
	setup("Anglo", "II Corps (Hill*)", 3)
	setup("Anglo", "Cav Corps (Uxbridge)", 4)
	setup("Anglo", "Cav Detachment (Collaert)", 1211)
	setup("Anglo", "I Detachment (Perponcher)", 2618)

	setup("Prussian", "Blucher HQ", 2324)
	setup("Prussian", "Cav Corps (Gneisenau)", 2324, 1)
	setup("Prussian", "I Corps (Ziethen)", 1922, 1)
	setup("Prussian", "II Corps (Pirch)", 1928)
	setup("Prussian", "III Corps (Thielmann)", 1737)
	setup("Prussian", "IV Corps (Bulow)", 3)
	setup("Prussian", "I Detachment (Lutzow)", 1623)

	goto_command_phase()

	return game
}

// === COMMON ===

exports.view = function (state, player) {
	view = {
		prompt: null,
		actions: null,
		log: game.log,
		hex: game.hex,
		mode: game.mode,
	}

	if (game.state === "game_over") {
		view.prompt = game.victory
	} else if (game.active !== player) {
		let inactive = states[game.state].inactive || game.state
		view.prompt = `Waiting for ${game.active} \u2014 ${inactive}.`
	} else {
		view.actions = {}
		view.who = game.who
		if (states[game.state])
			states[game.state].prompt(current)
		else
			view.prompt = "Unknown state: " + game.state
		if (view.actions.undo === undefined) {
			if (game.undo && game.undo.length > 0)
				view.actions.undo = 1
			else
				view.actions.undo = 0
		}
	}

	return view
}

exports.action = function (state, player, action, arg) {
	game = state
	let S = states[game.state]
	if (action in S)
		S[action](arg, player)
	else if (action === "undo" && game.undo && game.undo.length > 0)
		pop_undo()
	else
		throw new Error("Invalid action: " + action)
	return game
}

exports.resign = function (state, player) {
	game = state
	if (game.state !== 'game_over') {
		if (player === RED)
			goto_game_over(BLUE, RED + " resigned.")
		if (player === BLUE)
			goto_game_over(RED, BLUE + " resigned.")
	}
	return game
}

// === COMMON LIBRARY ===

function log(msg) {
	game.log.push(msg)
}

function clear_undo() {
	game.undo.length = 0
}

function push_undo() {
	let copy = {}
	for (let k in game) {
		let v = game[k]
		if (k === "undo")
			continue
		else if (k === "log")
			v = v.length
		else if (typeof v === "object" && v !== null)
			v = object_copy(v)
		copy[k] = v
	}
	game.undo.push(copy)
}

function pop_undo() {
	let save_log = game.log
	let save_undo = game.undo
	game = save_undo.pop()
	save_log.length = game.log
	game.log = save_log
	game.undo = save_undo
}

function random(range) {
	// An MLCG using integer arithmetic with doubles.
	// https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
	// m = 2**35 − 31
	return (game.seed = game.seed * 200105 % 34359738337) % range
}

// Fast deep copy for objects without cycles
function object_copy(original) {
	if (Array.isArray(original)) {
		let n = original.length
		let copy = new Array(n)
		for (let i = 0; i < n; ++i) {
			let v = original[i]
			if (typeof v === "object" && v !== null)
				copy[i] = object_copy(v)
			else
				copy[i] = v
		}
		return copy
	} else {
		let copy = {}
		for (let i in original) {
			let v = original[i]
			if (typeof v === "object" && v !== null)
				copy[i] = object_copy(v)
			else
				copy[i] = v
		}
		return copy
	}
}