summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--play.css20
-rw-r--r--play.html1
-rw-r--r--play.js170
-rw-r--r--rules.js142
-rw-r--r--tools/borders.svg626
-rw-r--r--tools/coord.js7
-rw-r--r--tools/genborders.js53
-rw-r--r--tools/makeborders.js29
-rw-r--r--tools/slice.sh16
9 files changed, 1044 insertions, 20 deletions
diff --git a/play.css b/play.css
index 19731a1..d6aac5a 100644
--- a/play.css
+++ b/play.css
@@ -147,6 +147,26 @@ header.your_turn { background-color: orange; }
visibility: hidden;
}
+.border {
+ position: absolute;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ text-align: center;
+ line-height: 24px;
+ font-size: 16px;
+ font-weight: bold;
+ color: #000c;
+ background-color: #535a26;
+}
+
+.border.sea { background-color: #ecb30c; }
+.border.major { background-color: #cbc183; }
+.border.minor { background-color: #946127; }
+.border.river { background-color: #91a1a9; }
+.border.Lancaster { background-color: brown; color: #fed; }
+.border.York { background-color: #fdfefc; color: #ae2e24; }
+
/* BLOCKS */
body.shift .block.known:hover {
diff --git a/play.html b/play.html
index 538aeb6..01194aa 100644
--- a/play.html
+++ b/play.html
@@ -529,6 +529,7 @@ l0 -3 539 0 540 0 2 2 3 3 -1 78 c0 60 0 79 1 83 3 11 14 23 25 27 5 1 24 1
</g>
</svg>
+<div id="borders"></div>
<div id="blocks"></div>
<div id="offmap" style="visibility:hidden"></div>
</div>
diff --git a/play.js b/play.js
index 2870dff..7071231 100644
--- a/play.js
+++ b/play.js
@@ -1,5 +1,37 @@
"use strict"
+function set_has(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function map_get(map, key, missing) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return map[(m<<1)+1]
+ }
+ return missing
+}
+
const LANCASTER = "Lancaster"
const YORK = "York"
const ENEMY = { York: "Lancaster", Lancaster: "York" }
@@ -76,6 +108,100 @@ const LONG_NAME = {
"Gloucester": "Duke of Gloucester",
}
+// :!r node tools/genborders.js
+const BORDERS_XY = {
+ "English Channel / Calais": [1317,1792],
+ "English Channel / France": [378,125],
+ "English Channel / Cornwall": [382,1762],
+ "English Channel / Dorset": [860,1660],
+ "English Channel / Kent": [1437,1565],
+ "English Channel / Sussex": [1090,1631],
+ "Irish Sea / France": [185,245],
+ "Irish Sea / Ireland": [176,396],
+ "Irish Sea / Scotland": [575,284],
+ "Irish Sea / Caernarvon": [524,816],
+ "Irish Sea / Chester": [627,782],
+ "Irish Sea / Cornwall": [286,1646],
+ "Irish Sea / Cumbria": [570,482],
+ "Irish Sea / Glamorgan": [459,1366],
+ "Irish Sea / Isle of Man": [389,467],
+ "Irish Sea / Lancaster": [645,654],
+ "Irish Sea / Pembroke": [303,1187],
+ "Irish Sea / Somerset": [688,1418],
+ "North Sea / Calais": [1565,1760],
+ "North Sea / Scotland": [844,16],
+ "North Sea / East Anglia": [1582,972],
+ "North Sea / East Yorks": [1224,643],
+ "North Sea / Essex": [1504,1295],
+ "North Sea / Kent": [1562,1445],
+ "North Sea / Lincoln": [1305,801],
+ "North Sea / Middlesex": [1382,1392],
+ "North Sea / Northumbria": [952,248],
+ "North Sea / Rutland": [1298,965],
+ "Scotland / Cumbria": [679,248],
+ "Scotland / Northumbria": [774,171],
+ "Caernarvon / Chester": [677,920],
+ "Caernarvon / Pembroke": [479,1071],
+ "Caernarvon / Powys": [522,995],
+ "Chester / Derby": [842,879],
+ "Chester / Hereford": [717,1012],
+ "Chester / Lancaster": [762,794],
+ "Chester / Powys": [610,986],
+ "Chester / Warwick": [793,982],
+ "Cornwall / Dorset": [613,1627],
+ "Cornwall / Somerset": [528,1574],
+ "Cumbria / Lancaster": [740,517],
+ "Cumbria / North Yorks": [819,478],
+ "Cumbria / Northumbria": [782,342],
+ "Derby / Lancaster": [881,784],
+ "Derby / Leicester": [1029,943],
+ "Derby / Lincoln": [1098,840],
+ "Derby / South Yorks": [986,814],
+ "Derby / Warwick": [878,981],
+ "Dorset / Somerset": [716,1566],
+ "Dorset / Sussex": [965,1582],
+ "Dorset / Wilts": [879,1558],
+ "East Anglia / Essex": [1452,1168],
+ "East Anglia / Rutland": [1354,1042],
+ "East Yorks / North Yorks": [971,521],
+ "East Yorks / Northumbria": [997,431],
+ "East Yorks / South Yorks": [1039,654],
+ "Essex / Leicester": [1218,1165],
+ "Essex / Middlesex": [1300,1261],
+ "Essex / Rutland": [1287,1132],
+ "Glamorgan / Hereford": [712,1226],
+ "Glamorgan / Pembroke": [447,1274],
+ "Glamorgan / Powys": [563,1195],
+ "Gloucester / Hereford": [799,1282],
+ "Gloucester / Oxford": [956,1254],
+ "Gloucester / Somerset": [786,1397],
+ "Gloucester / Warwick": [881,1208],
+ "Gloucester / Wilts": [913,1351],
+ "Hereford / Powys": [640,1112],
+ "Hereford / Warwick": [793,1127],
+ "Kent / Middlesex": [1329,1401],
+ "Kent / Sussex": [1276,1509],
+ "Lancaster / North Yorks": [791,623],
+ "Lancaster / South Yorks": [880,701],
+ "Leicester / Lincoln": [1145,937],
+ "Leicester / Middlesex": [1157,1180],
+ "Leicester / Oxford": [1051,1173],
+ "Leicester / Rutland": [1172,1057],
+ "Leicester / Warwick": [974,1048],
+ "Lincoln / Rutland": [1211,967],
+ "Lincoln / South Yorks": [1111,741],
+ "Middlesex / Oxford": [1138,1298],
+ "Middlesex / Sussex": [1152,1413],
+ "North Yorks / Northumbria": [905,434],
+ "North Yorks / South Yorks": [939,611],
+ "Oxford / Sussex": [1085,1404],
+ "Oxford / Warwick": [975,1128],
+ "Oxford / Wilts": [1032,1363],
+ "Pembroke / Powys": [517,1141],
+ "Somerset / Wilts": [838,1452],
+ "Sussex / Wilts": [1018,1495],
+}
+
function toggle_blocks() {
document.getElementById("map").classList.toggle("hide_blocks")
}
@@ -85,6 +211,7 @@ let ui = {
card_backs: {},
areas: {},
blocks: {},
+ borders: [],
battle_menu: {},
battle_block: {},
present: new Set(),
@@ -458,6 +585,23 @@ function build_map() {
build_battle_block(b, block)
build_map_block(b, block)
}
+
+ for (let name in BORDERS_XY) {
+ let [x, y] = BORDERS_XY[name]
+ let [a, b] = name.split(" / ")
+ let id = area_index[a] * 100 + area_index[b]
+ let type = BORDERS[id] || "sea"
+ if (type !== "sea") {
+ let e = document.createElement("div")
+ e.my_id = id
+ e.my_show = "border " + type
+ e.className = "hide"
+ e.style.left = (x - 12) + "px"
+ e.style.top = (y - 12) + "px"
+ ui.borders.push(e)
+ document.getElementById("borders").appendChild(e)
+ }
+ }
}
function update_steps(b, steps, element) {
@@ -672,6 +816,32 @@ function update_map() {
if (view.who !== NOBODY)
ui.blocks[view.who].classList.add('selected')
}
+
+ for (let e of ui.borders) {
+ let u = map_get(view.last_used, e.my_id, 0)
+ let n = map_get(view.border_limit, e.my_id, "")
+ if (view.main_border && set_has(view.main_border, e.my_id))
+ n += "*"
+ switch (u) {
+ case 1:
+ e.className = "border Lancaster"
+ e.textContent = n
+ break
+ case 2:
+ e.className = "border York"
+ e.textContent = n
+ break
+ case 0:
+ if (n) {
+ e.className = e.my_show
+ e.textContent = n
+ } else {
+ e.className = "hide"
+ e.textContent = ""
+ }
+ break
+ }
+ }
}
function update_cards() {
diff --git a/rules.js b/rules.js
index 5efce8c..9bca996 100644
--- a/rules.js
+++ b/rules.js
@@ -27,6 +27,9 @@ const ENEMY = { Lancaster: "York", York: "Lancaster" }
const OBSERVER = "Observer"
const BOTH = "Both"
+const PLAYER_ID = { "": 0, Lancaster: 1, York: 2 }
+const ID_PLAYER = [ "", "Lancaster", "York" ]
+
// areas
const NOWHERE = 0
const POOL = 1
@@ -608,11 +611,11 @@ function border_id(a, b) {
}
function border_was_last_used_by_enemy(from, to) {
- return game.last_used[border_id(from, to)] === ENEMY[game.active]
+ return map_get(game.last_used, border_id(from, to), 0) === PLAYER_ID[ENEMY[game.active]]
}
function border_was_last_used_by_active(from, to) {
- return game.last_used[border_id(from, to)] === game.active
+ return map_get(game.last_used, border_id(from, to), 0) === PLAYER_ID[game.active]
}
function border_type(a, b) {
@@ -620,11 +623,15 @@ function border_type(a, b) {
}
function border_limit(a, b) {
- return game.border_limit[border_id(a,b)] || 0
+ return map_get(game.border_limit, border_id(a,b), 0)
+}
+
+function set_border_limit(a, b, n) {
+ map_set(game.border_limit, border_id(a,b), n)
}
function reset_border_limits() {
- game.border_limit = {}
+ game.border_limit.length = 0
}
function count_friendly(where) {
@@ -1399,7 +1406,7 @@ function start_game_turn() {
// Reset movement and attack tracking state
reset_border_limits()
- game.last_used = {}
+ game.last_used = []
game.attacker = {}
set_clear(game.reserves)
set_clear(game.moved)
@@ -1733,7 +1740,7 @@ states.muster_move_2 = {
// ACTION PHASE
function use_border(from, to) {
- game.border_limit[border_id(from, to)] = border_limit(from, to) + 1
+ set_border_limit(from, to, border_limit(from, to) + 1)
}
function move_block(who, from, to) {
@@ -1741,12 +1748,12 @@ function move_block(who, from, to) {
use_border(from, to)
game.distance ++
if (is_contested_area(to)) {
- game.last_used[border_id(from, to)] = game.active
+ map_set(game.last_used, border_id(from, to), PLAYER_ID[game.active])
if (!game.attacker[to]) {
game.attacker[to] = game.active
- game.main_border[to] = from
+ map_set(game.main_border, to, from)
} else {
- if (game.attacker[to] !== game.active || game.main_border[to] !== from) {
+ if (game.attacker[to] !== game.active || map_get(game.main_border, to, 0) !== from) {
set_add(game.reserves, who)
return RESERVE_MARK
}
@@ -1760,8 +1767,8 @@ function goto_action_phase(moves) {
game.state = 'action_phase'
game.moves = moves
game.activated = []
- game.move_port = {}
- game.main_border = {}
+ game.move_port = []
+ game.main_border = []
game.turn_log = []
game.recruit_log = []
clear_undo()
@@ -1812,7 +1819,7 @@ states.action_phase = {
}
if (can_block_sea_move(b)) {
if (game.moves === 0) {
- if (game.move_port[game.location[b]] !== undefined)
+ if (map_has(game.move_port, game.location[b]))
gen_action(view, 'block', b)
} else {
gen_action(view, 'block', b)
@@ -1892,7 +1899,7 @@ states.move_to = {
let has_destination_port = false
if (game.moves === 0) {
for (let port of AREAS[to].exits)
- if (game.move_port[game.origin] === port)
+ if (map_get(game.move_port, game.origin) === port)
has_destination_port = true
} else {
if (game.active === game.piracy)
@@ -1923,6 +1930,7 @@ states.move_to = {
log_move_start(from)
game.last_from = from
if (is_sea_area(to)) {
+ // XXX use_border(from, to) // if displaying sea moves
log_move_continue(to)
game.location[game.who] = to
game.state = 'sea_move_to'
@@ -1953,7 +1961,7 @@ states.sea_move_to = {
continue
if (is_friendly_or_vacant_area(to)) {
if (game.moves === 0) {
- if (game.move_port[game.origin] === to)
+ if (map_get(game.move_port, game.origin) === to)
gen_action(view, 'area', to)
} else {
gen_action(view, 'area', to)
@@ -1965,6 +1973,8 @@ states.sea_move_to = {
}
},
area: function (to) {
+ // XXX use_border(game.location[game.who], to) // if displaying sea moves
+
game.location[game.who] = to
set_add(game.moved, game.who)
@@ -1979,13 +1989,13 @@ states.sea_move_to = {
} else {
// Can sea move two blocks between same major ports for 1 AP.
log_move_continue(to)
- if (game.move_port[game.origin] === to) {
- delete game.move_port[game.origin]
+ if (map_get(game.move_port, game.origin) === to) {
+ map_delete(game.move_port, game.origin)
} else {
logp("sea moved.")
--game.moves
if (is_major_port(game.origin) && is_major_port(to))
- game.move_port[game.origin] = to
+ map_set(game.move_port, game.origin, to)
}
}
@@ -2020,6 +2030,7 @@ function end_action() {
// BATTLE PHASE
function goto_battle_phase() {
+ reset_border_limits()
if (have_contested_areas()) {
game.active = game.p1
game.state = 'battle_phase'
@@ -3463,9 +3474,9 @@ exports.setup = function (seed, scenario, options) {
reserves: [],
attacker: {},
- border_limit: {},
- last_used: {},
- main_border: {},
+ border_limit: [],
+ last_used: [],
+ main_border: [],
}
// Old RNG for ancient replays
@@ -3537,10 +3548,18 @@ exports.view = function(state, current) {
steps: game.steps,
moved: game.moved,
dead: game.dead,
+ last_used: game.last_used,
+ border_limit: game.border_limit,
battle: null,
prompt: null,
}
+ if (game.main_border && game.main_border.length > 0) {
+ view.main_border = []
+ for (let i = 0; i < game.main_border.length; i += 2)
+ set_add(view.main_border, border_id(game.main_border[i+0], game.main_border[i+1]))
+ }
+
states[game.state].prompt(view, current)
if (states[game.state].show_battle)
@@ -3560,6 +3579,13 @@ function array_remove(array, index) {
return array
}
+function array_remove_pair(array, index) {
+ let n = array.length
+ for (let i = index + 2; i < n; ++i)
+ array[i - 2] = array[i]
+ array.length = n - 2
+}
+
// insert item at index (faster than splice)
function array_insert(array, index, item) {
for (let i = array.length; i > index; --i)
@@ -3568,6 +3594,15 @@ function array_insert(array, index, item) {
return array
}
+function array_insert_pair(array, index, key, value) {
+ for (let i = array.length; i > index; i -= 2) {
+ array[i] = array[i-2]
+ array[i+1] = array[i-1]
+ }
+ array[index] = key
+ array[index+1] = value
+}
+
function set_clear(set) {
set.length = 0
}
@@ -3620,6 +3655,73 @@ function set_delete(set, item) {
return set
}
+function map_has(map, key) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function map_get(map, key, missing) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return map[(m<<1)+1]
+ }
+ return missing
+}
+
+function map_set(map, key, value) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else {
+ map[(m<<1)+1] = value
+ return
+ }
+ }
+ array_insert_pair(map, a<<1, key, value)
+}
+
+function map_delete(map, item) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else {
+ array_remove_pair(map, m<<1)
+ return
+ }
+ }
+}
+
// Fast deep copy for objects without cycles
function object_copy(original) {
if (Array.isArray(original)) {
diff --git a/tools/borders.svg b/tools/borders.svg
new file mode 100644
index 0000000..af5d4a5
--- /dev/null
+++ b/tools/borders.svg
@@ -0,0 +1,626 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1688"
+ height="1950"
+ version="1.1"
+ id="svg184"
+ sodipodi:docname="borders.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata190">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs188" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="640"
+ inkscape:window-height="480"
+ id="namedview186"
+ showgrid="false"
+ inkscape:zoom="1.1527496"
+ inkscape:cx="1088.4392"
+ inkscape:cy="1254.8844"
+ inkscape:current-layer="svg184"
+ inkscape:document-rotation="0" />
+ <image
+ sodipodi:absref="/home/tor/src/rally/public/richard-iii/map75.png"
+ xlink:href="../map75.png"
+ x="0"
+ y="0"
+ width="1688"
+ height="1950"
+ image-rendering="pixelated"
+ sodipodi:insensitive="true"
+ id="image2" />
+ <circle
+ inkscape:label="English Channel / Calais"
+ cx="1317.2211"
+ cy="1791.8992"
+ r="12"
+ id="circle4"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="English Channel / France"
+ cx="378.1528"
+ cy="124.8569"
+ r="12"
+ id="circle6"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="English Channel / Cornwall"
+ cx="381.73529"
+ cy="1761.797"
+ r="12"
+ id="circle8"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="English Channel / Dorset"
+ cx="860.13171"
+ cy="1660.2496"
+ r="12"
+ id="circle10"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="English Channel / Kent"
+ cx="1437.1509"
+ cy="1564.6982"
+ r="12"
+ id="circle12"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="English Channel / Sussex"
+ cx="1089.8998"
+ cy="1631.4418"
+ r="12"
+ id="circle14"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / France"
+ cx="185.40173"
+ cy="245.46254"
+ r="12"
+ id="circle16"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Ireland"
+ cx="176.3347"
+ cy="396.28262"
+ r="12"
+ id="circle18"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Scotland"
+ cx="574.70367"
+ cy="283.78055"
+ r="12"
+ id="circle20"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Caernarvon"
+ cx="523.86005"
+ cy="815.64746"
+ r="12"
+ id="circle22"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Chester"
+ cx="627.04523"
+ cy="782.03564"
+ r="12"
+ id="circle24"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Cornwall"
+ cx="286.44171"
+ cy="1645.9286"
+ r="12"
+ id="circle26"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Cumbria"
+ cx="570.30176"
+ cy="482.10016"
+ r="12"
+ id="circle28"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Glamorgan"
+ cx="459.25238"
+ cy="1366.3389"
+ r="12"
+ id="circle30"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Isle of Man"
+ cx="388.50479"
+ cy="466.7449"
+ r="12"
+ id="circle32"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Lancaster"
+ cx="645.15912"
+ cy="653.99316"
+ r="12"
+ id="circle34"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Pembroke"
+ cx="303.14954"
+ cy="1187.4073"
+ r="12"
+ id="circle36"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Irish Sea / Somerset"
+ cx="688.13031"
+ cy="1418.3497"
+ r="12"
+ id="circle38"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Calais"
+ cx="1565.3402"
+ cy="1760.4211"
+ r="12"
+ id="circle40"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Scotland"
+ cx="843.83649"
+ cy="16.06251"
+ r="12"
+ id="circle42"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / East Anglia"
+ cx="1581.7091"
+ cy="972.2962"
+ r="12"
+ id="circle44"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / East Yorks"
+ cx="1224.0212"
+ cy="642.82007"
+ r="12"
+ id="circle46"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Essex"
+ cx="1503.5902"
+ cy="1294.7825"
+ r="12"
+ id="circle48"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Kent"
+ cx="1562.2502"
+ cy="1444.588"
+ r="12"
+ id="circle50"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Lincoln"
+ cx="1304.6338"
+ cy="801.10284"
+ r="12"
+ id="circle52"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Middlesex"
+ cx="1382.402"
+ cy="1391.8545"
+ r="12"
+ id="circle54"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Northumbria"
+ cx="951.58667"
+ cy="247.53162"
+ r="12"
+ id="circle56"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="North Sea / Rutland"
+ cx="1298.3813"
+ cy="965.31958"
+ r="12"
+ id="circle58"
+ style="fill:#3b4dcf;fill-opacity:1" />
+ <circle
+ inkscape:label="Scotland / Cumbria"
+ cx="679.41974"
+ cy="247.78326"
+ r="12"
+ id="circle60" />
+ <circle
+ inkscape:label="Scotland / Northumbria"
+ cx="774.32373"
+ cy="171.45955"
+ r="12"
+ id="circle62" />
+ <circle
+ inkscape:label="Caernarvon / Chester"
+ cx="677.22357"
+ cy="919.91083"
+ r="12"
+ id="circle64" />
+ <circle
+ inkscape:label="Caernarvon / Pembroke"
+ cx="479.18152"
+ cy="1071.2867"
+ r="12"
+ id="circle66" />
+ <circle
+ inkscape:label="Caernarvon / Powys"
+ cx="522"
+ cy="995"
+ r="12"
+ id="circle68" />
+ <circle
+ inkscape:label="Chester / Derby"
+ cx="841.73511"
+ cy="878.95709"
+ r="12"
+ id="circle70" />
+ <circle
+ inkscape:label="Chester / Hereford"
+ cx="717"
+ cy="1012"
+ r="12"
+ id="circle72" />
+ <circle
+ inkscape:label="Chester / Lancaster"
+ cx="762.40192"
+ cy="794.37726"
+ r="12"
+ id="circle74" />
+ <circle
+ inkscape:label="Chester / Powys"
+ cx="610.23871"
+ cy="986.29907"
+ r="12"
+ id="circle76" />
+ <circle
+ inkscape:label="Chester / Warwick"
+ cx="793.16736"
+ cy="981.92181"
+ r="12"
+ id="circle78" />
+ <circle
+ inkscape:label="Cornwall / Dorset"
+ cx="613.08881"
+ cy="1626.8434"
+ r="12"
+ id="circle80" />
+ <circle
+ inkscape:label="Cornwall / Somerset"
+ cx="528.29218"
+ cy="1573.7902"
+ r="12"
+ id="circle82" />
+ <circle
+ inkscape:label="Cumbria / Lancaster"
+ cx="739.91083"
+ cy="517.10699"
+ r="12"
+ id="circle84" />
+ <circle
+ inkscape:label="Cumbria / North Yorks"
+ cx="819.25238"
+ cy="478.09601"
+ r="12"
+ id="circle86" />
+ <circle
+ inkscape:label="Cumbria / Northumbria"
+ cx="782"
+ cy="342"
+ r="12"
+ id="circle88" />
+ <circle
+ inkscape:label="Derby / Lancaster"
+ cx="880.53363"
+ cy="784.37726"
+ r="12"
+ id="circle90" />
+ <circle
+ inkscape:label="Derby / Leicester"
+ cx="1029.3416"
+ cy="943.33472"
+ r="12"
+ id="circle92" />
+ <circle
+ inkscape:label="Derby / Lincoln"
+ cx="1098.0782"
+ cy="840.03564"
+ r="12"
+ id="circle94" />
+ <circle
+ inkscape:label="Derby / South Yorks"
+ cx="986.32373"
+ cy="814.30591"
+ r="12"
+ id="circle96" />
+ <circle
+ inkscape:label="Derby / Warwick"
+ cx="877.66943"
+ cy="980.64062"
+ r="12"
+ id="circle98" />
+ <circle
+ inkscape:label="Dorset / Somerset"
+ cx="716.47736"
+ cy="1566.2812"
+ r="12"
+ id="circle100" />
+ <circle
+ inkscape:label="Dorset / Sussex"
+ cx="964.68726"
+ cy="1581.8723"
+ r="12"
+ id="circle102" />
+ <circle
+ inkscape:label="Dorset / Wilts"
+ cx="878.70093"
+ cy="1558.0961"
+ r="12"
+ id="circle104" />
+ <circle
+ inkscape:label="East Anglia / Essex"
+ cx="1452.0713"
+ cy="1167.5514"
+ r="12"
+ id="circle106" />
+ <circle
+ inkscape:label="East Anglia / Rutland"
+ cx="1354.4843"
+ cy="1041.9039"
+ r="12"
+ id="circle108" />
+ <circle
+ inkscape:label="East Yorks / North Yorks"
+ cx="971.37036"
+ cy="520.69409"
+ r="12"
+ id="circle110" />
+ <circle
+ inkscape:label="East Yorks / Northumbria"
+ cx="997.01782"
+ cy="430.68314"
+ r="12"
+ id="circle112" />
+ <circle
+ inkscape:label="East Yorks / South Yorks"
+ cx="1038.9218"
+ cy="654.40192"
+ r="12"
+ id="circle114" />
+ <circle
+ inkscape:label="Essex / Leicester"
+ cx="1217.6516"
+ cy="1164.9644"
+ r="12"
+ id="circle116" />
+ <circle
+ inkscape:label="Essex / Middlesex"
+ cx="1299.6338"
+ cy="1261.3169"
+ r="12"
+ id="circle118" />
+ <circle
+ inkscape:label="Essex / Rutland"
+ cx="1286.609"
+ cy="1132.0891"
+ r="12"
+ id="circle120" />
+ <circle
+ inkscape:label="Glamorgan / Hereford"
+ cx="711.75031"
+ cy="1225.7545"
+ r="12"
+ id="circle122" />
+ <circle
+ inkscape:label="Glamorgan / Pembroke"
+ cx="447.43951"
+ cy="1273.673"
+ r="12"
+ id="circle124" />
+ <circle
+ inkscape:label="Glamorgan / Powys"
+ cx="562.64062"
+ cy="1195.0713"
+ r="12"
+ id="circle126" />
+ <circle
+ inkscape:label="Gloucester / Hereford"
+ cx="799.41571"
+ cy="1282.0376"
+ r="12"
+ id="circle128" />
+ <circle
+ inkscape:label="Gloucester / Oxford"
+ cx="955.68311"
+ cy="1253.8971"
+ r="12"
+ id="circle130" />
+ <circle
+ inkscape:label="Gloucester / Somerset"
+ cx="786.47028"
+ cy="1396.5815"
+ r="12"
+ id="circle132" />
+ <circle
+ inkscape:label="Gloucester / Warwick"
+ cx="881.19202"
+ cy="1208.0782"
+ r="12"
+ id="circle134" />
+ <circle
+ inkscape:label="Gloucester / Wilts"
+ cx="913.00684"
+ cy="1351.3525"
+ r="12"
+ id="circle136" />
+ <circle
+ inkscape:label="Hereford / Powys"
+ cx="640"
+ cy="1112"
+ r="12"
+ id="circle138" />
+ <circle
+ inkscape:label="Hereford / Warwick"
+ cx="792.65845"
+ cy="1126.9287"
+ r="12"
+ id="circle140" />
+ <circle
+ inkscape:label="Kent / Middlesex"
+ cx="1329.3594"
+ cy="1401.3594"
+ r="12"
+ id="circle142" />
+ <circle
+ inkscape:label="Kent / Sussex"
+ cx="1275.7544"
+ cy="1509.413"
+ r="12"
+ id="circle144" />
+ <circle
+ inkscape:label="Lancaster / North Yorks"
+ cx="790.7298"
+ cy="622.5871"
+ r="12"
+ id="circle146" />
+ <circle
+ inkscape:label="Lancaster / South Yorks"
+ cx="880.07819"
+ cy="700.62274"
+ r="12"
+ id="circle148" />
+ <circle
+ inkscape:label="Leicester / Lincoln"
+ cx="1145"
+ cy="937"
+ r="12"
+ id="circle150" />
+ <circle
+ inkscape:label="Leicester / Middlesex"
+ cx="1157"
+ cy="1180"
+ r="12"
+ id="circle152" />
+ <circle
+ inkscape:label="Leicester / Oxford"
+ cx="1050.7723"
+ cy="1172.6228"
+ r="12"
+ id="circle154" />
+ <circle
+ inkscape:label="Leicester / Rutland"
+ cx="1172"
+ cy="1057"
+ r="12"
+ id="circle156" />
+ <circle
+ inkscape:label="Leicester / Warwick"
+ cx="974.4129"
+ cy="1048.3347"
+ r="12"
+ id="circle158" />
+ <circle
+ inkscape:label="Lincoln / Rutland"
+ cx="1211.4664"
+ cy="967.40192"
+ r="12"
+ id="circle160" />
+ <circle
+ inkscape:label="Lincoln / South Yorks"
+ cx="1111.3237"
+ cy="741.33472"
+ r="12"
+ id="circle162" />
+ <circle
+ inkscape:label="Middlesex / Oxford"
+ cx="1138.4268"
+ cy="1298.3546"
+ r="12"
+ id="circle164" />
+ <circle
+ inkscape:label="Middlesex / Sussex"
+ cx="1152.1249"
+ cy="1412.6763"
+ r="12"
+ id="circle166" />
+ <circle
+ inkscape:label="North Yorks / Northumbria"
+ cx="905.06036"
+ cy="434.40192"
+ r="12"
+ id="circle168" />
+ <circle
+ inkscape:label="North Yorks / South Yorks"
+ cx="938.86829"
+ cy="611.4129"
+ r="12"
+ id="circle170" />
+ <circle
+ inkscape:label="Oxford / Sussex"
+ cx="1084.5089"
+ cy="1404.4308"
+ r="12"
+ id="circle172" />
+ <circle
+ inkscape:label="Oxford / Warwick"
+ cx="975.07819"
+ cy="1127.7229"
+ r="12"
+ id="circle174" />
+ <circle
+ inkscape:label="Oxford / Wilts"
+ cx="1032.4266"
+ cy="1363.1674"
+ r="12"
+ id="circle176" />
+ <circle
+ inkscape:label="Pembroke / Powys"
+ cx="517.39093"
+ cy="1141.3169"
+ r="12"
+ id="circle178" />
+ <circle
+ inkscape:label="Somerset / Wilts"
+ cx="838.11383"
+ cy="1451.9932"
+ r="12"
+ id="circle180" />
+ <circle
+ inkscape:label="Sussex / Wilts"
+ cx="1017.5446"
+ cy="1495.0356"
+ r="12"
+ id="circle182" />
+</svg>
diff --git a/tools/coord.js b/tools/coord.js
new file mode 100644
index 0000000..5776990
--- /dev/null
+++ b/tools/coord.js
@@ -0,0 +1,7 @@
+for (let row=1; row<=9; ++row) {
+ let y = -(row-1) * 62
+ for (let col=1; col<=7; ++col) {
+ let x = -(col-1) * 62
+ console.log(`.known.block_${row}${col}{background-position:${x}px ${y}px}`)
+ }
+}
diff --git a/tools/genborders.js b/tools/genborders.js
new file mode 100644
index 0000000..f639e8a
--- /dev/null
+++ b/tools/genborders.js
@@ -0,0 +1,53 @@
+const fs = require("fs")
+
+const { round, floor, ceil } = Math
+
+let output = {}
+let mode, name, x, y, w, h, cx, cy, rx, ry
+
+function flush() {
+ if (mode === 'circle') {
+ output[name] = [ cx, cy ]
+ }
+ x = y = w = h = cx = cy = rx = ry = 0
+ name = null
+}
+
+for (let line of fs.readFileSync("tools/borders.svg", "utf-8").split("\n")) {
+ line = line.trim()
+ if (line.startsWith("<rect")) {
+ flush()
+ mode = "rect"
+ x = y = w = h = 0
+ } else if (line.startsWith("<ellipse") || line.startsWith("<circle")) {
+ flush()
+ mode = "circle"
+ cx = cy = rx = ry = 0
+ } else if (line.startsWith('x="'))
+ x = round(Number(line.split('"')[1]))
+ else if (line.startsWith('y="'))
+ y = round(Number(line.split('"')[1]))
+ else if (line.startsWith('width="'))
+ w = round(Number(line.split('"')[1]))
+ else if (line.startsWith('height="'))
+ h = round(Number(line.split('"')[1]))
+ else if (line.startsWith('cx="'))
+ cx = round(Number(line.split('"')[1]))
+ else if (line.startsWith('cy="'))
+ cy = round(Number(line.split('"')[1]))
+ else if (line.startsWith('r="'))
+ rx = ry = round(Number(line.split('"')[1]))
+ else if (line.startsWith('rx="'))
+ rx = round(Number(line.split('"')[1]))
+ else if (line.startsWith('ry="'))
+ ry = round(Number(line.split('"')[1]))
+ else if (line.startsWith('inkscape:label="'))
+ name = line.split('"')[1]
+}
+
+flush()
+
+console.log("const BORDERS_XY = {")
+for (let key in output)
+ console.log("\t\"" + key + "\": " + JSON.stringify(output[key]) + ",")
+console.log("}")
diff --git a/tools/makeborders.js b/tools/makeborders.js
new file mode 100644
index 0000000..2dfdbd5
--- /dev/null
+++ b/tools/makeborders.js
@@ -0,0 +1,29 @@
+const print = console.log
+
+const data = require("../data.js")
+
+var w = 1688
+var h = 1950
+var m = "../map75.png"
+
+print(`<?xml version="1.0" encoding="UTF-8"?>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="${w}"
+ height="${h}"
+>
+<image xlink:href="${m}" x="0" y="0" width="${w}" height="${h}" image-rendering="pixelated" sodipodi:insensitive="true" />`)
+
+for (let id in data.BORDERS) {
+ let a = (id / 100) | 0
+ let b = id % 100
+ let x = (data.AREAS[a].layout.x + data.AREAS[b].layout.x) >> 1
+ let y = (data.AREAS[a].layout.y + data.AREAS[b].layout.y) >> 1
+ let label = data.AREAS[a].name + " / " + data.AREAS[b].name
+ print(`<circle inkscape:label="${label}" cx="${x}" cy="${y}" r="12"/>`)
+}
+
+print(`</svg>`)
diff --git a/tools/slice.sh b/tools/slice.sh
new file mode 100644
index 0000000..8053af7
--- /dev/null
+++ b/tools/slice.sh
@@ -0,0 +1,16 @@
+# white ffffff
+# red eb2127
+
+pnmcut -left 247 -top 394 -width 1736 -height 2232 tools/labels.ppm | pnmtopng > blocks300.png
+
+exit
+
+i=1
+for y in 394 642 890 1138 1386 1634 1882 2130 2378
+do
+ for x in 247 495 743 991 1239 1487 1735
+ do
+ pnmcut -left $x -top $y -width 248 -height 248 tools/labels.ppm > tools/b$i.ppm
+ i=$(expr $i + 1)
+ done
+done