-
Notifications
You must be signed in to change notification settings - Fork 11
/
world.js
1344 lines (1190 loc) · 47.7 KB
/
world.js
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
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* world6.js
TM AI
Copyright (C) 2013 by Lode Vandevenne
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
//hex math and world map, and some rules related to buildings and towns
//when forming a town, you need 4 buildings in the cluster. However, the mermaids skipped river tile does not count, and the sanctuary counts for 2. This function encapsulates that knowledge.
//given building may not be B_NONE or undefined.
function getBuildingTownSize(building) {
if(building == B_MERMAIDS) return 0;
if(building == B_SA) return 2;
return 1;
}
//Grid coordinates to array coordinates
function arCo(x, y) {
return y * game.bw + x;
}
function arCo2(x, y, bw) {
return y * bw + x;
}
//returns coordinates of neighbor tile in given direction
function dirCo(x, y, dir, btoggle) {
var togglemod = (btoggle ? 0 : 1);
if(dir == D_NE) result = [(y % 2 == togglemod) ? x + 1 : x, y - 1];
else if(dir == D_E) result = [x + 1, y];
else if(dir == D_SE) result = [(y % 2 == togglemod) ? x + 1 : x, y + 1];
else if(dir == D_SW) result = [(y % 2 == togglemod) ? x : x - 1, y + 1];
else if(dir == D_W) result = [x - 1, y];
else if(dir == D_NW) result = [(y % 2 == togglemod) ? x : x - 1, y - 1];
else return null; //ERROR
if(outOfBounds(result[0], result[1])) return null; //out of bounds
return result;
}
//returns coordinates of other side of bridge pointing in that direction
function bridgeCo(x, y, dir, btoggle) {
var togglemod = (btoggle ? 0 : 1);
if(dir == D_N) return [x, y - 2];
if(dir == D_NE) return [(y % 2 == togglemod) ? x + 2 : x + 1, y - 1];
if(dir == D_SE) return [(y % 2 == togglemod) ? x + 2 : x + 1, y + 1];
if(dir == D_S) return [x, y + 2];
if(dir == D_SW) return [(y % 2 == togglemod) ? x - 1 : x - 2, y + 1];
if(dir == D_NW) return [(y % 2 == togglemod) ? x - 1 : x - 2, y - 1];
return [x, y]; //ERROR
}
var worldNames = []; // names for the dropdown
var worldGenerators = []; // array of functions that generate a world: takes parameter "game", sets game.bw, game.bh, game.btoggle and game.world array.
var worldCodeNames = [];
var codeNameToWorld = {}; //name to index
function registerWorld(name, codename, fun) {
worldNames.push(name);
worldCodeNames.push(codename);
worldGenerators.push(fun);
codeNameToWorld[codename] = worldNames.length - 1;
}
registerWorld('Standard', 'standard', initStandardWorld);
registerWorld('Randomized*', 'randomized', bind(randomizeWorld, false));
registerWorld('Randomized Small*', 'randomized_small', bind(randomizeWorld, true));
var standardWorld = [U,S,G,B,Y,R,U,K,R,G,B,R,K,
Y,I,I,U,K,I,I,Y,K,I,I,Y,N,
I,I,K,I,S,I,G,I,G,I,S,I,I,
G,B,Y,I,I,R,B,I,R,I,R,U,N,
K,U,R,B,K,U,S,Y,I,I,G,K,B,
S,G,I,I,Y,G,I,I,I,U,S,U,N,
I,I,I,S,I,R,I,G,I,Y,K,B,Y,
Y,B,U,I,I,I,B,K,I,S,U,S,N,
R,K,S,B,R,G,Y,U,S,I,B,G,R];
//The world array has the color of each hex. It is inited with the standard world map.
function initStandardWorld(game) {
game.bw = 13;
game.bh = 9;
game.btoggle = false;
game.world = clone(standardWorld);
}
function randomizeWorld(small) {
var world = game.world;
game.btoggle = false;
var NUMISLANDS = 5;
var NUMPERCOLOR = 11;
if(small) {
game.bh = 7;
game.bw = 11;
NUMISLANDS = 4;
NUMPERCOLOR = 7;
} else {
game.bw = 13;
game.bh = 9;
}
for(var y = 0; y < game.bh; y++)
for(var x = 0; x < game.bw; x++)
{
world[y * game.bw + x] = outOfBounds(x, y) ? N : I;
}
var done = 0;
while(done < NUMISLANDS) {
var r = done == 0 ? 0 : done == 1 ?
(game.bw - 1) : done == 2 ?
(game.bw * game.bh - 1) : done == 3 ?
(game.bw * game.bh - game.bw) : Math.floor((game.bw * game.bh / 2));
if(world[r] != I) continue;
world[r] = R + done;
done++;
}
done = 0;
var attempt = 0;
while(done < NUMPERCOLOR * 7 - NUMISLANDS) {
attempt++;
var r = randomInt(game.bw * game.bh);
if(world[r] != I) continue;
var x = r % game.bw;
var y = Math.floor(r / game.bw);
var neighbors = getNeighborTiles(x, y);
var c = I;
var ok = true;
for(var i = 0; i < neighbors.length; i++) {
var nc = getWorld(neighbors[i][0], neighbors[i][1]);
if(nc != I) {
if(c == I) {
c = nc;
} else {
if(c != nc) {
ok = false;
break;
}
}
}
}
if(c == I) ok = false;
if(attempt > 500) ok = true;
if(ok) {
attempt = 0;
world[r] = c;
done++;
}
}
//give all land tiles a temporary unexisting color
var landtiles = [];
for(var y = 0; y < game.bh; y++)
for(var x = 0; x < game.bw; x++)
{
var i = y * game.bw + x;
if(world[i] != I && world[i] != N) {
world[i] = 999;
landtiles.push(i);
}
}
//position ok for this color? That is, no same colored neighbors, no 2-dist for yellow.
var posok = function(x, y, color, impatient) {
if(color == Y && !impatient) {
var neighbors = getTilesWithinRadius(x, y, 2);
for(var i = 0; i < neighbors.length; i++) if(getWorld(neighbors[i][0], neighbors[i][1]) == color) return false;
return true;
} else if(color == B && !impatient) {
// should be next to river, but not in too large splotches
var neighbors = getNeighborTiles(x, y);
var count = 0;
for(var i = 0; i < neighbors.length; i++) {
if(getWorld(neighbors[i][0], neighbors[i][1]) == color) return false;
if(getWorld(neighbors[i][0], neighbors[i][1]) == I) count++;
}
return count > 0 && count < 3;
} else if(color == I) {
// try to extend existing waters
var neighbors = getNeighborTiles(x, y);
var count = 0;
for(var i = 0; i < neighbors.length; i++) {
if(getWorld(neighbors[i][0], neighbors[i][1]) == I) count++;
}
return count > 0;
} else {
var neighbors = getNeighborTiles(x, y);
for(var i = 0; i < neighbors.length; i++) if(getWorld(neighbors[i][0], neighbors[i][1]) == color) return false;
return true;
}
};
//yellow and blue first because they have more rectrictions
var bad = [];
var colors = [Y, B, U, K, G, S, R];
for(var c = 0; c < colors.length; c++) {
done = 0;
attempt = 0;
while(done < NUMPERCOLOR) {
var i = randomIndex(landtiles);
var r = landtiles[i];
var x = r % game.bw;
var y = Math.floor(r / game.bw);
if(world[r] == I || world[r] == N) continue;
attempt++;
var color = colors[c];
var impatient = attempt > 50 || (color == B && done > (NUMPERCOLOR * 2 / 3));
var giveup = attempt > 200;
if(giveup || posok(x, y, color, impatient)) {
world[r] = color;
landtiles.splice(i, 1);
done++;
if(giveup) bad.push(r);
}
}
}
//add ponds to the bad tiles
var isPond = function(x, y) {
if(getWorld(x, y) != I) return false;
var neighbors = getNeighborTiles(x, y);
var count = 0;
for(var i = 0; i < neighbors.length; i++) {
if(getWorld(neighbors[i][0], neighbors[i][1]) == I) return false;
}
return true;
};
for(var y = 0; y < game.bh; y++)
for(var x = 0; x < game.bw; x++)
{
if(isPond(x, y)) bad.push(arCo(x, y));
}
//fix up bad ones, e.g. for the last color it did it's likely there are bad positions
for(var i = 0; i < bad.length; i++) {
for(var i2 = 0; i2 < game.bw * game.bh; i2++) {
var i1 = bad[i];
if(i1 == i2) continue;
var color1 = world[i1];
var x1 = i1 % game.bw;
var y1 = Math.floor(i1 / game.bw);
var color2 = world[i2];
var x2 = i2 % game.bw;
var y2 = Math.floor(i2 / game.bw);
if(color1 == N || color2 == N || color2 == I) continue;
if(hexDist(x1, y1, x2, y2) < 3) continue; //posok function needs to know exact neighborhood closer than this, cannot use it when swapping nearby tiles
if(posok(x1, y1, color2, false) && posok(x2, y2, color1, false)) {
world[i1] = color2;
world[i2] = color1;
break;
}
}
}
}
//slightly different format than in the savegame: semicolons denote the width
function serializeWorld() {
var togglemod = (game.btoggle ? 0 : 1);
var result = '';
for(var y = 0; y < game.bh; y++) {
var w = game.bw;
if(y % 2 == togglemod) {
result += ' ';
w--;
}
for(var x = 0; x < w; x++) {
result += colorCodeName[getWorld(x, y)];
result += ((x + 1 < w) ? ',' : ';');
}
if(y + 1 != game.bh) result += '\n';
}
return result;
}
//slightly different format than in the savegame: semicolons denote the width
//TODO: let newlines denote the width
//TODO: don't save in global game object but in a given one
function parseWorld(text) {
var lines = text.split(';');
game.btoggle = false;
if(lines.length >= 2 && lines[0][0] == ' ' && lines[1][0] != ' ') game.btoggle = true; //TODO: maybe instead check line 1 has less of any whitespace than line 2...
var togglemod = (game.btoggle ? 0 : 1);
game.bh = lines.length - 1;
game.bw = 0;
var l = lines[0];
for(var j = 0; j < l.length; j++) {
var c = l.charAt(j);
if(codeNameToColor[c] != undefined) game.bw++;
}
for(var y = 0; y < game.bh; y++)
for(var x = 0; x < game.bw; x++)
{
setWorld(x, y, (y % 2 == togglemod && x + 1 >= game.bw) ? N : I);
}
var x = 0;
var y = 0;
for(var i = 0; i < lines.length; i++) {
var l = lines[i];
for(var j = 0; j < l.length; j++) {
var c = l.charAt(j);
if(codeNameToColor[c] != undefined) {
setWorld(x, y, codeNameToColor[c]);
x++;
if(x >= game.bw || (y % 2 == togglemod && x + 1 >= game.bw)) break;
}
}
x = 0;
y++;
if(y >= game.bh) break;
}
}
function setWorld(x, y, color) {
game.world[arCo(x, y)] = color;
}
function getWorld(x, y) {
return game.world[arCo(x, y)];
}
function initBridges(bridges, bw, bh) {
bridges.length = 0;
for(var y = 0; y < bh; y++)
for(var x = 0; x < bw; x++)
{
bridges[arCo2(x, y, bw)] = [ N, N, N ];
}
}
function initBuildings(buildings, bw, bh) {
buildings.length = 0;
for(var y = 0; y < game.bh; y++)
for(var x = 0; x < game.bw; x++)
{
buildings[arCo2(x, y, bw)] = [ B_NONE, N ];
}
}
function setBuilding(x, y, building, color) {
game.buildings[arCo(x, y)] = [building, color];
}
// returns array of [building enum, woodcolor]
function getBuilding(x, y) {
return game.buildings[arCo(x, y)];
}
//returns tiles connected to the given tile by bridge or direct adjacency (that is, everything that connects towns or allows power leeching), as array of [x,y] coordinates
function getConnectedTiles(tx, ty) {
var result = [];
for(var y = ty - 2; y <= ty + 2; y++)
for(var x = tx - 2; x <= tx + 2; x++)
{
if(outOfBounds(x, y)) continue;
if(x == tx && y == ty) continue;
var tile = getWorld(x, y);
if(tile == I) {
if(getBuilding(x, y)[0] == B_MERMAIDS && hexDist(x, y, tx, ty) == 1) result.push([x, y]);
else continue;
}
if(tile == N) continue;
if(landConnected(x, y, tx, ty)) {
result.push([x, y]);
}
}
return result;
}
//returns the 6 neighbors (or less if at the sides)
function getNeighborTiles(x, y) {
var result = [];
var dirs = [D_NE, D_E, D_SE, D_SW, D_W, D_NW];
for(var i = 0; i < dirs.length; i++) {
var t = dirCo(x, y, dirs[i], game.btoggle);
if(t) result.push(t);
}
return result;
}
//returns all tiles up to (and including) that radius, except the center tile itself. E.g. for radius 1, it has a similar result (except ordering) as getNeighborTiles.
function getTilesWithinRadius(x, y, radius) {
var result = [];
for(var y2 = y - radius; y2 <= y + radius; y2++)
for(var x2 = x - radius; x2 <= x + radius; x2++)
{
var dist = hexDist(x, y, x2, y2);
if(dist <= radius && dist > 0) result.push([x2, y2]);
}
return result;
}
var TOWNWAMOUNT = 4; //number of buildings required to form a town (an SA counts for two)
function calculateClustersGeneric(clusters, clustermap, isBuildingFun, getConnectedTilesFun) {
for(var y = 0; y < game.bh; y++)
for(var x = 0; x < game.bw; x++)
{
clustermap[arCo(x, y)] = 0;
}
clusters.length = 0; //clear an array while keeping the references to it
// TODO: why is this dummy cluster there? Maybe to indicate which tiles belong to no cluster? If so, document that.
clusters[0] = {};
clusters[0].color = N; //Not any player
clusters[0].tiles = [];
clusters[0].power = 0;
clusters[0].townamount = 0;
clusters[0].networkamount = 0;
for(var y = 0; y < game.bh; y++)
for(var x = 0; x < game.bw; x++)
{
if(clustermap[arCo(x, y)] != 0) continue;
var b = getBuilding(x, y);
if(!isBuildingFun(b[0])) continue;
var stack = [[x, y]];
var newclusterindex = clusters.length;
clusters[newclusterindex] = {};
var cluster = clusters[newclusterindex];
cluster.tiles = [];
cluster.power = 0;
cluster.townamount = 0;
cluster.networkamount = 0;
cluster.color = b[1];
var player = game.players[woodColorToPlayerMap[cluster.color]];
if(!player) throw new Error('no player for building color');
while(stack.length > 0) {
var t = stack.pop();
if(clustermap[arCo(t[0], t[1])] != 0) continue; //already handled
var b2 = getBuilding(t[0], t[1]);
if(!isBuildingFun(b2[0]) || b2[1] != b[1]) continue;
clustermap[arCo(t[0], t[1])] = newclusterindex;
cluster.tiles.push(t);
// The values for forming a town, when power is >= 7 (or >= 6 for some players), and amount >= 4, it's a town (Sanctuary has getBuildingTownSize=2 to represent its ability to form a town with 3 buildings, so 4 always works)
// NOTE: for the end game network scoring, use "tiles.length" instead (and call this with the reachable rather than connected tiles function)
cluster.power += player.getFaction().getBuildingPower(b2[0]);
cluster.townamount += getBuildingTownSize(b2[0]);
cluster.networkamount += (b2[0] == B_MERMAIDS ? 0 : 1);
stack = stack.concat(getConnectedTilesFun(player, t[0], t[1]));
}
}
}
//for biggest network end game scoring.
//similar to townclusters and townmap, but for connected networks that span over rivers, dwarve/fakir tunneling, ...
var networkclusters = [];
var networkmap = [];
function calculateNetworkClusters() {
calculateClustersGeneric(networkclusters, networkmap, function(b) { return b!= B_NONE && b != B_MERMAIDS }, function(player, x, y) {
return getNetworkConnectedTiles(player, x, y, true, true);
});
}
//get biggest network of this player. Precondition: calculateNetworkClusters() must have been called
function getBiggestNetwork(player) {
var result = 0;
for(var i = 0; i < networkclusters.length; i++) {
if(networkclusters[i].color == player.woodcolor) result = Math.max(result, networkclusters[i].networkamount);
}
return result;
}
//clusters and the townmap, for clusters of buildings (potentially forming a town). All buildings touching or connected by bridge form a cluster.
//The townmap value is an index in the clusters array.
//townclusters is an array with cluster index as index, object with color,tiles(array of[x,y]),power,townamount,netamount as value
//The townmap is an array with world 1D arco coordinate as index, and cluster index as value
var townclusters = [];
var townmap = [];
function calculateTownClusters() {
calculateClustersGeneric(townclusters, townmap, function(b) { return b!= B_NONE }, function(player, x, y) {
//player parameter ignored
return getConnectedTiles(x, y);
});
}
//index must be > 0. It is an index in the clusters array.
function getTownClusterColor(index) {
return townclusters[index].color;
}
//returns whether new town is formed after an upgrade of an existing building. reqpower = 6 or 7
//returns empty array if no town is formed, array with the cluster index of which this building was part otherwise (for consistency with the next functions)
function makesNewTownByUpgrade(player, x, y, frombuilding, tobuilding, reqpower) {
var frompower = player.getFaction().getBuildingPower(frombuilding);
var topower = player.getFaction().getBuildingPower(tobuilding);
var diff = topower - frompower;
if(diff <= 0) return false; //TP->TE keeps same power, no new town can be formed this way
var index = townmap[arCo(x, y)];
var cluster = townclusters[index];
var power = cluster.power + diff; //all upgrades increase power by one, except to TE which is already canceled out above.
var amount = cluster.townamount + (tobuilding == B_SA ? 1 : 0); //sanctuary counts for amount 2
if(amount >= TOWNWAMOUNT && power >= reqpower) {
//only if it makes new town, not if it already existed before as town.
//so either if the power reached exactly the required power, or, in case of SA upgrade, the amount reached the exact required amount
if((power >= reqpower && (power - diff < reqpower)) || (amount == TOWNWAMOUNT && amount != cluster.townamount)) return [index]
return [];
}
return [];
}
//returns if connecting these tiles (by bridge or new building) forms a new town
//tiles = array of tile coordinates involved in the connecting
//currentpower/currentamount: power and amount already involved before the connection (= the new building that connects them)
//returns empty array if no town is formed, array of all the connected clusters otherwise (can be more than 1 if multiple clusters got joined together: they don't get a new single index at this point)
function makesNewTownByConnectingTiles(tiles, currentpower, currentamount, reqpower, color) {
var clusters = {}; //amount of clusters touched by this tile
for(var i = 0; i < tiles.length; i++) {
var t = tiles[i];
var c = townmap[arCo(t[0], t[1])];
if(c == 0) continue;
clusters[c] = 1;
}
var power = currentpower;
var amount = currentamount;
var result = [];
for(var c in clusters) {
if(!clusters.hasOwnProperty(c)) continue;
if(getTownClusterColor(c) != color) continue;
var cluster = townclusters[c];
var subpower = cluster.power
var subamount = cluster.townamount;
if(subpower >= reqpower && subamount >= TOWNWAMOUNT) return false; //already touches an existing town, no new town formed
power += subpower;
amount += subamount;
result.push(c);
}
if(power >= reqpower && amount >= TOWNWAMOUNT) {
return result;
} else {
return [];
}
}
//returns whether new town is formed building a new dwelling or mermaids town connection tile. reqpower = 6 or 7
//returns empty array if no town is formed, array of all the connected clusters otherwise (can be more than 1 if multiple clusters got joined together, they don't get a new single index at this point however)
function makesNewTownByBuilding(x, y, building, reqpower, color) {
var tiles = getConnectedTiles(x, y);
var player = game.players[woodColorToPlayerMap[color]];
if(!player) throw new Error('no player for building color');
return makesNewTownByConnectingTiles(tiles, player.getFaction().getBuildingPower(building), getBuildingTownSize(building), reqpower, color);
}
//returns whether new town is formed when placing a bridge there. reqpower = 6 or 7
//precondition: must be a legal new bridge
//returns empty array if no town is formed, array of all the connected clusters otherwise (can be more than 1 if multiple clusters got joined together, they don't get a new single index at this point however)
function makesNewTownByBridge(x0, y0, x1, y1, reqpower, color) {
return makesNewTownByConnectingTiles([[x0, y0], [x1, y1]], 0, 0, reqpower, color);
}
//whether this tile is part of, or touches, an existing town
function touchesExistingTown(x, y, color) {
var tiles = getConnectedTiles(x, y);
tiles.push([x, y]);
for(var i = 0; i < tiles.length; i++) {
if(getWorld(tiles[i][0], tiles[i][1]) == color && isInTown(tiles[i][0], tiles[i][1])) return true;
}
return false;
}
function touchesExistingTownWood(x, y, woodcolor) {
var tiles = getConnectedTiles(x, y);
tiles.push([x, y]);
for(var i = 0; i < tiles.length; i++) {
var build1 = getBuilding(tiles[i][0], tiles[i][1]);
if(build1[1] == woodcolor && isInTown(tiles[i][0], tiles[i][1])) return true;
}
return false;
}
//returns the largest town cluster (by power) of that color that this touches (including itself), or null if none
function mostPowerfulTouchedCluster(x, y, color) {
var tiles = getConnectedTiles(x, y);
tiles.push([x, y]);
var power = 0;
var result = null;
for(var i = 0; i < tiles.length; i++) {
var x = tiles[i][0];
var y = tiles[i][1];
var index = townmap[arCo(x, y)];
if(!index) continue;
var cluster = townclusters[index];
if(cluster && getTownClusterColor(index) == color && cluster.power > power) {
power = cluster.power;
result = cluster;
}
}
return result;
}
function addToTestConnections(testconnections, co0, co1) {
var i0 = arCo(co0[0], co0[1]);
if(!testconnections[i0]) testconnections[i0] = [];
var o = testconnections[i0];
o.push([co1[0], co1[1]]);
}
function getConnectedTilesWithTestConnections(x, y, testconnections) {
var tiles = getConnectedTiles(x, y);
var o = testconnections[arCo(x, y)];
if(o) {
for(var i = 0; i < o.length; i++) tiles.push(o[i]);
}
return tiles;
}
//for checking whether an action forms a town
//updates the given town clusters based on the given action, merging or increasing them as needed
//returns an array as follows: [changed cluster, [removed clusters], extrapower, extraamount], where all clusters are indicated by their index in the new cluster map
//removed clusters still have an index in testclusters, but no tile in testmap refers to them anymore:
//they are obsolete (and all part of the changed cluster), but are kept to check what their old sizes were.
//the removed clusters will also get a new member "obsolete" containing the index of the new cluster they point to
//extrapower and extraamount is what the action itself adds (e.g. a house adds 1 power and amount, while a bridge adds nothing)
//testconnections is a map of tiles to an array of tile coordinates they connect to, to keep track of additional bridges added that are not yet known by the getConnectedTiles function.
function updateTestClusters(action, testclusters, testmap, testconnections, color) {
if(isBridgeAction(action)) {
var i0 = testmap[arCo(action.cos[0][0], action.cos[0][1])];
var i1 = testmap[arCo(action.cos[1][0], action.cos[1][1])];
if(i0 == i1 || i0 == 0 || i1 == 0) return [0, [], 0, 0]; //bridge connecting its own or an empty cluster does nothing
var oldc0 = testclusters[i0];
var oldc1 = testclusters[i1];
if(oldc0.color != color || oldc1.color != color) return [0, [], 0, 0];
//merge the two clusters into a new one
var newi = testclusters.length; //index of new cluster
testclusters[newi] = {};
var newc = testclusters[newi];
newc.tiles = [];
newc.power = oldc0.power + oldc1.power;
newc.townamount = oldc0.townamount + oldc1.townamount;
newc.networkamount = oldc0.networkamount + oldc1.networkamount;
newc.color = color;
for(var i = 0; i < oldc0.tiles.length; i++) newc.tiles.push(oldc0.tiles[i]);
for(var i = 0; i < oldc1.tiles.length; i++) newc.tiles.push(oldc1.tiles[i]);
for(var i = 0; i < newc.tiles.length; i++) testmap[arCo(newc.tiles[i][0], newc.tiles[i][1])] = newi;
addToTestConnections(testconnections, action.cos[0], action.cos[1]);
addToTestConnections(testconnections, action.cos[1], action.cos[0]);
oldc0.obsolete = newi;
oldc1.obsolete = newi;
return [newi, [i0, i1], 0, 0];
}
if(isTownyBuildAction(action)) {
var x = action.co[0];
var y = action.co[1];
var tiles = getConnectedTilesWithTestConnections(x, y, testconnections);
var clusters = {}; //amount of clusters touched by this tile
for(var i = 0; i < tiles.length; i++) {
var t = tiles[i];
var c = testmap[arCo(t[0], t[1])];
if(c == 0) continue;
clusters[c] = 1;
}
var clusterarray = []; //array of cluster indices of relevant clusters
for(var c in clusters) {
if(!clusters.hasOwnProperty(c)) continue;
if(testclusters[c].color != color) continue;
clusterarray.push(c);
}
if(clusterarray.length == 0) {
var newc = {};
var newi = testclusters.length;
testclusters.push(newc);
newc.tiles = [[x,y]];
newc.power = 0;
newc.townamount = 0;
newc.networkamount = 0;
newc.color = color;
testmap[arCo(x, y)] = newi;
if(action.type != A_CONNECT_WATER_TOWN) {
newc.power++;
newc.townamount++;
newc.networkamount++;
return [newi, [], 1, 1];
}
else return [newi, [], 0, 0];
}
else if(clusterarray.length == 1) {
var c = clusterarray[0]; //affected cluster index
var cluster = testclusters[c];
cluster.tiles.push([x, y]);
testmap[arCo(x, y)] = c;
if(action.type != A_CONNECT_WATER_TOWN) {
cluster.power++;
cluster.townamount++;
cluster.networkamount++;
return [c, [], 1, 1];
}
else return [c, [], 0, 0];
} else {
var newc = {};
var newi = testclusters.length;
testclusters.push(newc);
newc.tiles = [[x,y]];
newc.power = 0;
newc.townamount = 0;
newc.networkamount = 0;
newc.color = color;
for(var j = 0; j < clusterarray.length; j++) {
var oldc = testclusters[clusterarray[j]];
for(var i = 0; i < oldc.tiles.length; i++) newc.tiles.push(oldc.tiles[i]);
newc.power += oldc.power;
newc.townamount += oldc.townamount;
newc.networkamount += oldc.networkamount;
oldc.obsolete = testclusters.length - 1;
}
for(var i = 0; i < newc.tiles.length; i++) testmap[arCo(newc.tiles[i][0], newc.tiles[i][1])] = newi;
if(action.type != A_CONNECT_WATER_TOWN) {
newc.power++;
newc.townamount++;
newc.networkamount++;
return [newi, clusterarray, 1, 1];
}
else return [newi, clusterarray, 0, 0];
}
}
else if (isUpgradeAction(action) && getUpgradeActionOutputBuilding(action) != B_TE) {
var c = testmap[arCo(action.co[0], action.co[1])];
var cluster = testclusters[c];
var extrapower = 1;
var extramount = (getUpgradeActionOutputBuilding(action) == B_SA) ? 1 : 0;
cluster.power += extrapower;
cluster.townamount += extramount;
return [c, [], extrapower, extramount];
}
return [0, [], 0, 0];
}
//if multiple actions in the same sequence together form a town, actionCreatesTown misses it. This is a heavy duty one for those cases.
//For example for:
//-placing mermaids tile after upgrading or building something.
//-chaos magicians double action
//-halflings upgrade to SH, then dig and build dwelling next to it
//It returns the amount of towns formed by the given action.
//It only returns towns formed by action. It takes previous actions from the actions array into account (e.g. building size increases they make),
//but will not return towns already completely formed by those earlier actions.
function actionsCreateTown(player, actions, action) {
// TODO! make a copy of all the cluster information and virtually work on them
//TODO: I think this function does not work correct if in this action sequence, a previous action also creates a town, and townTilesAvailable is 1.
var testclusters = clone(townclusters);
var testmap = clone(townmap);
var testconnections = {};
var reqpower = getTownReqPower(player);
var involved = {}; //the indices of the clusters involved in the towns (to avoid duplicates, e.g when both upgrading to SA and taking 6 town size tile)
for(var i = 0; i < actions.length; i++) {
var a = actions[i];
var iscurrent = a == action;
//favtiles must be checked first, for when you pick fav tile for town size 6 and at the same time upgrade to SA making some town size 6.
for(var j = 0; j < a.favtiles.length; j++) {
if(a.favtiles[j] == T_FAV_2F_6TW) {
reqpower = 6; //from now on for next actions this reqpower is used
if(iscurrent) {
var tw = getPlayerTownsOfSize6(player.woodcolor, testclusters);
for(var k = 0; k < tw.length; k++) involved[tw[k]] = 1;
}
}
}
var result = updateTestClusters(a, testclusters, testmap, testconnections, player.woodcolor);
var c = result[0];
if(c != 0 && iscurrent && testclusters[c].power >= reqpower && testclusters[c].townamount >= 4) {
var oldc = result[1];
var extrapower = result[2];
var extraamount = result[3];
if(oldc.length == 0) {
var oldpower = testclusters[c].power - extrapower;
var oldamount = testclusters[c].townamount - extraamount;
if(oldpower < reqpower || oldamount < 4) {
involved[c] = 1;
}
} else {
var already = false; //was one of the old clusters already a town?
for(var j = 0; j < oldc.length; j++) {
if(testclusters[oldc[j]].power >= reqpower && testclusters[oldc[j]].townamount >= 4) {
already = true;
break;
}
}
if(!already) {
involved[c] = 1;
}
}
}
if(iscurrent) break; //the next actions don't matter anymore for this check
}
var num = 0;
for(i in involved) {
if(!involved.hasOwnProperty(i)) continue;
num++;
}
//if there are no more town tiles left, it does not count, not even for extra VP bonus, swarmlings 2 workers, etc...
return Math.min(num, townTilesAvailable(num));
}
//numactions = where to stop testing (set to actions.length to do all)
//TODO: after extensive testing (scenarios with A_DOUBLE, mermaid town, bridges, fav6, ...), use this function in actionsCreateTown to remove code duplication
function actionsCreateTowns(player, actions, numactions) {
var result = [];
for(var i = 0; i < numactions; i++) result[i] = 0;
var testclusters = clone(townclusters);
var testmap = clone(townmap);
var testconnections = {};
var reqpower = getTownReqPower(player);
var involved = {}; //the indices of the clusters involved in the towns (to avoid duplicates, e.g when both upgrading to SA and taking 6 town size tile)
//if there are no more town tiles left, it does not count, not even for extra VP bonus, swarmlings 2 workers, etc...
var tilesleft = townTilesAvailable(999);
for(var i = 0; i < numactions; i++) {
var a = actions[i];
//favtiles must be checked first, for when you pick fav tile for town size 6 and at the same time upgrade to SA making some town size 6.
for(var j = 0; j < a.favtiles.length; j++) {
if(a.favtiles[j] == T_FAV_2F_6TW) {
reqpower = 6; //from now on for next actions this reqpower is used
var tw = getPlayerTownsOfSize6(player.woodcolor, testclusters);
for(var k = 0; k < tw.length; k++) {
involved[tw[k]] = 1;
result[i]++;
tilesleft--;
if(tilesleft <= 0) return result;
}
}
}
var updated = updateTestClusters(a, testclusters, testmap, testconnections, player.woodcolor);
var c = updated[0];
if(c != 0 && testclusters[c].power >= reqpower && testclusters[c].townamount >= 4) {
var oldc = updated[1]; //removed clusters (if any)
var extrapower = updated[2];
var extraamount = updated[3];
if(oldc.length == 0) {
var oldpower = testclusters[c].power - extrapower;
var oldamount = testclusters[c].townamount - extraamount;
if(oldpower < reqpower || oldamount < 4) {
if(!involved[c]) {
involved[c] = 1;
result[i]++;
tilesleft--;
if(tilesleft <= 0) return result;
}
}
} else {
var already = false; //was one of the old clusters already a town?
for(var j = 0; j < oldc.length; j++) {
if(testclusters[oldc[j]].power >= reqpower && testclusters[oldc[j]].townamount >= 4) {
already = true;
}
}
if(!already&& !involved[c]) {
involved[c] = 1;
result[i]++;
tilesleft--;
if(tilesleft <= 0) return result;
}
}
}
}
return result;
}
//This is for identifying new towns of power 6 after getting that favor tile
//returns array of cluster indices
function getPlayerTownsOfSize6(color, clusters) {
var result = [];
for(var i = 1; i < clusters.length; i++) {
if(getTownClusterColor(i) == color && clusters[i].power == 6 && clusters[i].townamount >= TOWNWAMOUNT && !clusters[i].obsolete) result.push(i);
}
return result;
}
function townClusterContains(index, x, y) {
return townmap[arCo(x, y)] == index;
}
function isInTown(x, y) {
var index = townmap[arCo(x, y)];
if(!index) return false;
var cluster = townclusters[index];
var color = getTownClusterColor(index);
var player = game.players[woodColorToPlayerMap[color]];
return cluster && cluster.power >= getTownReqPower(player) && cluster.townamount >= 4;
}
//out of range of the hex board
function outOfBounds(x, y) {
if(x < 0 || y < 0 || x >= game.bw || y >= game.bh) return true;
var togglemod = (game.btoggle ? 0 : 1);
// TODO: is this check actually needed? Those tiles are already of type "N".
if(y % 2 == togglemod && x == game.bw - 1) return true;
return false;
}
//Checks if two tiles are connected by the player's land or shipping ability, but does NOT take intermediate jumps using
//other buildings of the player into account, so does not determine if two far apart buildings are connected to each other through the whole players network.
//tunnelcarpet: whether to include connections with extra cost: dwarves tunneling and fakirs carpets
//endscoring: if true:
//-riverwalkers get a virtual land-distance of 1, so some tiles that are normally not reachable for them are: for final scoring their touching buildings are connected.
//-bonus tile shipping is not taken into account
function networkConnected(player, x0, y0, x1, y1, tunnelcarpet, endscoring) {
if((endscoring || player.landdist == 1) && landConnected(x0, y0, x1, y1)) {
return true;
}
if(waterDistance(x0, y0, x1, y1) <= getShipping(player, endscoring) /*this should be always 0 for dwarves and fakirs, hence correctly supporting their non-shipping*/) {
return true;
}
if(tunnelcarpet && factionConnected(player, x0, y0, x1, y1)) {
return true;
}
return false;
}
//is this tile in reach by one of the buildings of this color?
//tunnelcarpet: whether to include connections with extra cost: dwarves tunneling and fakirs carpets
function inReach(player, x, y, tunnelcarpet) {
return inReachButDontCountCo(player, x, y, tunnelcarpet, null);
}
//dongcountco: coordinates to exclude for reachability, e.g. because player just built dwelling there. May be null/undefined to allow all coords
function inReachButDontCountCo(player, x, y, tunnelcarpet, dontcountco) {
var color = player.woodcolor;
var extra = getShipping(player, false);
if(player.faction == F_DWARVES || player.faction == F_FAKIRS) extra = player.tunnelcarpetdistance + 1;
if(extra < 1) extra = 1; //for bridges
for(var tx = x - extra - 1; tx <= x + extra + 1; tx++)
for(var ty = y - extra - 1; ty <= y + extra + 1; ty++) {
if(outOfBounds(tx, ty)) continue;
if(dontcountco && tx == dontcountco[0] && ty == dontcountco[1]) continue;
var building = getBuilding(tx, ty);
if(building[0] == B_NONE || building[0] == B_MERMAIDS || building[1] != color) continue;
if(networkConnected(player, x, y, tx, ty, tunnelcarpet, false)) {
return true;
}
}
return false;
}
//get all tiles reachable from x,y for the given player (this assumes the player has a building on x,y), reachable through shipping etc... too.
//tunnelcarpet: whether to include connections with extra cost: dwarves tunneling and fakirs carpets