diff options
author | Tor Andersson <tor@ccxvii.net> | 2023-01-18 13:28:02 +0100 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2023-02-18 13:02:39 +0100 |
commit | 26073215b9a279d3ea77274143da385615dd8a65 (patch) | |
tree | e7f2bf59efb4d458807b03a796818aae9da7505f | |
parent | e73f785e5353c5b7b0fc437c8ce2cbbc82adbd1e (diff) | |
download | nevsky-26073215b9a279d3ea77274143da385615dd8a65.tar.gz |
2E supply rules.
Only show Supply button if supply is possible.
Smarter Lodya when Novgorod is in enemy hands.
-rw-r--r-- | data.js | 106 | ||||
-rw-r--r-- | play.html | 10 | ||||
-rw-r--r-- | play.js | 3 | ||||
-rw-r--r-- | rules.js | 639 | ||||
-rw-r--r-- | tools/gendata.js | 20 |
5 files changed, 565 insertions, 213 deletions
@@ -6,59 +6,59 @@ strongholds:[0,1,7,8,9,10,11,12,13,24,25,26,27,32,33,34,35,36], steppe_warriors:[18,19,22,23], summer_crusaders:[2,13], locales:[ -{"name":"Reval","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Danish Estonia","ways":[[5,25],[3,32]],"box":{"x":601,"y":3564,"w":206,"h":91},"adjacent":[3,5],"adjacent_by_trackway":[3,5],"adjacent_by_waterway":[]}, -{"name":"Wesenberg","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Danish Estonia","ways":[[5,26],[17,30],[6,31]],"box":{"x":1448,"y":3625,"w":304,"h":60},"adjacent":[5,6,17],"adjacent_by_trackway":[5,6,17],"adjacent_by_waterway":[]}, -{"name":"Narwia","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[7,2],[38,2],[49,2],[46,17],[6,27],[33,28]],"box":{"x":2371,"y":3549,"w":123,"h":31},"adjacent":[6,7,33,38,46,49],"adjacent_by_trackway":[6,33],"adjacent_by_waterway":[7,38,46,49]}, -{"name":"Warbola","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[0,32],[4,33],[8,35]],"box":{"x":292,"y":3797,"w":142,"h":31},"adjacent":[0,4,8],"adjacent_by_trackway":[0,4,8],"adjacent_by_waterway":[]}, -{"name":"Harrien","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[3,33],[17,34]],"box":{"x":567,"y":3983,"w":200,"h":100},"adjacent":[3,17],"adjacent_by_trackway":[3,17],"adjacent_by_waterway":[]}, -{"name":"Revala","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[0,25],[1,26]],"box":{"x":1030,"y":3410,"w":200,"h":100},"adjacent":[0,1],"adjacent_by_trackway":[0,1],"adjacent_by_waterway":[]}, -{"name":"Wierland","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[2,27],[23,29],[1,31]],"box":{"x":1999,"y":3680,"w":200,"h":100},"adjacent":[1,2,23],"adjacent_by_trackway":[1,2,23],"adjacent_by_waterway":[]}, -{"name":"Dorpat","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[12,0,3],[22,0],[2,2],[38,2],[49,2],[11,3],[23,36]],"box":{"x":1625,"y":4589,"w":253,"h":91},"adjacent":[2,11,12,22,23,38,49],"adjacent_by_trackway":[12,22,23],"adjacent_by_waterway":[2,11,12,38,49]}, -{"name":"Leal","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[3,35],[15,37]],"box":{"x":108,"y":4266,"w":205,"h":91},"adjacent":[3,15],"adjacent_by_trackway":[3,15],"adjacent_by_waterway":[]}, -{"name":"Riga","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[13,8]],"box":{"x":273,"y":6231,"w":205,"h":91},"adjacent":[13],"adjacent_by_trackway":[],"adjacent_by_waterway":[13]}, -{"name":"Adsel","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[13,9],[18,44],[21,44],[14,45]],"box":{"x":1504,"y":5612,"w":185,"h":60},"adjacent":[13,14,18,21],"adjacent_by_trackway":[14,18,21],"adjacent_by_waterway":[13]}, -{"name":"Fellin","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[7,3],[12,3],[15,7],[17,7],[20,38]],"box":{"x":1013,"y":4583,"w":184,"h":61},"adjacent":[7,12,15,17,20],"adjacent_by_trackway":[20],"adjacent_by_waterway":[7,12,15,17]}, -{"name":"Odenpäh","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[7,0,3],[22,0],[11,3],[14,46]],"box":{"x":1378,"y":5103,"w":250,"h":61},"adjacent":[7,11,14,22],"adjacent_by_trackway":[7,14,22],"adjacent_by_waterway":[7,11]}, -{"name":"Wenden","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[9,8],[10,9],[19,40],[21,41]],"box":{"x":909,"y":5759,"w":232,"h":60},"adjacent":[9,10,19,21],"adjacent_by_trackway":[19,21],"adjacent_by_waterway":[9,10]}, -{"name":"Kirrumpäh","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[10,45],[12,46],[32,47]],"box":{"x":1877,"y":5389,"w":175,"h":30},"adjacent":[10,12,32],"adjacent_by_trackway":[10,12,32],"adjacent_by_waterway":[]}, -{"name":"Pernau","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[11,7],[17,7],[8,37]],"box":{"x":517,"y":4580,"w":118,"h":30},"adjacent":[8,11,17],"adjacent_by_trackway":[8],"adjacent_by_waterway":[11,17]}, -{"name":"Rositten","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[21,42],[18,43]],"box":{"x":2046,"y":6307,"w":146,"h":30},"adjacent":[18,21],"adjacent_by_trackway":[18,21],"adjacent_by_waterway":[]}, -{"name":"Jerwen","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[11,7],[15,7],[1,30],[4,34]],"box":{"x":1064,"y":3946,"w":200,"h":100},"adjacent":[1,4,11,15],"adjacent_by_trackway":[1,4],"adjacent_by_waterway":[11,15]}, -{"name":"Lettgallia","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[16,43],[10,44],[21,44],[39,50],[32,51]],"box":{"x":2048,"y":5777,"w":200,"h":100},"adjacent":[10,16,21,32,39],"adjacent_by_trackway":[10,16,21,32,39],"adjacent_by_waterway":[]}, -{"name":"Metsepole","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[20,39],[13,40]],"box":{"x":509,"y":5226,"w":200,"h":100},"adjacent":[13,20],"adjacent_by_trackway":[13,20],"adjacent_by_waterway":[]}, -{"name":"Sackala","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[11,38],[19,39]],"box":{"x":617,"y":4769,"w":200,"h":100},"adjacent":[11,19],"adjacent_by_trackway":[11,19],"adjacent_by_waterway":[]}, -{"name":"Tolowa","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[13,41],[16,42],[10,44],[18,44]],"box":{"x":1541,"y":5933,"w":200,"h":100},"adjacent":[10,13,16,18],"adjacent_by_trackway":[10,13,16,18],"adjacent_by_waterway":[]}, -{"name":"Ugaunia","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[7,0],[12,0],[49,48],[32,49]],"box":{"x":1957,"y":4940,"w":200,"h":100},"adjacent":[7,12,32,49],"adjacent_by_trackway":[7,12,32,49],"adjacent_by_waterway":[]}, -{"name":"Waiga","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[6,29],[7,36]],"box":{"x":1535,"y":4113,"w":200,"h":100},"adjacent":[6,7],"adjacent_by_trackway":[6,7],"adjacent_by_waterway":[]}, -{"name":"Novgorod","type":"novgorod","stronghold":3,"walls":3,"vp":3,"region":"Novgorodan Rus","ways":[[27,6],[47,6],[31,23],[41,62],[40,63]],"box":{"x":4318,"y":4315,"w":333,"h":112},"adjacent":[27,31,40,41,47],"adjacent_by_trackway":[40,41],"adjacent_by_waterway":[27,31,47]}, -{"name":"Ladoga","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[31,22],[30,24],[44,59]],"box":{"x":4619,"y":2817,"w":238,"h":90},"adjacent":[30,31,44],"adjacent_by_trackway":[44],"adjacent_by_waterway":[30,31]}, -{"name":"Pskov","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[49,4],[39,10],[32,52],[52,68],[37,69]],"box":{"x":2680,"y":5263,"w":205,"h":91},"adjacent":[32,37,39,49,52],"adjacent_by_trackway":[32,37,52],"adjacent_by_waterway":[39,49]}, -{"name":"Rusa","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[24,6],[47,6],[28,13]],"box":{"x":4329,"y":5166,"w":205,"h":92},"adjacent":[24,28,47],"adjacent_by_trackway":[],"adjacent_by_waterway":[24,28,47]}, -{"name":"Lovat","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[27,13],[36,14]],"box":{"x":4243,"y":5581,"w":187,"h":63},"adjacent":[27,36],"adjacent_by_trackway":[],"adjacent_by_waterway":[27,36]}, -{"name":"Luga","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[33,18]],"box":{"x":2667,"y":3295,"w":148,"h":62},"adjacent":[33],"adjacent_by_trackway":[],"adjacent_by_waterway":[33]}, -{"name":"Neva","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[25,24],[51,55],[45,57],[44,58]],"box":{"x":3924,"y":2934,"w":148,"h":62},"adjacent":[25,44,45,51],"adjacent_by_trackway":[44,45,51],"adjacent_by_waterway":[25]}, -{"name":"Volkhov","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[25,22],[24,23]],"box":{"x":4591,"y":3783,"w":231,"h":63},"adjacent":[24,25],"adjacent_by_trackway":[],"adjacent_by_waterway":[24,25]}, -{"name":"Izborsk","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[14,47],[22,49],[18,51],[26,52]],"box":{"x":2240,"y":5431,"w":241,"h":62},"adjacent":[14,18,22,26],"adjacent_by_trackway":[14,18,22,26],"adjacent_by_waterway":[]}, -{"name":"Kaibolovo","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[29,18],[42,19],[2,28],[34,53]],"box":{"x":2904,"y":3522,"w":285,"h":62},"adjacent":[2,29,34,42],"adjacent_by_trackway":[2,34],"adjacent_by_waterway":[29,42]}, -{"name":"Koporye","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[33,53],[51,54]],"box":{"x":3133,"y":3160,"w":241,"h":62},"adjacent":[33,51],"adjacent_by_trackway":[33,51],"adjacent_by_waterway":[]}, -{"name":"Porkhov","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[47,16],[37,70],[48,71]],"box":{"x":3515,"y":5467,"w":241,"h":63},"adjacent":[37,47,48],"adjacent_by_trackway":[37,48],"adjacent_by_waterway":[47]}, -{"name":"Velikiye Luki","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[28,14],[50,72]],"box":{"x":3706,"y":6347,"w":351,"h":61},"adjacent":[28,50],"adjacent_by_trackway":[50],"adjacent_by_waterway":[28]}, -{"name":"Dubrovno","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[47,15],[26,69],[35,70]],"box":{"x":3153,"y":5214,"w":161,"h":31},"adjacent":[26,35,47],"adjacent_by_trackway":[26,35],"adjacent_by_waterway":[47]}, -{"name":"Gdov","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[49,1,2],[2,2],[7,2],[46,65]],"box":{"x":2427,"y":4149,"w":88,"h":30},"adjacent":[2,7,46,49],"adjacent_by_trackway":[46],"adjacent_by_waterway":[2,7,49]}, -{"name":"Ostrov","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[26,10],[50,11],[18,50]],"box":{"x":2746,"y":5717,"w":115,"h":30},"adjacent":[18,26,50],"adjacent_by_trackway":[18],"adjacent_by_waterway":[26,50]}, -{"name":"Sablia","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[42,20],[24,63],[47,64]],"box":{"x":3788,"y":4541,"w":104,"h":31},"adjacent":[24,42,47],"adjacent_by_trackway":[24,47],"adjacent_by_waterway":[42]}, -{"name":"Tesovo","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[42,21],[43,61],[24,62]],"box":{"x":3936,"y":4102,"w":121,"h":32},"adjacent":[24,42,43],"adjacent_by_trackway":[24,43],"adjacent_by_waterway":[42]}, -{"name":"Zheltsy","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[33,19],[40,20],[41,21],[46,66]],"box":{"x":3501,"y":4176,"w":128,"h":30},"adjacent":[33,40,41,46],"adjacent_by_trackway":[46],"adjacent_by_waterway":[33,40,41]}, -{"name":"Ingria","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[51,56],[44,60],[41,61]],"box":{"x":3820,"y":3639,"w":200,"h":100},"adjacent":[41,44,51],"adjacent_by_trackway":[41,44,51],"adjacent_by_waterway":[]}, -{"name":"Izhora","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[30,58],[25,59],[43,60]],"box":{"x":4074,"y":3323,"w":200,"h":100},"adjacent":[25,30,43],"adjacent_by_trackway":[25,30,43],"adjacent_by_waterway":[]}, -{"name":"Karelia","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[30,57]],"box":{"x":3833,"y":2408,"w":200,"h":100},"adjacent":[30],"adjacent_by_trackway":[30],"adjacent_by_waterway":[]}, -{"name":"Plyussa River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[2,17],[38,65],[42,66],[52,67]],"box":{"x":2829,"y":4234,"w":200,"h":100},"adjacent":[2,38,42,52],"adjacent_by_trackway":[38,42,52],"adjacent_by_waterway":[2]}, -{"name":"Shelon River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[24,6],[27,6],[37,15],[35,16],[40,64]],"box":{"x":3654,"y":4864,"w":200,"h":100},"adjacent":[24,27,35,37,40],"adjacent_by_trackway":[40],"adjacent_by_waterway":[24,27,35,37]}, -{"name":"Sorot River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[50,12],[35,71]],"box":{"x":3299,"y":5781,"w":200,"h":100},"adjacent":[35,50],"adjacent_by_trackway":[35],"adjacent_by_waterway":[50]}, -{"name":"Uzmen","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[38,1,2],[2,2],[7,2],[26,4],[52,5],[22,48]],"box":{"x":2112,"y":4692,"w":200,"h":100},"adjacent":[2,7,22,26,38,52],"adjacent_by_trackway":[22],"adjacent_by_waterway":[2,7,26,38,52]}, -{"name":"Velikaya River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[39,11],[48,12],[36,72]],"box":{"x":3029,"y":6090,"w":200,"h":100},"adjacent":[36,39,48],"adjacent_by_trackway":[36],"adjacent_by_waterway":[39,48]}, -{"name":"Vod","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[34,54],[30,55],[43,56]],"box":{"x":3488,"y":3345,"w":200,"h":100},"adjacent":[30,34,43],"adjacent_by_trackway":[30,34,43],"adjacent_by_waterway":[]}, -{"name":"Zhelcha River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[49,5],[46,67],[26,68]],"box":{"x":2782,"y":4586,"w":200,"h":100},"adjacent":[26,46,49],"adjacent_by_trackway":[26,46],"adjacent_by_waterway":[49]}, +{"name":"Reval","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Danish Estonia","ways":[[5,25],[3,32]],"box":{"x":601,"y":3564,"w":206,"h":91},"adjacent":[3,5],"adjacent_by_trackway":[3,5],"adjacent_by_waterway":[],"trackways":[[3,32],[5,25]],"waterways":[]}, +{"name":"Wesenberg","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Danish Estonia","ways":[[5,26],[17,30],[6,31]],"box":{"x":1448,"y":3625,"w":304,"h":60},"adjacent":[5,6,17],"adjacent_by_trackway":[5,6,17],"adjacent_by_waterway":[],"trackways":[[5,26],[6,31],[17,30]],"waterways":[]}, +{"name":"Narwia","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[7,2],[38,2],[49,2],[46,17],[6,27],[33,28]],"box":{"x":2371,"y":3549,"w":123,"h":31},"adjacent":[6,7,33,38,46,49],"adjacent_by_trackway":[6,33],"adjacent_by_waterway":[7,38,46,49],"trackways":[[6,27],[33,28]],"waterways":[[7,2],[38,2],[49,2],[46,17]]}, +{"name":"Warbola","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[0,32],[4,33],[8,35]],"box":{"x":292,"y":3797,"w":142,"h":31},"adjacent":[0,4,8],"adjacent_by_trackway":[0,4,8],"adjacent_by_waterway":[],"trackways":[[0,32],[4,33],[8,35]],"waterways":[]}, +{"name":"Harrien","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[3,33],[17,34]],"box":{"x":567,"y":3983,"w":200,"h":100},"adjacent":[3,17],"adjacent_by_trackway":[3,17],"adjacent_by_waterway":[],"trackways":[[3,33],[17,34]],"waterways":[]}, +{"name":"Revala","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[0,25],[1,26]],"box":{"x":1030,"y":3410,"w":200,"h":100},"adjacent":[0,1],"adjacent_by_trackway":[0,1],"adjacent_by_waterway":[],"trackways":[[0,25],[1,26]],"waterways":[]}, +{"name":"Wierland","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Danish Estonia","ways":[[2,27],[23,29],[1,31]],"box":{"x":1999,"y":3680,"w":200,"h":100},"adjacent":[1,2,23],"adjacent_by_trackway":[1,2,23],"adjacent_by_waterway":[],"trackways":[[1,31],[2,27],[23,29]],"waterways":[]}, +{"name":"Dorpat","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[12,0,3],[22,0],[2,2],[38,2],[49,2],[11,3],[23,36]],"box":{"x":1625,"y":4589,"w":253,"h":91},"adjacent":[2,11,12,22,23,38,49],"adjacent_by_trackway":[12,22,23],"adjacent_by_waterway":[2,11,12,38,49],"trackways":[[12,0],[22,0],[23,36]],"waterways":[[12,3],[2,2],[38,2],[49,2],[11,3]]}, +{"name":"Leal","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[3,35],[15,37]],"box":{"x":108,"y":4266,"w":205,"h":91},"adjacent":[3,15],"adjacent_by_trackway":[3,15],"adjacent_by_waterway":[],"trackways":[[3,35],[15,37]],"waterways":[]}, +{"name":"Riga","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[13,8]],"box":{"x":273,"y":6231,"w":205,"h":91},"adjacent":[13],"adjacent_by_trackway":[],"adjacent_by_waterway":[13],"trackways":[],"waterways":[[13,8]]}, +{"name":"Adsel","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[13,9],[18,44],[21,44],[14,45]],"box":{"x":1504,"y":5612,"w":185,"h":60},"adjacent":[13,14,18,21],"adjacent_by_trackway":[14,18,21],"adjacent_by_waterway":[13],"trackways":[[14,45],[18,44],[21,44]],"waterways":[[13,9]]}, +{"name":"Fellin","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[7,3],[12,3],[15,7],[17,7],[20,38]],"box":{"x":1013,"y":4583,"w":184,"h":61},"adjacent":[7,12,15,17,20],"adjacent_by_trackway":[20],"adjacent_by_waterway":[7,12,15,17],"trackways":[[20,38]],"waterways":[[7,3],[12,3],[15,7],[17,7]]}, +{"name":"Odenpäh","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[7,0,3],[22,0],[11,3],[14,46]],"box":{"x":1378,"y":5103,"w":250,"h":61},"adjacent":[7,11,14,22],"adjacent_by_trackway":[7,14,22],"adjacent_by_waterway":[7,11],"trackways":[[7,0],[14,46],[22,0]],"waterways":[[7,3],[11,3]]}, +{"name":"Wenden","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[9,8],[10,9],[19,40],[21,41]],"box":{"x":909,"y":5759,"w":232,"h":60},"adjacent":[9,10,19,21],"adjacent_by_trackway":[19,21],"adjacent_by_waterway":[9,10],"trackways":[[19,40],[21,41]],"waterways":[[9,8],[10,9]]}, +{"name":"Kirrumpäh","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[10,45],[12,46],[32,47]],"box":{"x":1877,"y":5389,"w":175,"h":30},"adjacent":[10,12,32],"adjacent_by_trackway":[10,12,32],"adjacent_by_waterway":[],"trackways":[[10,45],[12,46],[32,47]],"waterways":[]}, +{"name":"Pernau","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[11,7],[17,7],[8,37]],"box":{"x":517,"y":4580,"w":118,"h":30},"adjacent":[8,11,17],"adjacent_by_trackway":[8],"adjacent_by_waterway":[11,17],"trackways":[[8,37]],"waterways":[[11,7],[17,7]]}, +{"name":"Rositten","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[21,42],[18,43]],"box":{"x":2046,"y":6307,"w":146,"h":30},"adjacent":[18,21],"adjacent_by_trackway":[18,21],"adjacent_by_waterway":[],"trackways":[[18,43],[21,42]],"waterways":[]}, +{"name":"Jerwen","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[11,7],[15,7],[1,30],[4,34]],"box":{"x":1064,"y":3946,"w":200,"h":100},"adjacent":[1,4,11,15],"adjacent_by_trackway":[1,4],"adjacent_by_waterway":[11,15],"trackways":[[1,30],[4,34]],"waterways":[[11,7],[15,7]]}, +{"name":"Lettgallia","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[16,43],[10,44],[21,44],[39,50],[32,51]],"box":{"x":2048,"y":5777,"w":200,"h":100},"adjacent":[10,16,21,32,39],"adjacent_by_trackway":[10,16,21,32,39],"adjacent_by_waterway":[],"trackways":[[10,44],[16,43],[21,44],[32,51],[39,50]],"waterways":[]}, +{"name":"Metsepole","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[20,39],[13,40]],"box":{"x":509,"y":5226,"w":200,"h":100},"adjacent":[13,20],"adjacent_by_trackway":[13,20],"adjacent_by_waterway":[],"trackways":[[13,40],[20,39]],"waterways":[]}, +{"name":"Sackala","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[11,38],[19,39]],"box":{"x":617,"y":4769,"w":200,"h":100},"adjacent":[11,19],"adjacent_by_trackway":[11,19],"adjacent_by_waterway":[],"trackways":[[11,38],[19,39]],"waterways":[]}, +{"name":"Tolowa","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[13,41],[16,42],[10,44],[18,44]],"box":{"x":1541,"y":5933,"w":200,"h":100},"adjacent":[10,13,16,18],"adjacent_by_trackway":[10,13,16,18],"adjacent_by_waterway":[],"trackways":[[10,44],[13,41],[16,42],[18,44]],"waterways":[]}, +{"name":"Ugaunia","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[7,0],[12,0],[49,48],[32,49]],"box":{"x":1957,"y":4940,"w":200,"h":100},"adjacent":[7,12,32,49],"adjacent_by_trackway":[7,12,32,49],"adjacent_by_waterway":[],"trackways":[[7,0],[12,0],[32,49],[49,48]],"waterways":[]}, +{"name":"Waiga","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Crusader Livonia","ways":[[6,29],[7,36]],"box":{"x":1535,"y":4113,"w":200,"h":100},"adjacent":[6,7],"adjacent_by_trackway":[6,7],"adjacent_by_waterway":[],"trackways":[[6,29],[7,36]],"waterways":[]}, +{"name":"Novgorod","type":"novgorod","stronghold":3,"walls":3,"vp":3,"region":"Novgorodan Rus","ways":[[27,6],[47,6],[31,23],[41,62],[40,63]],"box":{"x":4318,"y":4315,"w":333,"h":112},"adjacent":[27,31,40,41,47],"adjacent_by_trackway":[40,41],"adjacent_by_waterway":[27,31,47],"trackways":[[40,63],[41,62]],"waterways":[[27,6],[47,6],[31,23]]}, +{"name":"Ladoga","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[31,22],[30,24],[44,59]],"box":{"x":4619,"y":2817,"w":238,"h":90},"adjacent":[30,31,44],"adjacent_by_trackway":[44],"adjacent_by_waterway":[30,31],"trackways":[[44,59]],"waterways":[[31,22],[30,24]]}, +{"name":"Pskov","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[49,4],[39,10],[32,52],[52,68],[37,69]],"box":{"x":2680,"y":5263,"w":205,"h":91},"adjacent":[32,37,39,49,52],"adjacent_by_trackway":[32,37,52],"adjacent_by_waterway":[39,49],"trackways":[[32,52],[37,69],[52,68]],"waterways":[[49,4],[39,10]]}, +{"name":"Rusa","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[24,6],[47,6],[28,13]],"box":{"x":4329,"y":5166,"w":205,"h":92},"adjacent":[24,28,47],"adjacent_by_trackway":[],"adjacent_by_waterway":[24,28,47],"trackways":[],"waterways":[[24,6],[47,6],[28,13]]}, +{"name":"Lovat","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[27,13],[36,14]],"box":{"x":4243,"y":5581,"w":187,"h":63},"adjacent":[27,36],"adjacent_by_trackway":[],"adjacent_by_waterway":[27,36],"trackways":[],"waterways":[[27,13],[36,14]]}, +{"name":"Luga","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[33,18]],"box":{"x":2667,"y":3295,"w":148,"h":62},"adjacent":[33],"adjacent_by_trackway":[],"adjacent_by_waterway":[33],"trackways":[],"waterways":[[33,18]]}, +{"name":"Neva","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[25,24],[51,55],[45,57],[44,58]],"box":{"x":3924,"y":2934,"w":148,"h":62},"adjacent":[25,44,45,51],"adjacent_by_trackway":[44,45,51],"adjacent_by_waterway":[25],"trackways":[[44,58],[45,57],[51,55]],"waterways":[[25,24]]}, +{"name":"Volkhov","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[25,22],[24,23]],"box":{"x":4591,"y":3783,"w":231,"h":63},"adjacent":[24,25],"adjacent_by_trackway":[],"adjacent_by_waterway":[24,25],"trackways":[],"waterways":[[25,22],[24,23]]}, +{"name":"Izborsk","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[14,47],[22,49],[18,51],[26,52]],"box":{"x":2240,"y":5431,"w":241,"h":62},"adjacent":[14,18,22,26],"adjacent_by_trackway":[14,18,22,26],"adjacent_by_waterway":[],"trackways":[[14,47],[18,51],[22,49],[26,52]],"waterways":[]}, +{"name":"Kaibolovo","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[29,18],[42,19],[2,28],[34,53]],"box":{"x":2904,"y":3522,"w":285,"h":62},"adjacent":[2,29,34,42],"adjacent_by_trackway":[2,34],"adjacent_by_waterway":[29,42],"trackways":[[2,28],[34,53]],"waterways":[[29,18],[42,19]]}, +{"name":"Koporye","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[33,53],[51,54]],"box":{"x":3133,"y":3160,"w":241,"h":62},"adjacent":[33,51],"adjacent_by_trackway":[33,51],"adjacent_by_waterway":[],"trackways":[[33,53],[51,54]],"waterways":[]}, +{"name":"Porkhov","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[47,16],[37,70],[48,71]],"box":{"x":3515,"y":5467,"w":241,"h":63},"adjacent":[37,47,48],"adjacent_by_trackway":[37,48],"adjacent_by_waterway":[47],"trackways":[[37,70],[48,71]],"waterways":[[47,16]]}, +{"name":"Velikiye Luki","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[28,14],[50,72]],"box":{"x":3706,"y":6347,"w":351,"h":61},"adjacent":[28,50],"adjacent_by_trackway":[50],"adjacent_by_waterway":[28],"trackways":[[50,72]],"waterways":[[28,14]]}, +{"name":"Dubrovno","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[47,15],[26,69],[35,70]],"box":{"x":3153,"y":5214,"w":161,"h":31},"adjacent":[26,35,47],"adjacent_by_trackway":[26,35],"adjacent_by_waterway":[47],"trackways":[[26,69],[35,70]],"waterways":[[47,15]]}, +{"name":"Gdov","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[49,1,2],[2,2],[7,2],[46,65]],"box":{"x":2427,"y":4149,"w":88,"h":30},"adjacent":[2,7,46,49],"adjacent_by_trackway":[46],"adjacent_by_waterway":[2,7,49],"trackways":[[46,65]],"waterways":[[49,1],[2,2],[7,2]]}, +{"name":"Ostrov","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[26,10],[50,11],[18,50]],"box":{"x":2746,"y":5717,"w":115,"h":30},"adjacent":[18,26,50],"adjacent_by_trackway":[18],"adjacent_by_waterway":[26,50],"trackways":[[18,50]],"waterways":[[26,10],[50,11]]}, +{"name":"Sablia","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[42,20],[24,63],[47,64]],"box":{"x":3788,"y":4541,"w":104,"h":31},"adjacent":[24,42,47],"adjacent_by_trackway":[24,47],"adjacent_by_waterway":[42],"trackways":[[24,63],[47,64]],"waterways":[[42,20]]}, +{"name":"Tesovo","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[42,21],[43,61],[24,62]],"box":{"x":3936,"y":4102,"w":121,"h":32},"adjacent":[24,42,43],"adjacent_by_trackway":[24,43],"adjacent_by_waterway":[42],"trackways":[[24,62],[43,61]],"waterways":[[42,21]]}, +{"name":"Zheltsy","type":"town","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[33,19],[40,20],[41,21],[46,66]],"box":{"x":3501,"y":4176,"w":128,"h":30},"adjacent":[33,40,41,46],"adjacent_by_trackway":[46],"adjacent_by_waterway":[33,40,41],"trackways":[[46,66]],"waterways":[[33,19],[40,20],[41,21]]}, +{"name":"Ingria","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[51,56],[44,60],[41,61]],"box":{"x":3820,"y":3639,"w":200,"h":100},"adjacent":[41,44,51],"adjacent_by_trackway":[41,44,51],"adjacent_by_waterway":[],"trackways":[[41,61],[44,60],[51,56]],"waterways":[]}, +{"name":"Izhora","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[30,58],[25,59],[43,60]],"box":{"x":4074,"y":3323,"w":200,"h":100},"adjacent":[25,30,43],"adjacent_by_trackway":[25,30,43],"adjacent_by_waterway":[],"trackways":[[25,59],[30,58],[43,60]],"waterways":[]}, +{"name":"Karelia","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[30,57]],"box":{"x":3833,"y":2408,"w":200,"h":100},"adjacent":[30],"adjacent_by_trackway":[30],"adjacent_by_waterway":[],"trackways":[[30,57]],"waterways":[]}, +{"name":"Plyussa River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[2,17],[38,65],[42,66],[52,67]],"box":{"x":2829,"y":4234,"w":200,"h":100},"adjacent":[2,38,42,52],"adjacent_by_trackway":[38,42,52],"adjacent_by_waterway":[2],"trackways":[[38,65],[42,66],[52,67]],"waterways":[[2,17]]}, +{"name":"Shelon River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[24,6],[27,6],[37,15],[35,16],[40,64]],"box":{"x":3654,"y":4864,"w":200,"h":100},"adjacent":[24,27,35,37,40],"adjacent_by_trackway":[40],"adjacent_by_waterway":[24,27,35,37],"trackways":[[40,64]],"waterways":[[24,6],[27,6],[37,15],[35,16]]}, +{"name":"Sorot River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[50,12],[35,71]],"box":{"x":3299,"y":5781,"w":200,"h":100},"adjacent":[35,50],"adjacent_by_trackway":[35],"adjacent_by_waterway":[50],"trackways":[[35,71]],"waterways":[[50,12]]}, +{"name":"Uzmen","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[38,1,2],[2,2],[7,2],[26,4],[52,5],[22,48]],"box":{"x":2112,"y":4692,"w":200,"h":100},"adjacent":[2,7,22,26,38,52],"adjacent_by_trackway":[22],"adjacent_by_waterway":[2,7,26,38,52],"trackways":[[22,48]],"waterways":[[38,1],[2,2],[7,2],[26,4],[52,5]]}, +{"name":"Velikaya River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[39,11],[48,12],[36,72]],"box":{"x":3029,"y":6090,"w":200,"h":100},"adjacent":[36,39,48],"adjacent_by_trackway":[36],"adjacent_by_waterway":[39,48],"trackways":[[36,72]],"waterways":[[39,11],[48,12]]}, +{"name":"Vod","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[34,54],[30,55],[43,56]],"box":{"x":3488,"y":3345,"w":200,"h":100},"adjacent":[30,34,43],"adjacent_by_trackway":[30,34,43],"adjacent_by_waterway":[],"trackways":[[30,55],[34,54],[43,56]],"waterways":[]}, +{"name":"Zhelcha River","type":"region","stronghold":0,"walls":0,"vp":0,"region":"Novgorodan Rus","ways":[[49,5],[46,67],[26,68]],"box":{"x":2782,"y":4586,"w":200,"h":100},"adjacent":[26,46,49],"adjacent_by_trackway":[26,46],"adjacent_by_waterway":[49],"trackways":[[26,68],[46,67]],"waterways":[[49,5]]}, ], ways:[ {"type":"trackway","locales":[7,12,22],"name":"Crossroads"}, @@ -720,9 +720,6 @@ body.shift .mustered_vassals { .locale.bishopric { border-radius: 50% 50% 0 0 } .locale.novgorod { border-radius: 50% 50% 0 0 } -.locale.action { border-color: white; box-shadow: 0 0 4px white; } -.locale.action.laden { border-color: red; box-shadow: 0 0 4px red; } - .locale.action.region { background-color: #5A02 } .locale.action.town { background-color: #fc02 } .locale.action.traderoute { background-color: #0af2 } @@ -732,6 +729,13 @@ body.shift .mustered_vassals { .locale.action.castle { background-color: #0002 } .locale.action.bishopric { background-color: #0002 } +.locale.action { border-color: white; box-shadow: 0 0 4px white; } +.locale.action.laden { border-color: white; box-shadow: 0 0 0 3px indianred; background-color: #cd5c5c66 } + +.locale.supply_path { border-color: indianred; background-color: #cd5c5c66; } +.locale.supply_source { border-color: seagreen; background-color: #2e8b5766; } +.locale.supply_source.action { border-color: white; background-color: #2e8b5766; box-shadow: 0 0 0 3px seagreen; } + .locale.tip, .locale_name.tip { background-color: #ff08; box-shadow: 0 0 8px #ff08; @@ -1231,9 +1231,10 @@ function update_locale(loc) { ui.locale[loc].classList.toggle("action", is_locale_action(loc) || is_laden_march_action(loc)) ui.locale[loc].classList.toggle("laden", is_laden_march_action(loc)) + ui.locale[loc].classList.toggle("supply_path", !!(view.supply && view.supply[0] === loc)) + ui.locale[loc].classList.toggle("supply_source", !!(view.supply && view.supply[1] === loc)) if (ui.locale_name[loc]) { ui.locale_name[loc].classList.toggle("action", is_locale_action(loc) || is_laden_march_action(loc)) - ui.locale_name[loc].classList.toggle("laden", is_laden_march_action(loc)) } ui.locale_markers[loc].replaceChildren() @@ -7,13 +7,10 @@ // FIXME: lift_sieges / besieged needs checking! (automatic after disband_lord, manual after move/sail, extra careful manual after battle) // FIXME: remove_legate_if_endangered needs checking! (automatic after disband_lord, manual after move/sail, manual after battle) -// GUI: Remove battle mat. -// GUI: tucked map capabilities pop up on shift key +// GUI: siegeworks markers for walls and siegeworks (colored by side) // GUI: show moved/fought markers on mats during Feed phase // GUI: show sally/relief sally as unbesieged on map -// GUI: show siegeworks + walls on battle mat for protection indication // GUI: show feed x2 on lord mats with > 6 units -// GUI: battle event on lord mat (at top) - in client for Bridge and Field Organs // WONTFIX: choose crossbow/normal hit application order @@ -26,6 +23,8 @@ const data = require("./data.js") +const AUTOWALK = true + const BOTH = "Both" const TEUTONS = "Teutons" const RUSSIANS = "Russians" @@ -2839,6 +2838,7 @@ function end_heinrich_sees_the_curia() { if (game.state === "actions" && game.command === LORD_HEINRICH) { spend_all_actions() resume_actions() + update_supply_possible() } // No more muster of Heinrich @@ -4259,6 +4259,7 @@ function goto_actions() { } resume_actions() + update_supply_possible() } function resume_actions() { @@ -4633,6 +4634,7 @@ function march_with_group_3() { game.march = 0 spend_all_actions() resume_actions() + update_supply_possible() return } @@ -4650,6 +4652,7 @@ function march_with_group_3() { game.march = 0 resume_actions() + update_supply_possible() } // === ACTION: MARCH - AVOID BATTLE === @@ -5182,6 +5185,7 @@ function end_siege() { spend_all_actions() resume_actions() + update_supply_possible() } // === ACTION: STORM === @@ -5262,14 +5266,34 @@ function init_lodya_supply() { if (!is_winter()) { let lord = find_lodya_lord_in_shared() if (lord !== NOBODY) { - // No choice if enough ships and 2x boats >= boats + extra ships - let ships = get_lord_assets(lord, SHIP) let boats = get_lord_assets(lord, BOAT) - if (ships < 2 || boats < ships - 2) - return true - log("Lodya: Boats as 2 Boats each.") + let ships = get_lord_assets(lord, SHIP) + + // Automatic choice if Novgorod is unavailable for seaport supply. + if (is_supply_forbidden(LOC_NOVGOROD)) { + if (boats < ships) { + game.flags.lodya = -ships + log_lodya() + return false + } else { + game.flags.lodya = 0 + log_lodya() + return false + } + } + + // Automatic choice if enough ships and 2x boats >= boats + extra ships. + if (ships >= 2 && boats >= ships - 2) { + game.flags.lodya = 0 + log_lodya() + return false + } + + // Manual choice. + return true } } + // No choice. return false } @@ -5315,6 +5339,7 @@ function end_supply_lodya() { push_undo() log_lodya() init_supply() + resume_supply() game.state = "supply_source" } @@ -5327,7 +5352,37 @@ function log_lodya() { log(`Lodya: ${game.flags.lodya} Ships as Boats.`) } -// === ACTION: SUPPLY === +// === ACTION: SUPPLY (SEARCHING) === + +let _supply_stat = 0 +let _supply_stop = new Array(last_locale+1) +let _supply_reached = new Array(last_locale+1) + +let _supply_seen = new Array(last_locale+1).fill(0) +let _supply_cost = new Array(last_locale+1) +let _supply_boats = new Array(last_locale+1) +let _supply_carts = new Array(last_locale+1) + +function is_supply_forbidden(here) { + if (has_unbesieged_enemy_lord(here)) + return true + if (is_unbesieged_enemy_stronghold(here)) + return true + if (is_friendly_territory(here) && has_conquered_marker(here)) + if (!has_siege_marker(here)) + return true + return false +} + +function init_supply_forbidden() { + _supply_stat = 0 + for (let here = 0; here <= last_locale; ++here) { + if (is_supply_forbidden(here)) + _supply_stop[here] = 1 + else + _supply_stop[here] = 0 + } +} function init_supply() { let season = current_season() @@ -5336,7 +5391,7 @@ function init_supply() { let carts = 0 let sleds = 0 let ships = 0 - let seats = 2 + let available = 2 if (season === SUMMER) { carts = get_shared_assets(here, CART) @@ -5353,84 +5408,184 @@ function init_supply() { ships = 2 if (is_famine_in_play()) - seats = game.flags.famine ? 0 : 1 - - let sources = list_supply_sources(ships) - let reachable = filter_reachable_supply_sources(sources, boats, carts, sleds) - let supply_seats = filter_usable_supply_seats(reachable) - let supply_seaports = filter_usable_supply_seaports(reachable, ships) + available = game.flags.famine ? 0 : 1 - game.supply = { supply_seats, supply_seaports, seats, boats, carts, sleds, ships } -} - -function list_supply_sources(ships) { - let sources = [] - - for_each_seat(game.command, seat => { set_add(sources, seat) }, false) + let seats = [] + if (available > 0) { + for_each_seat(game.command, seat => { + if (!is_supply_forbidden(seat)) + seats.push(seat) + }, true) + available = Math.min(seats.length, available) + } + let seaports = [] if (ships > 0) { if (game.active === TEUTONS) for (let port of data.seaports) - set_add(sources, port) + if (!is_supply_forbidden(port)) + seaports.push(port) if (game.active === RUSSIANS) - set_add(sources, LOC_NOVGOROD) + if (!is_supply_forbidden(LOC_NOVGOROD)) + seaports.push(LOC_NOVGOROD) } + if (seaports.length === 0) + ships = 0 - return sources + game.supply = { seats, seaports, available, boats, carts, sleds, ships } } -let _supply_stat = 0 -let _supply_stop = new Array(last_locale+1) -let _supply_reached = new Array(last_locale+1) +function search_supply_winter(start, sleds, exit) { + if (_supply_stop[start]) + return 0 + _supply_reached[start] = 1 + _supply_cost[start] = 0 + if (exit && set_has(exit, start)) + return 1 + if (sleds === 0) + return 0 + let queue = [ start ] + while (queue.length > 0) { + let item = queue.shift() + let here = item & 63 + let used = item >> 6 + if (used + 1 <= sleds) { + for (let next of data.locales[here].adjacent) { + if (!_supply_reached[next] && !_supply_stop[next]) { + if (exit && set_has(exit, next)) + return 1 + _supply_reached[next] = 1 + _supply_cost[next] = used + 1 + if (used + 1 < sleds) + queue.push(next | ((used + 1) << 6)) + } + } + } + _supply_stat++ + } + return 0 +} -let _supply_seen = new Array(last_locale+1) -let _supply_boats = new Array(last_locale+1) -let _supply_carts = new Array(last_locale+1) +function search_supply_rasputitsa(start, boats, exit) { + if (_supply_stop[start]) + return 0 + _supply_reached[start] = 1 + _supply_cost[start] = 0 + if (exit && set_has(exit, start)) + return 1 + if (boats === 0) + return 0 + let queue = [ start ] + while (queue.length > 0) { + let item = queue.shift() + let here = item & 63 + let used = item >> 6 + if (used + 1 <= boats) { + for (let next of data.locales[here].adjacent_by_waterway) { + if (!_supply_reached[next] && !_supply_stop[next]) { + if (exit && set_has(exit, next)) + return 1 + _supply_reached[next] = 1 + _supply_cost[next] = used + 1 + if (used + 1 < boats) + queue.push(next | ((used + 1) << 6)) + } + } + } + _supply_stat++ + } + return 0 +} -function filter_reachable_supply_sources(sources, boats, carts, sleds) { - _supply_stat = 0 - _supply_stop.fill(0) - _supply_reached.fill(0) +function search_supply_summer(here, boats, carts, exit) { + _supply_stat++ - for (let here = 0; here <= last_locale; ++here) { - if (has_unbesieged_enemy_lord(here)) - _supply_stop[here] = 1 - else if (is_unbesieged_enemy_stronghold(here)) - _supply_stop[here] = 1 - else if (is_friendly_territory(here) && has_conquered_marker(here)) - if (!has_siege_marker(here)) - _supply_stop[here] = 1 + // Been here before with same or more transports remaining + if (_supply_boats[here] >= boats && _supply_carts[here] >= carts) + return 0 + + // First time here with this many transports remaining + if (_supply_boats[here] <= boats && _supply_carts[here] <= carts) { + _supply_boats[here] = boats + _supply_carts[here] = carts } - // NOTE: Impossible situation - if (_supply_stop[get_lord_locale(game.command)]) - return [] + _supply_reached[here] = 1 + if (exit && set_has(exit, here)) + return 1 - switch (current_season()) { - case SUMMER: - _supply_seen.fill(0) - _supply_boats.fill(-1) - _supply_carts.fill(-1) - search_supply_reachable_summer(get_lord_locale(game.command), boats, carts) - break - case EARLY_WINTER: - case LATE_WINTER: - search_supply_reachable_winter(get_lord_locale(game.command), sleds) - break - case RASPUTITSA: - search_supply_reachable_rasputitsa(get_lord_locale(game.command), boats) - break + _supply_seen[here] = 1 + + if (boats > 0) { + for (let next of data.locales[here].adjacent_by_waterway) { + if (!_supply_seen[next] && !_supply_stop[next]) { + if (search_supply_summer(next, boats-1, carts, exit)) { + _supply_seen[here] = 0 + return 1 + } + } + } } - let result = [] - for (let here of sources) - if (_supply_reached[here]) - set_add(result, here) - console.log("SUPPLY SEARCH", _supply_stat, sources, result, _supply_reached.join("")) - return result + if (carts > 0) { + for (let next of data.locales[here].adjacent_by_trackway) { + if (!_supply_seen[next] && !_supply_stop[next]) { + if (search_supply_summer(next, boats, carts-1, exit)) { + _supply_seen[here] = 0 + return 1 + } + } + } + } + + _supply_seen[here] = 0 + return 0 } -function search_supply_reachable_summer(here, boats, carts) { +function init_summer_path() { + init_supply_forbidden() + + // First pass to create best-cost-so-far for each combo of boats to carts + let gate = { + boats: new Array(game.supply.boats+1).fill(0), + carts: new Array(game.supply.carts+1).fill(0), + } + _supply_stat = 0 + _supply_boats.fill(-1) + _supply_carts.fill(-1) + search_summer_path_pass1(game.supply.here, game.supply.end, game.supply.boats, game.supply.carts, gate) + console.log("SUMMER GATE", _supply_stat, JSON.stringify(gate)) + + // Second pass which lists acceptable paths + _supply_stat = 0 + _supply_boats.fill(-1) + _supply_carts.fill(-1) + game.supply.path = [] + search_summer_path_pass2([], game.supply.here, game.supply.end, game.supply.boats, game.supply.carts, gate) + console.log("SUMMER PATH", _supply_stat, JSON.stringify(game.supply.path).length) + + // Auto-pick path if only one choice. + if (AUTOWALK && game.supply.path.length === 2) + walk_supply_path_way(game.supply.path[0] >> 8, game.supply.path[0] & 255) +} + +function search_summer_path_pass1(here, end, boats, carts, gate) { + _supply_stat++ + + if (here === end) { + for (let c = 0; c <= carts; ++c) + if (boats > gate.boats[c]) + gate.boats[c] = boats + for (let b = 0; b <= boats; ++b) + if (carts > gate.carts[b]) + gate.carts[b] = carts + return + } + + // Worse than the best path found + if (boats < gate.boats[carts] || carts < gate.carts[boats]) + return + // Been here before with same or more transports remaining if (_supply_boats[here] >= boats && _supply_carts[here] >= carts) return @@ -5441,102 +5596,171 @@ function search_supply_reachable_summer(here, boats, carts) { _supply_carts[here] = carts } - _supply_reached[here] = 1 - - _supply_stat++ _supply_seen[here] = 1 + if (boats > 0) for (let next of data.locales[here].adjacent_by_waterway) - if (!_supply_seen[next] && !_supply_stop[next]) - search_supply_reachable_summer(next, boats - 1, carts) + if (!_supply_stop[next] && !_supply_seen[next]) + search_summer_path_pass1(next, end, boats-1, carts, gate) + if (carts > 0) for (let next of data.locales[here].adjacent_by_trackway) - if (!_supply_seen[next] && !_supply_stop[next]) - search_supply_reachable_summer(next, boats, carts - 1) + if (!_supply_stop[next] && !_supply_seen[next]) + search_summer_path_pass1(next, end, boats, carts-1, gate) + _supply_seen[here] = 0 } -function search_supply_reachable_winter(start, sleds) { - _supply_reached[start] = 1 - if (0 < sleds) { - let queue = [ start ] - while (queue.length > 0) { - let item = queue.shift() - let here = item & 63 - let used = item >> 6 - for (let next of data.locales[here].adjacent) { - if (!_supply_reached[next] && !_supply_stop[next]) { - _supply_reached[next] = 1 - if (used + 1 < sleds) - queue.push(next | ((used + 1) << 6)) - } +function search_summer_path_pass2(path, here, end, boats, carts, gate) { + _supply_stat++ + + // Worse than the best path found + if (boats < gate.boats[carts] || carts < gate.carts[boats]) + return + + if (here === end) { + console.log(" path", path.map(wl=>data.locales[wl>>8].name).join(","), boats, carts) + let out1 = game.supply.path + for (let i = 0; i < path.length; ++i) { + let wayloc = path[i] + let out2 = map_get(out1, wayloc, null) + if (out2 === null) { + if (i < path.length - 1) + map_set(out1, wayloc, out2 = []) + else + map_set(out1, wayloc, 0) } + out1 = out2 } + return + } + + // Been here before with same or more transports remaining + if (_supply_boats[here] >= boats && _supply_carts[here] >= carts) + return + + // First time here with this many transports remaining + if (_supply_boats[here] <= boats && _supply_carts[here] <= carts) { + _supply_boats[here] = boats + _supply_carts[here] = carts } + + _supply_seen[here] = 1 + + if (boats > 0) { + for (let [next, way] of data.locales[here].waterways) { + if (!_supply_stop[next] && !_supply_seen[next]) { + path.push((next << 8) | way) + search_summer_path_pass2(path, next, end, boats-1, carts, gate) + path.pop() + } + } + } + + if (carts > 0) { + for (let [next, way] of data.locales[here].trackways) { + if (!_supply_stop[next] && !_supply_seen[next]) { + path.push((next << 8) | way) + search_summer_path_pass2(path, next, end, boats, carts-1, gate) + path.pop() + } + } + } + + _supply_seen[here] = 0 } -function search_supply_reachable_rasputitsa(start, boats) { - _supply_reached[start] = 1 - if (0 < boats) { - let queue = [ start ] - while (queue.length > 0) { - let item = queue.shift() - let here = item & 63 - let used = item >> 6 - for (let next of data.locales[here].adjacent_by_waterway) { - if (!_supply_reached[next] && !_supply_stop[next]) { - _supply_reached[next] = 1 - if (used + 1 < boats) - queue.push(next | ((used + 1) << 6)) - } +// === ACTION: SUPPLY === + +function update_supply_possible() { + if (game.actions < 1) { + game.supply = 0 + return + } + + let lord = find_lodya_lord_in_shared() + if (lord !== NOBODY) { + if (!is_winter()) { + if (!is_supply_forbidden(LOC_NOVGOROD)) { + if (get_lord_assets(lord, BOAT) >= 2 && update_supply_possible_lodya(-2)) + return + if (get_lord_assets(lord, BOAT) >= 1 && update_supply_possible_lodya(-1)) + return } + if (get_lord_assets(lord, SHIP) >= 2 && update_supply_possible_lodya(2)) + return + if (get_lord_assets(lord, SHIP) >= 1 && update_supply_possible_lodya(1)) + return } + update_supply_possible_lodya(0) + } else { + update_supply_possible_pass() + console.log("POSSIBLE SEARCH", _supply_stat, game.supply) } } -function filter_usable_supply_seats(reachable) { +function update_supply_possible_lodya(x) { + game.flags.lodya = x + update_supply_possible_pass() + console.log("LODYA POSSIBLE SEARCH", _supply_stat, x, game.supply) + return game.supply +} + +function update_supply_possible_pass() { + init_supply() + init_supply_forbidden() + _supply_reached.fill(0) let sources = [] - for_each_seat( - game.command, - (seat) => { - if (set_has(reachable, seat)) - sources.push(seat) - }, - true - ) - return sources + for (let loc of game.supply.seats) + set_add(sources, loc) + for (let loc of game.supply.seaports) + set_add(sources, loc) + switch (current_season()) { + case SUMMER: + _supply_boats.fill(-1) + _supply_carts.fill(-1) + game.supply = search_supply_summer(get_lord_locale(game.command), game.supply.boats, game.supply.carts, sources) + break + case EARLY_WINTER: + case LATE_WINTER: + game.supply = search_supply_winter(get_lord_locale(game.command), game.supply.sleds, sources) + break + case RASPUTITSA: + game.supply = search_supply_rasputitsa(get_lord_locale(game.command), game.supply.boats, sources) + break + } } -function filter_usable_supply_seaports(reachable, ships) { - if (ships > 0) { - let sources = [] - if (game.active === TEUTONS) { - for (let port of data.seaports) { - if (set_has(reachable, port)) { - set_add(sources, port) - } - } - } - if (game.active === RUSSIANS) { - if (set_has(reachable, LOC_NOVGOROD)) { - set_add(sources, LOC_NOVGOROD) - } - } - return sources +function search_supply_cost() { + init_supply_forbidden() + _supply_reached.fill(0) + switch (current_season()) { + case SUMMER: + _supply_boats.fill(-1) + _supply_carts.fill(-1) + search_supply_summer(get_lord_locale(game.command), game.supply.boats, game.supply.carts, null) + break + case EARLY_WINTER: + case LATE_WINTER: + search_supply_winter(get_lord_locale(game.command), game.supply.sleds, null) + break + case RASPUTITSA: + search_supply_rasputitsa(get_lord_locale(game.command), game.supply.boats, null) + break } - return null + console.log("SUPPLY COST", _supply_stat) } function can_action_supply() { if (game.actions < 1) return false - return true // TODO - Lodya & pre-search? + return !!game.supply } function can_supply() { - if (game.supply.seats > 0 && game.supply.supply_seats.length > 0) + if (game.supply.available > 0 && game.supply.seats.length > 0) return true - if (game.supply.ships > 0 && game.supply.supply_seaports.length > 0) + if (game.supply.ships > 0 && game.supply.seaports.length > 0) return true return false } @@ -5547,10 +5771,27 @@ function goto_supply() { game.state = "supply_lodya" } else { init_supply() + resume_supply() game.state = "supply_source" } } +function resume_supply() { + if (game.supply.available + game.supply.ships === 0) { + game.supply.seats = [] + game.supply.seaports = [] + } else { + search_supply_cost() + game.supply.seats = game.supply.seats.filter(loc => _supply_reached[loc]) + game.supply.seaports = game.supply.seaports.filter(loc => _supply_reached[loc]) + } + + if (can_supply()) + game.state = "supply_source" + else + end_supply() +} + states.supply_source = { prompt() { if (!can_supply()) { @@ -5569,25 +5810,23 @@ states.supply_source = { if (game.supply.ships > 0) view.prompt += ` ${game.supply.ships} ship` - if (game.supply.seats > 0) - for (let source of game.supply.supply_seats) + if (game.supply.available > 0) + for (let source of game.supply.seats) gen_action_locale(source) if (game.supply.ships > 0) - for (let source of game.supply.supply_seaports) + for (let source of game.supply.seaports) gen_action_locale(source) view.actions.end_supply = 1 }, locale(source) { - // TODO: 2nd ed - no reusing of transports! - - if (game.supply.supply_seats.includes(source)) { + if (game.supply.seats.includes(source)) { log(`Supplied from seat at %${source}.`) if (is_famine_in_play()) { log("Famine.") game.flags.famine = 1 } - array_remove_item(game.supply.supply_seats, source) - game.supply.seats-- + game.supply.available-- + array_remove_item(game.supply.seats, source) } else { log(`Supplied from seaport at %${source}.`) game.supply.ships-- @@ -5595,16 +5834,119 @@ states.supply_source = { add_lord_assets(game.command, PROV, 1) - if (!can_supply()) - end_supply() + spend_supply_transport(source) }, end_supply: end_supply, } function end_supply() { - game.supply = 0 spend_action(1) resume_actions() + game.supply = 1 // supply is possible! +} + +function spend_supply_transport(source) { + if (source === get_lord_locale(game.command)) { + resume_supply() + return + } + + switch (current_season()) { + case SUMMER: + game.supply.here = get_lord_locale(game.command) + game.supply.end = source + game.state = "supply_path" + init_summer_path() + break + case EARLY_WINTER: + case LATE_WINTER: + search_supply_cost() + game.supply.sleds -= _supply_cost[source] + resume_supply() + break + case RASPUTITSA: + search_supply_cost() + game.supply.boats -= _supply_cost[source] + resume_supply() + break + } +} + +states.supply_path = { + prompt() { + view.prompt = "Supply: Trace path to supply source." + view.supply = [ game.supply.here, game.supply.end ] + if (game.supply.boats > 0) + view.prompt += ` ${game.supply.boats} boat` + if (game.supply.carts > 0) + view.prompt += ` ${game.supply.carts} cart` + for (let i = 0; i < game.supply.path.length; i += 2) { + let wayloc = game.supply.path[i] + gen_action_locale(wayloc >> 8) + } + }, + locale(next) { + let useloc = -1 + let useway = -1 + let twoway = false + for (let i = 0; i < game.supply.path.length; i += 2) { + let wayloc = game.supply.path[i] + let way = wayloc & 255 + let loc = wayloc >> 8 + if (loc === next) { + if (useloc < 0) { + useloc = loc + useway = way + } else { + twoway = true + } + } + } + if (twoway) { + game.state = "supply_path_way" + game.supply.next = next + } else { + walk_supply_path_way(next, useway) + } + }, +} + +function walk_supply_path_way(next, way) { + let type = data.ways[way].type + if (type === "waterway") + game.supply.boats-- + else + game.supply.carts-- + game.supply.here = next + game.supply.path = map_get(game.supply.path, (next << 8) | way) + if (game.supply.path === 0) + resume_supply() + else + // Auto-pick path if only one choice. + if (AUTOWALK && game.supply.path.length === 2) + walk_supply_path_way(game.supply.path[0] >> 8, game.supply.path[0] & 255) +} + +states.supply_path_way = { + prompt() { + view.prompt = "Supply: Trace path to supply source." + view.supply = [ game.supply.here, game.supply.end ] + if (game.supply.boats > 0) + view.prompt += ` ${game.supply.boats} boat` + if (game.supply.carts > 0) + view.prompt += ` ${game.supply.carts} cart` + for (let i = 0; i < game.supply.path.length; i += 2) { + let wayloc = game.supply.path[i] + let way = wayloc & 255 + let loc = wayloc >> 8 + if (loc === game.supply.next) + gen_action_way(way) + } + }, + way(way) { + game.state = "supply_path" + walk_supply_path_way(game.supply.next, way) + }, } // === ACTION: FORAGE === @@ -5930,6 +6272,7 @@ states.sail = { spend_all_actions() resume_actions() + update_supply_possible() }, } @@ -6424,7 +6767,6 @@ states.array_attacker = { } }, array: action_array_place, - array: action_array_place, lord: action_select_lord, end_array: end_array_attacker, } @@ -7481,10 +7823,6 @@ for each battle step: */ -function format_group(g) { - return g.map(p=>lord_name[game.battle.array[p]]).join(", ") -} - function format_strike_step() { // TODO: format strike group and target groups too? if (game.battle.storm) @@ -7494,7 +7832,6 @@ function format_strike_step() { function format_hits() { if (game.battle.xhits > 0 && game.battle.hits > 0) { - return `${game.battle.xhits} crossbow hits and ${game.battle.hits} hits` if (game.battle.xhits > 1 && game.battle.hits > 1) return `${game.battle.xhits} crossbow hits and ${game.battle.hits} hits` else if (game.battle.xhits > 1) diff --git a/tools/gendata.js b/tools/gendata.js index c671cf6..05ac486 100644 --- a/tools/gendata.js +++ b/tools/gendata.js @@ -5,6 +5,7 @@ const fs = require('fs') function cmpnum(a,b) { return a - b } +function cmpnum2(a,b) { return a[0] - b[0] } // :r !python3 genboxes.py const boxes = { @@ -307,24 +308,33 @@ locales.forEach(loc => { loc.adjacent = [] loc.adjacent_by_trackway = [] loc.adjacent_by_waterway = [] + loc.trackways = [] + loc.waterways = [] for (let data of loc.ways) { let to = data[0] for (let i = 1; i < data.length; ++i) { let way = data[i] - console.log("WAY", loc.name, to, way) if (!loc.adjacent.includes(to)) loc.adjacent.push(to) - if (ways[way].type === "trackway") - if (!loc.adjacent_by_trackway.includes(to)) + if (ways[way].type === "trackway") { + if (!loc.adjacent_by_trackway.includes(to)) { loc.adjacent_by_trackway.push(to) - if (ways[way].type === "waterway") - if (!loc.adjacent_by_waterway.includes(to)) + loc.trackways.push([to,way]) + } + } + if (ways[way].type === "waterway") { + if (!loc.adjacent_by_waterway.includes(to)) { loc.adjacent_by_waterway.push(to) + loc.waterways.push([to,way]) + } + } } } loc.adjacent.sort(cmpnum) loc.adjacent_by_trackway.sort(cmpnum) loc.adjacent_by_waterway.sort(cmpnum) + loc.trackways.sort(cmpnum2) + loc.trackways.sort(cmpnum2) }) function seats(list) { |