Merge remote-tracking branch 'origin/main' into defenseposture

This commit is contained in:
1brucben
2025-04-19 02:12:28 +02:00
21 changed files with 653 additions and 343 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+88 -4
View File
@@ -1,5 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="512px" height="512px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path fill="#9e0000" d="M 57.5,16.5 C 79.6232,16.1252 88.4565,26.7919 84,48.5C 83.2196,51.5117 82.0529,54.3451 80.5,57C 111.436,74.9338 136.936,98.7672 157,128.5C 182.331,109.668 210.831,100.334 242.5,100.5C 242.5,106.833 242.5,113.167 242.5,119.5C 215.494,119.225 191.16,127.058 169.5,143C 181.768,144.418 194.101,145.418 206.5,146C 223.046,163.709 239.379,181.543 255.5,199.5C 271.621,181.543 287.954,163.709 304.5,146C 316.899,145.418 329.232,144.418 341.5,143C 319.84,127.058 295.506,119.225 268.5,119.5C 268.5,113.167 268.5,106.833 268.5,100.5C 300.169,100.334 328.669,109.668 354,128.5C 374.064,98.7672 399.564,74.9338 430.5,57C 422.929,44.1613 423.929,32.1613 433.5,21C 441.053,16.4447 449.053,15.4447 457.5,18C 478.079,25.4111 490.079,39.9111 493.5,61.5C 490.867,80.2657 480.201,87.4324 461.5,83C 458.488,82.2196 455.655,81.0529 453,79.5C 435.066,110.436 411.233,135.936 381.5,156C 400.886,181.491 410.72,210.324 411,242.5C 404.924,243.476 398.758,243.81 392.5,243.5C 391.875,215.457 383.375,190.124 367,167.5C 365.477,180.07 364.477,192.737 364,205.5C 344.658,222.677 325.492,240.01 306.5,257.5C 367.877,327.636 426.377,399.969 482,474.5C 486.572,480.683 490.572,487.016 494,493.5C 465.55,472.718 437.384,451.551 409.5,430C 357.697,387.864 306.197,345.364 255,302.5C 178.278,369.57 98.9443,433.237 17,493.5C 20.4282,487.016 24.4282,480.683 29,474.5C 84.5044,400.043 143.004,327.876 204.5,258C 185.762,240.273 166.596,222.773 147,205.5C 146.523,192.737 145.523,180.07 144,167.5C 127.625,190.124 119.125,215.457 118.5,243.5C 112.242,243.81 106.076,243.476 100,242.5C 100.28,210.324 110.114,181.491 129.5,156C 99.7672,135.936 75.9338,110.436 58,79.5C 46.6724,86.2353 35.5058,86.0686 24.5,79C 18.004,71.1766 16.1706,62.3433 19,52.5C 25.9173,34.08 38.7506,22.08 57.5,16.5 Z"/></g>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 100"
id="svg10"
sodipodi:docname="TraitorIcon.svg"
width="100"
height="100"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs14" />
<sodipodi:namedview
id="namedview12"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
height="100px"
inkscape:zoom="6.984"
inkscape:cx="49.971363"
inkscape:cy="62.142039"
inkscape:window-width="3072"
inkscape:window-height="1653"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="svg10" />
<g
transform="translate(0,-952.36218)"
id="g4">
<path
style="color:#000000;text-indent:0;text-transform:none;direction:ltr;baseline-shift:baseline;enable-background:accumulate"
d="m 49.682164,957.35229 c -0.08371,0.007 -0.167129,0.0173 -0.249965,0.0312 l -35.995035,7.00077 C 12.072501,964.6449 10.989932,965.96376 11,967.35339 v 32.00351 c 0.0012,0.13594 0.01165,0.27179 0.03125,0.4063 1.29114,9.0104 3.853487,15.5969 8.498827,22.2837 0.990121,1.4292 3.318267,1.6525 4.561871,0.4375 4.250005,-4.1812 7.885056,-7.78 11.967099,-11.9697 0.44046,-0.4164 0.750052,-0.9694 0.87488,-1.5626 0.628358,-3.8527 1.729676,-9.31167 2.530901,-13.59583 h 15.529108 c 1.243283,0.002 2.441795,-0.85453 2.843357,-2.03147 l 9.998621,-29.00319 c 0.567687,-1.6172 -0.568544,-3.6061 -2.249689,-3.93794 l -14.997932,-3.00033 c -0.298144,-0.0562 -0.604826,-0.0667 -0.906125,-0.0312 z m 23.902953,5.00055 c -1.078435,0.15321 -2.042235,0.94196 -2.405918,1.96897 L 58.899643,998.35679 H 44.995311 c -1.406702,-0.005 -2.733797,1.11295 -2.968341,2.50031 l -1.81225,10.9699 -14.27928,13.3139 c -1.128859,1.0576 -1.217322,3.0338 -0.187474,4.188 6.269465,7.139 13.562725,12.4576 22.778108,17.627 0.892103,0.5009 2.044992,0.5009 2.937095,0 19.535202,-10.9581 34.280717,-24.7622 37.494828,-47.1927 0.06997,-10.76177 0.03124,-21.61653 0.03124,-32.40981 0.0049,-1.40704 -1.112648,-2.73446 -2.499655,-2.96908 l -11.99834,-2.00021 c -0.298144,-0.0562 -0.604826,-0.0667 -0.906125,-0.0312 z m -23.621742,1.06261 11.060975,2.18774 -8.186371,23.75262 H 36.996414 c -1.378861,0.004 -2.67845,1.08304 -2.937095,2.43777 l -2.812112,15.06422 c -2.889686,2.9098 -5.919847,5.9695 -8.686302,8.7197 -2.891489,-4.8594 -4.568269,-9.6725 -5.561733,-16.50187 v -29.25322 z m 25.996415,5.28184 7.03028,1.18763 v 29.19071 c -2.868962,19.61817 -15.058331,31.46397 -32.995449,41.78587 -7.128175,-4.1138 -12.790202,-8.2954 -17.716306,-13.4077 l 12.779487,-11.9076 c 0.479911,-0.4525 0.804009,-1.0673 0.906125,-1.719 l 1.59353,-9.4698 h 13.435647 c 1.225447,-0.01 2.403546,-0.8446 2.812112,-2.0002 z"
fill="#000000"
fill-opacity="1"
fill-rule="nonzero"
stroke="none"
marker="none"
visibility="visible"
display="inline"
overflow="visible"
id="path2" />
</g>
<path
style="fill:#d40000;fill-opacity:0;stroke-width:0.143184"
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
id="path944" />
<path
style="fill:#d40000;fill-opacity:0;stroke-width:0.143184"
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
id="path983" />
<path
style="fill:#d40000;fill-opacity:0;stroke-width:0.143184"
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
id="path1022" />
<path
style="fill:#d40000;fill-opacity:0;stroke-width:0.143184"
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
id="path1061" />
<path
style="fill:#d40000;fill-opacity:0;stroke-width:0.143184"
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
id="path1100" />
<path
style="fill:#d40000;fill-opacity:0;stroke-width:0.143184"
d="M 22.451679,62.831985 C 22.040716,62.318926 20.614744,59.414324 19.904415,57.643392 18.969118,55.311589 18.276544,53.003918 17.644871,50.114548 L 17.190978,48.038373 17.14702,32.753436 c -0.02418,-8.406715 0.006,-15.283838 0.06717,-15.282495 0.06112,0.0013 7.450905,-1.423883 16.421746,-3.16717 l 16.310618,-3.169613 5.310229,1.037928 c 2.920626,0.570861 5.383063,1.077918 5.472081,1.126793 0.108366,0.0595 -1.175715,3.968788 -3.885785,11.829987 l -4.047636,11.741123 -8.356483,0.07159 c -6.695958,0.05736 -8.427615,0.110521 -8.714445,0.267501 -0.60381,0.330461 -1.419391,1.193902 -1.576025,1.66851 -0.08176,0.247726 -0.783479,3.851808 -1.559383,8.00907 l -1.410734,7.55866 -4.287731,4.287731 c -3.320136,3.320136 -4.321858,4.245124 -4.438961,4.098932 z"
id="path1139" />
<path
style="fill:#000000;fill-opacity:1;stroke-width:0.143184"
d="m 40.914948,36.977471 c 2.185353,-0.02223 5.761384,-0.02223 7.946736,0 2.185352,0.02223 0.397337,0.04042 -3.973368,0.04042 -4.370704,0 -6.15872,-0.01819 -3.973368,-0.04042 z"
id="path1178" />
<path
style="fill:#aa0000;fill-opacity:1;stroke-width:0.143184"
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
id="path9953" />
<path
style="fill:#aa0000;fill-opacity:1;stroke-width:0.143184"
d="M 49.327033,94.804719 C 48.714427,94.60513 47.768416,94.086861 45.174685,92.529868 37.917796,88.173624 32.73106,84.14232 27.795635,79.022291 c -1.145161,-1.187994 -2.213316,-2.431436 -2.374239,-2.763859 -0.397355,-0.820826 -0.385516,-1.87334 0.02963,-2.634656 0.211883,-0.388557 2.674852,-2.777493 7.195017,-6.978744 3.780069,-3.513371 7.046744,-6.559837 7.25928,-6.769926 0.367228,-0.363001 0.433179,-0.666405 1.327443,-6.106826 1.040786,-6.331825 1.066013,-6.412508 2.250084,-7.196324 l 0.596675,-0.39498 7.446948,-0.07159 7.446947,-0.07159 6.097509,-16.895762 c 3.353629,-9.292669 6.184278,-17.104473 6.290327,-17.359565 0.271361,-0.652727 1.177566,-1.423686 1.905252,-1.620901 0.55181,-0.149551 1.267997,-0.05876 6.811404,0.863504 3.407116,0.566845 6.423919,1.093902 6.704006,1.171238 0.637026,0.175891 1.521753,0.963097 1.870312,1.664151 0.253992,0.510853 0.264529,1.240952 0.247407,17.14297 -0.01766,16.399934 -0.02182,16.631061 -0.329684,18.327606 -2.58954,14.26993 -9.981308,25.824745 -22.991493,35.940307 -3.466755,2.695441 -7.896512,5.630746 -12.608706,8.354951 -2.064486,1.193517 -2.838015,1.444605 -3.642726,1.182428 z M 52.47709,87.119888 C 59.271254,83.059324 64.365846,79.189404 68.951193,74.605944 76.565106,66.995162 80.844811,58.899716 82.755326,48.494191 l 0.363231,-1.978318 V 32.007709 17.499544 l -0.322165,-0.07398 c -0.957493,-0.219871 -6.829216,-1.157603 -6.884521,-1.099478 -0.03586,0.03769 -2.785127,7.60719 -6.109478,16.821108 -3.324352,9.213917 -6.177636,16.993316 -6.340633,17.287551 -0.353145,0.637482 -1.164147,1.232386 -1.918855,1.407559 -0.300488,0.06975 -3.5908,0.128346 -7.311805,0.130225 -3.721005,0.0019 -6.765464,0.04073 -6.765464,0.08633 0,0.474978 -1.637454,9.59135 -1.804511,10.046442 -0.187845,0.511725 -1.260378,1.573508 -6.797799,6.729668 -3.61612,3.367143 -6.574871,6.194451 -6.575002,6.282908 -3e-4,0.204934 2.512831,2.628316 4.451717,4.292733 1.895466,1.627144 5.431341,4.277542 7.645781,5.731075 2.084726,1.368391 5.558468,3.470987 5.657133,3.424166 0.03938,-0.01869 1.134737,-0.669367 2.434135,-1.44596 z"
id="path10066" />
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+6 -16
View File
@@ -209,26 +209,13 @@
<div class="bg-image"></div>
<!-- Main container with responsive padding -->
<main class="flex justify-center items-center flex-grow">
<div class="container">
<main class="flex justify-center flex-grow">
<div class="container pt-12">
<div class="container__row">
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
<username-input class="w-full"></username-input>
</div>
<div>
<a
target="_blank"
href="https://discord.gg/openfront"
class="w-full bg-[#5865F2] hover:bg-[#4752C4] text-white p-3 sm:p-4 lg:p-5 font-medium text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300 flex justify-center items-center gap-5"
>
<img
style="height: 50px; width: 50px"
alt="Discord"
src="../../resources/icons/discord.svg"
/>
<span data-i18n="main.join_discord"> Join the Discord! </span>
</a>
</div>
<div></div>
<div>
<public-lobby class="w-full"></public-lobby>
</div>
@@ -331,6 +318,9 @@
>
Wiki
</a>
<a target="_blank" href="https://discord.gg/openfront" class="t-link">
<span data-i18n="main.join_discord"> Join the Discord! </span>
</a>
</div>
<div class="l-footer__col t-text-white">
© 2025
+1 -1
View File
@@ -20,5 +20,5 @@
.l-footer__col {
display: flex;
gap: 10px;
gap: 20px;
}
+1 -9
View File
@@ -4,7 +4,6 @@ import { Executor } from "./execution/ExecutionManager";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import {
AllPlayers,
BuildableUnit,
Game,
GameUpdates,
NameViewData,
@@ -15,7 +14,6 @@ import {
PlayerInfo,
PlayerProfile,
PlayerType,
UnitType,
} from "./game/Game";
import { createGame } from "./game/GameImpl";
import {
@@ -161,13 +159,7 @@ export class GameRunner {
const actions = {
canBoat: player.canBoat(tile),
canAttack: player.canAttack(tile),
buildableUnits: Object.values(UnitType).map((u) => {
return {
type: u,
canBuild: player.canBuild(u, tile) != false,
cost: this.game.config().unitInfo(u).cost(player),
} as BuildableUnit;
}),
buildableUnits: player.buildableUnits(tile),
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
} as PlayerActions;
+3
View File
@@ -52,6 +52,7 @@ export interface NukeMagnitude {
export interface Config {
samHittingChance(): number;
samWarheadHittingChance(): number;
spawnImmunityDuration(): Tick;
serverConfig(): ServerConfig;
gameConfig(): GameConfig;
@@ -118,9 +119,11 @@ export interface Config {
difficultyModifier(difficulty: Difficulty): number;
// 0-1
traitorDefenseDebuff(): number;
traitorDuration(): number;
nukeMagnitudes(unitType: UnitType): NukeMagnitude;
defaultNukeSpeed(): number;
nukeDeathFactor(humans: number, tilesOwned: number): number;
structureMinDist(): number;
}
export interface Theme {
+19 -8
View File
@@ -69,7 +69,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
GameMapType.Europe,
].includes(map)
) {
return Math.random() < 0.2 ? 150 : 70;
return Math.random() < 0.2 ? 100 : 50;
}
// Maps with ~2.5 - ~3.5 mil pixels
if (
@@ -80,7 +80,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
GameMapType.Asia,
].includes(map)
) {
return Math.random() < 0.2 ? 100 : 50;
return Math.random() < 0.3 ? 50 : 25;
}
// Maps with ~2 mil pixels
if (
@@ -92,7 +92,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
GameMapType.FaroeIslands,
].includes(map)
) {
return Math.random() < 0.2 ? 70 : 40;
return Math.random() < 0.3 ? 50 : 25;
}
// Maps smaller than ~2 mil pixels
if (
@@ -102,14 +102,14 @@ export abstract class DefaultServerConfig implements ServerConfig {
GameMapType.Pangaea,
].includes(map)
) {
return Math.random() < 0.2 ? 60 : 35;
return Math.random() < 0.5 ? 30 : 15;
}
// world belongs with the ~2 mils, but these amounts never made sense so I assume the insanity is intended.
if (map == GameMapType.World) {
return Math.random() < 0.2 ? 150 : 60;
return Math.random() < 0.2 ? 150 : 50;
}
// default return for non specified map
return Math.random() < 0.2 ? 85 : 45;
return Math.random() < 0.2 ? 50 : 20;
}
workerIndex(gameID: GameID): number {
return simpleHash(gameID) % this.numWorkers();
@@ -140,8 +140,15 @@ export class DefaultConfig implements Config {
return 0.8;
}
samWarheadHittingChance(): number {
return 0.5;
}
traitorDefenseDebuff(): number {
return 0.8;
return 0.5;
}
traitorDuration(): number {
return 30 * 10; // 30 seconds
}
spawnImmunityDuration(): Tick {
return 5 * 10;
@@ -322,7 +329,7 @@ export class DefaultConfig implements Config {
p.type() == PlayerType.Human && this.infiniteGold()
? 0
: Math.min(
1_500_000 * 3,
3_000_000,
(p.unitsIncludingConstruction(UnitType.SAMLauncher).length +
1) *
1_500_000,
@@ -671,4 +678,8 @@ export class DefaultConfig implements Config {
nukeDeathFactor(humans: number, tilesOwned: number): number {
return (5 * humans) / Math.max(1, tilesOwned);
}
structureMinDist(): number {
return 18;
}
}
+8
View File
@@ -24,6 +24,14 @@ export class DevServerConfig extends DefaultServerConfig {
return Math.random() < 0.5 ? 2 : 3;
}
samWarheadHittingChance(): number {
return 1;
}
samHittingChance(): number {
return 1;
}
discordRedirectURI(): string {
return "http://localhost:3000/auth/callback";
}
+21 -58
View File
@@ -1,13 +1,7 @@
import {
Execution,
Game,
Player,
PlayerType,
TerraNullius,
} from "../game/Game";
import { Execution, Game, Player } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AttackExecution } from "./AttackExecution";
import { BotBehavior } from "./utils/BotBehavior";
export class BotExecution implements Execution {
private active = true;
@@ -16,18 +10,20 @@ export class BotExecution implements Execution {
private mg: Game;
private neighborsTerraNullius = true;
private behavior: BotBehavior | null = null;
constructor(private bot: Player) {
this.random = new PseudoRandom(simpleHash(bot.id()));
this.attackRate = this.random.nextInt(10, 50);
}
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game, ticks: number) {
init(mg: Game) {
this.mg = mg;
this.bot.setTargetTroopRatio(0.7);
// this.neighborsTerra = this.bot.neighbors().filter(n => n == this.gs.terraNullius()).length > 0
}
tick(ticks: number) {
@@ -40,14 +36,15 @@ export class BotExecution implements Execution {
return;
}
this.bot.incomingAllianceRequests().forEach((ar) => {
if (ar.requestor().isTraitor()) {
ar.reject();
} else {
ar.accept();
}
});
if (this.behavior === null) {
this.behavior = new BotBehavior(this.random, this.mg, this.bot, 1 / 20);
}
this.behavior.handleAllianceRequests();
this.maybeAttack();
}
private maybeAttack() {
const traitors = this.bot
.neighbors()
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
@@ -55,56 +52,22 @@ export class BotExecution implements Execution {
const toAttack = this.random.randElement(traitors);
const odds = this.bot.isFriendly(toAttack) ? 6 : 3;
if (this.random.chance(odds)) {
this.sendAttack(toAttack);
this.behavior.sendAttack(toAttack);
return;
}
}
if (this.neighborsTerraNullius) {
for (const b of this.bot.borderTiles()) {
for (const n of this.mg.neighbors(b)) {
if (!this.mg.hasOwner(n) && this.mg.isLand(n)) {
this.sendAttack(this.mg.terraNullius());
return;
}
}
if (this.bot.sharesBorderWith(this.mg.terraNullius())) {
this.behavior.sendAttack(this.mg.terraNullius());
return;
}
this.neighborsTerraNullius = false;
}
const border = Array.from(this.bot.borderTiles())
.flatMap((t) => this.mg.neighbors(t))
.filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot);
if (border.length == 0) {
return;
}
const toAttack = border[this.random.nextInt(0, border.length)];
const owner = this.mg.owner(toAttack);
if (owner.isPlayer()) {
if (this.bot.isFriendly(owner)) {
return;
}
if (owner.type() == PlayerType.FakeHuman) {
if (!this.random.chance(2)) {
return;
}
}
}
this.sendAttack(owner);
}
sendAttack(toAttack: Player | TerraNullius) {
if (toAttack.isPlayer() && this.bot.isOnSameTeam(toAttack)) return;
this.mg.addExecution(
new AttackExecution(
this.bot.troops() / 20,
this.bot.id(),
toAttack.isPlayer() ? toAttack.id() : null,
),
);
const enemy = this.behavior.selectRandomEnemy();
if (!enemy) return;
this.behavior.sendAttack(enemy);
}
owner(): Player {
+124 -121
View File
@@ -1,6 +1,5 @@
import { consolex } from "../Consolex";
import {
AllianceRequest,
Cell,
Difficulty,
Execution,
@@ -11,35 +10,33 @@ import {
PlayerType,
Relation,
TerrainType,
TerraNullius,
Tick,
Unit,
UnitType,
} from "../game/Game";
import { manhattanDistFN, TileRef } from "../game/GameMap";
import { euclDistFN, manhattanDistFN, TileRef } from "../game/GameMap";
import { PseudoRandom } from "../PseudoRandom";
import { GameID } from "../Schemas";
import { calculateBoundingBox, simpleHash } from "../Util";
import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution";
import { AttackExecution } from "./AttackExecution";
import { ConstructionExecution } from "./ConstructionExecution";
import { EmojiExecution } from "./EmojiExecution";
import { NukeExecution } from "./NukeExecution";
import { SpawnExecution } from "./SpawnExecution";
import { TransportShipExecution } from "./TransportShipExecution";
import { closestTwoTiles } from "./Util";
import { BotBehavior } from "./utils/BotBehavior";
export class FakeHumanExecution implements Execution {
private firstMove = true;
private active = true;
private random: PseudoRandom;
private behavior: BotBehavior | null = null;
private mg: Game;
private player: Player = null;
private enemy: Player | null = null;
private lastEnemyUpdateTick: number = 0;
private lastEmojiSent = new Map<Player, Tick>();
private lastNukeSent: [Tick, TileRef][] = [];
private embargoMalusApplied = new Set<PlayerID>();
constructor(
@@ -51,7 +48,7 @@ export class FakeHumanExecution implements Execution {
);
}
init(mg: Game, ticks: number) {
init(mg: Game) {
this.mg = mg;
if (this.random.chance(10)) {
// this.isTraitor = true
@@ -116,16 +113,23 @@ export class FakeHumanExecution implements Execution {
return;
}
}
if (this.firstMove) {
this.firstMove = false;
this.sendAttack(this.mg.terraNullius());
return;
}
if (!this.player.isAlive()) {
this.active = false;
return;
}
if (this.behavior === null) {
// Player is unavailable during init()
this.behavior = new BotBehavior(this.random, this.mg, this.player, 1 / 5);
}
if (this.firstMove) {
this.firstMove = false;
this.behavior.sendAttack(this.mg.terraNullius());
return;
}
if (ticks % this.random.nextInt(40, 80) != 0) {
return;
}
@@ -138,7 +142,7 @@ export class FakeHumanExecution implements Execution {
}
this.updateRelationsFromEmbargos();
this.handleAllianceRequests();
this.behavior.handleAllianceRequests();
this.handleEnemies();
this.handleUnits();
this.handleEmbargoesToHostileNations();
@@ -164,7 +168,7 @@ export class FakeHumanExecution implements Execution {
this.mg.playerBySmallID(this.mg.ownerID(t)),
);
if (enemiesWithTN.filter((o) => !o.isPlayer()).length > 0) {
this.sendAttack(this.mg.terraNullius());
this.behavior.sendAttack(this.mg.terraNullius());
return;
}
@@ -186,7 +190,7 @@ export class FakeHumanExecution implements Execution {
? enemies[0]
: this.random.randElement(enemies);
if (this.shouldAttack(toAttack)) {
this.sendAttack(toAttack);
this.behavior.sendAttack(toAttack);
}
}
@@ -223,97 +227,137 @@ export class FakeHumanExecution implements Execution {
}
handleEnemies() {
if (this.mg.ticks() - this.lastEnemyUpdateTick > 100) {
this.enemy = null;
}
const target =
this.player
.allies()
.filter((ally) => this.player.relation(ally) == Relation.Friendly)
.filter((ally) => ally.targets().length > 0)
.map((ally) => ({ ally: ally, t: ally.targets()[0] }))[0] ?? null;
if (
target != null &&
target.t != this.player &&
!this.player.isAlliedWith(target.t)
) {
this.player.updateRelation(target.ally, -20);
this.enemy = target.t;
this.lastEnemyUpdateTick = this.mg.ticks();
if (target.ally.type() == PlayerType.Human) {
this.mg.addExecution(
new EmojiExecution(this.player.id(), target.ally.id(), "👍"),
);
}
}
if (this.enemy == null) {
const mostHated = this.player.allRelationsSorted()[0] ?? null;
if (mostHated != null && mostHated.relation == Relation.Hostile) {
this.enemy = mostHated.player;
this.lastEnemyUpdateTick = this.mg.ticks();
}
}
if (this.enemy) {
if (this.player.isFriendly(this.enemy)) {
this.enemy = null;
return;
}
this.maybeSendEmoji();
this.maybeSendNuke(this.enemy);
if (this.player.sharesBorderWith(this.enemy)) {
this.sendAttack(this.enemy);
} else {
this.maybeSendBoatAttack(this.enemy);
}
return;
this.behavior.assistAllies();
const enemy = this.behavior.selectEnemy();
if (!enemy) return;
this.maybeSendEmoji(enemy);
this.maybeSendNuke(enemy);
if (this.player.sharesBorderWith(enemy)) {
this.behavior.sendAttack(enemy);
} else {
this.maybeSendBoatAttack(enemy);
}
}
private maybeSendEmoji() {
if (this.enemy.type() != PlayerType.Human) return;
const lastSent = this.lastEmojiSent.get(this.enemy) ?? -300;
private maybeSendEmoji(enemy: Player) {
if (enemy.type() != PlayerType.Human) return;
const lastSent = this.lastEmojiSent.get(enemy) ?? -300;
if (this.mg.ticks() - lastSent <= 300) return;
this.lastEmojiSent.set(this.enemy, this.mg.ticks());
this.lastEmojiSent.set(enemy, this.mg.ticks());
this.mg.addExecution(
new EmojiExecution(
this.player.id(),
this.enemy.id(),
enemy.id(),
this.random.randElement(["🤡", "😡"]),
),
);
}
private maybeSendNuke(other: Player) {
const silos = this.player.units(UnitType.MissileSilo);
if (
this.player.units(UnitType.MissileSilo).length == 0 ||
silos.length == 0 ||
this.player.gold() <
this.mg.config().unitInfo(UnitType.AtomBomb).cost(this.player) ||
other.type() == PlayerType.Bot ||
this.player.isOnSameTeam(other)
) {
return;
}
outer: for (let i = 0; i < 10; i++) {
const tile = this.randTerritoryTile(other);
if (tile == null) {
return;
}
const structures = other.units(
UnitType.City,
UnitType.DefensePost,
UnitType.MissileSilo,
UnitType.Port,
UnitType.SAMLauncher,
);
const structureTiles = structures.map((u) => u.tile());
const randomTiles: TileRef[] = new Array(10);
for (let i = 0; i < randomTiles.length; i++) {
randomTiles[i] = this.randTerritoryTile(other);
}
const allTiles = randomTiles.concat(structureTiles);
let bestTile = null;
let bestValue = 0;
this.removeOldNukeEvents();
outer: for (const tile of new Set(allTiles)) {
if (tile == null) continue;
for (const t of this.mg.bfs(tile, manhattanDistFN(tile, 15))) {
// Make sure we nuke at least 15 tiles in border
if (this.mg.owner(t) != other) {
continue outer;
}
}
if (this.player.canBuild(UnitType.AtomBomb, tile)) {
this.mg.addExecution(
new NukeExecution(UnitType.AtomBomb, this.player.id(), tile),
);
return;
if (!this.player.canBuild(UnitType.AtomBomb, tile)) continue;
const value = this.nukeTileScore(tile, silos, structures);
if (value > bestTile) {
bestTile = tile;
bestValue = value;
}
}
if (bestTile != null) {
this.sendNuke(bestTile);
}
}
private removeOldNukeEvents() {
const maxAge = 500;
const tick = this.mg.ticks();
while (
this.lastNukeSent.length > 0 &&
this.lastNukeSent[0][0] + maxAge < tick
) {
this.lastNukeSent.shift();
}
}
private sendNuke(tile: TileRef) {
const tick = this.mg.ticks();
this.lastNukeSent.push([tick, tile]);
this.mg.addExecution(
new NukeExecution(UnitType.AtomBomb, this.player.id(), tile),
);
}
private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number {
// Potential damage in a 25-tile radius
const dist = euclDistFN(tile, 25, false);
let tileValue = targets
.filter((unit) => dist(this.mg, unit.tile()))
.map((unit) => {
switch (unit.type()) {
case UnitType.City:
return 25_000;
case UnitType.DefensePost:
return 5_000;
case UnitType.MissileSilo:
return 50_000;
case UnitType.Port:
return 10_000;
case UnitType.SAMLauncher:
return 5_000;
default:
return 0;
}
})
.reduce((prev, cur) => prev + cur, 0);
// Prefer tiles that are closer to a silo
const siloTiles = silos.map((u) => u.tile());
const { x: closestSilo } = closestTwoTiles(this.mg, siloTiles, [tile]);
const distanceSquared = this.mg.euclideanDistSquared(tile, closestSilo);
const distanceToClosestSilo = Math.sqrt(distanceSquared);
tileValue -= distanceToClosestSilo * 30;
// Don't target near recent targets
tileValue -= this.lastNukeSent
.filter(([_tick, tile]) => dist(this.mg, tile))
.map((_) => 1_000_000)
.reduce((prev, cur) => prev + cur, 0);
return tileValue;
}
private maybeSendBoatAttack(other: Player) {
@@ -473,36 +517,6 @@ export class FakeHumanExecution implements Execution {
return this.mg.unitInfo(type).cost(this.player);
}
handleAllianceRequests() {
for (const req of this.player.incomingAllianceRequests()) {
if (req.requestor().isTraitor()) {
this.replyToAllianceRequest(req, false);
continue;
}
if (this.player.relation(req.requestor()) < Relation.Neutral) {
this.replyToAllianceRequest(req, false);
continue;
}
const requestorIsMuchLarger =
req.requestor().numTilesOwned() > this.player.numTilesOwned() * 3;
if (!requestorIsMuchLarger && req.requestor().alliances().length >= 3) {
this.replyToAllianceRequest(req, false);
continue;
}
this.replyToAllianceRequest(req, true);
}
}
private replyToAllianceRequest(req: AllianceRequest, accept: boolean): void {
this.mg.addExecution(
new AllianceRequestReplyExecution(
req.requestor().id(),
this.player.id(),
accept,
),
);
}
sendBoatRandomly() {
const oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
@@ -554,17 +568,6 @@ export class FakeHumanExecution implements Execution {
return null;
}
sendAttack(toAttack: Player | TerraNullius) {
if (toAttack.isPlayer() && this.player.isOnSameTeam(toAttack)) return;
this.mg.addExecution(
new AttackExecution(
this.player.troops() / 5,
this.player.id(),
toAttack.isPlayer() ? toAttack.id() : null,
),
);
}
private randOceanShoreTile(tile: TileRef, dist: number): TileRef | null {
const x = this.mg.x(tile);
const y = this.mg.y(tile);
-1
View File
@@ -170,7 +170,6 @@ export class MirvExecution implements Execution {
if (!this.mg.isValidCoord(x, y)) {
continue;
}
console.log(`got coord ${x}, ${y}`);
const tile = this.mg.ref(x, y);
if (!this.mg.isLand(tile)) {
continue;
+3 -2
View File
@@ -33,14 +33,15 @@ export class MissileSiloExecution implements Execution {
tick(ticks: number): void {
if (this.silo == null) {
if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) {
const spawn = this.player.canBuild(UnitType.MissileSilo, this.tile);
if (spawn === false) {
consolex.warn(
`player ${this.player} cannot build missile silo at ${this.tile}`,
);
this.active = false;
return;
}
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile, {
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, spawn, {
cooldownDuration: this.mg.config().SiloCooldown(),
});
+106 -44
View File
@@ -15,19 +15,28 @@ import { SAMMissileExecution } from "./SAMMissileExecution";
export class SAMLauncherExecution implements Execution {
private player: Player;
private mg: Game;
private sam: Unit;
private active: boolean = true;
private target: Unit = null;
private warheadTargets: Unit[] = [];
private searchRangeRadius = 75;
private searchRangeRadius = 80;
// As MIRV go very fast we have to detect them very early but we only
// shoot the one targeting very close (MIRVWarheadProtectionRadius)
private MIRVWarheadSearchRadius = 400;
private MIRVWarheadProtectionRadius = 50;
private pseudoRandom: PseudoRandom;
constructor(
private ownerId: PlayerID,
private tile: TileRef,
) {}
private sam: Unit | null = null,
) {
if (sam != null) {
this.tile = sam.tile();
}
}
init(mg: Game, ticks: number): void {
this.mg = mg;
@@ -39,6 +48,52 @@ export class SAMLauncherExecution implements Execution {
this.player = mg.player(this.ownerId);
}
private getSingleTarget(): Unit | null {
const nukes = this.mg
.nearbyUnits(this.sam.tile(), this.searchRangeRadius, [
UnitType.AtomBomb,
UnitType.HydrogenBomb,
])
.filter(
({ unit }) =>
unit.owner() !== this.player && !this.player.isFriendly(unit.owner()),
);
return (
nukes.sort((a, b) => {
const { unit: unitA, distSquared: distA } = a;
const { unit: unitB, distSquared: distB } = b;
// Prioritize Hydrogen Bombs
if (
unitA.type() === UnitType.HydrogenBomb &&
unitB.type() !== UnitType.HydrogenBomb
)
return -1;
if (
unitA.type() !== UnitType.HydrogenBomb &&
unitB.type() === UnitType.HydrogenBomb
)
return 1;
// If both are the same type, sort by distance (lower `distSquared` means closer)
return distA - distB;
})[0]?.unit ?? null
);
}
private isHit(type: UnitType, random: number): boolean {
if (type == UnitType.AtomBomb) {
return true;
}
if (type == UnitType.MIRVWarhead) {
return random < this.mg.config().samWarheadHittingChance();
}
return random < this.mg.config().samHittingChance();
}
tick(ticks: number): void {
if (this.sam == null) {
const spawnTile = this.player.canBuild(UnitType.SAMLauncher, this.tile);
@@ -64,36 +119,26 @@ export class SAMLauncherExecution implements Execution {
this.pseudoRandom = new PseudoRandom(this.sam.id());
}
const nukes = this.mg
.nearbyUnits(this.sam.tile(), this.searchRangeRadius, [
UnitType.AtomBomb,
UnitType.HydrogenBomb,
])
this.warheadTargets = this.mg
.nearbyUnits(
this.sam.tile(),
this.MIRVWarheadSearchRadius,
UnitType.MIRVWarhead,
)
.map(({ unit }) => unit)
.filter(
({ unit }) =>
(unit) =>
unit.owner() !== this.player && !this.player.isFriendly(unit.owner()),
)
.filter(
(unit) =>
this.mg.manhattanDist(unit.detonationDst(), this.sam.tile()) <
this.MIRVWarheadProtectionRadius,
);
this.target =
nukes.sort((a, b) => {
const { unit: unitA, distSquared: distA } = a;
const { unit: unitB, distSquared: distB } = b;
// Prioritize Hydrogen Bombs
if (
unitA.type() === UnitType.HydrogenBomb &&
unitB.type() !== UnitType.HydrogenBomb
)
return -1;
if (
unitA.type() !== UnitType.HydrogenBomb &&
unitB.type() === UnitType.HydrogenBomb
)
return 1;
// If both are the same type, sort by distance (lower `distSquared` means closer)
return distA - distB;
})[0]?.unit ?? null;
if (this.warheadTargets.length == 0) {
this.target = this.getSingleTarget();
}
if (
this.sam.isCooldown() &&
@@ -102,29 +147,46 @@ export class SAMLauncherExecution implements Execution {
this.sam.setCooldown(false);
}
if (this.target && !this.sam.isCooldown() && !this.target.targetedBySAM()) {
const isSingleTarget = this.target && !this.target.targetedBySAM();
if (
(isSingleTarget || this.warheadTargets.length > 0) &&
!this.sam.isCooldown()
) {
this.sam.setCooldown(true);
const type =
this.warheadTargets.length > 0
? UnitType.MIRVWarhead
: this.target.type();
const random = this.pseudoRandom.next();
let hit = true;
if (this.target.type() != UnitType.AtomBomb) {
hit = random < this.mg.config().samHittingChance();
}
const hit = this.isHit(type, random);
if (!hit) {
this.mg.displayMessage(
`Missile failed to intercept ${this.target.type()}`,
`Missile failed to intercept ${type}`,
MessageType.ERROR,
this.sam.owner().id(),
);
} else {
this.target.setTargetedBySAM(true);
this.mg.addExecution(
new SAMMissileExecution(
this.sam.tile(),
this.sam.owner(),
this.sam,
this.target,
),
);
if (this.warheadTargets.length > 0) {
// Message
this.mg.displayMessage(
`${this.warheadTargets.length} MIRV warheads intercepted`,
MessageType.SUCCESS,
this.sam.owner().id(),
);
// Delete warheads
this.warheadTargets.forEach((u) => u.delete());
} else {
this.target.setTargetedBySAM(true);
this.mg.addExecution(
new SAMMissileExecution(
this.sam.tile(),
this.sam.owner(),
this.sam,
this.target,
),
);
this.warheadTargets = [];
}
}
}
}
+140
View File
@@ -0,0 +1,140 @@
import {
AllianceRequest,
Game,
Player,
PlayerType,
Relation,
TerraNullius,
Tick,
} from "../../game/Game";
import { PseudoRandom } from "../../PseudoRandom";
import { AttackExecution } from "../AttackExecution";
import { EmojiExecution } from "../EmojiExecution";
export class BotBehavior {
private enemy: Player | null = null;
private enemyUpdated: Tick;
constructor(
private random: PseudoRandom,
private game: Game,
private player: Player,
private attackRatio: number,
) {}
handleAllianceRequests() {
for (const req of this.player.incomingAllianceRequests()) {
if (shouldAcceptAllianceRequest(this.player, req)) {
req.accept();
} else {
req.reject();
}
}
}
private emoji(player: Player, emoji: string) {
if (player.type() !== PlayerType.Human) return;
this.game.addExecution(
new EmojiExecution(this.player.id(), player.id(), emoji),
);
}
assistAllies() {
outer: for (const ally of this.player.allies()) {
if (ally.targets().length === 0) continue;
if (this.player.relation(ally) < Relation.Friendly) {
// this.emoji(ally, "🤦");
continue;
}
for (const target of ally.targets()) {
if (target === this.player) {
// this.emoji(ally, "💀");
continue;
}
if (this.player.isAlliedWith(target)) {
// this.emoji(ally, "👎");
continue;
}
// All checks passed, assist them
this.player.updateRelation(ally, -20);
this.enemy = target;
this.enemyUpdated = this.game.ticks();
this.emoji(ally, "👍");
break outer;
}
}
}
selectEnemy(): Player | null {
// Forget old enemies
if (this.game.ticks() - this.enemyUpdated > 100) {
this.enemy = null;
}
// Prefer neighboring bots
if (this.enemy === null) {
const bots = this.player
.neighbors()
.filter((n) => n.isPlayer() && n.type() === PlayerType.Bot) as Player[];
if (bots.length > 0) {
const density = (p: Player) => p.troops() / p.numTilesOwned();
this.enemy = bots.sort((a, b) => density(a) - density(b))[0];
this.enemyUpdated = this.game.ticks();
}
}
// Select the most hated player
if (this.enemy === null) {
const mostHated = this.player.allRelationsSorted()[0] ?? null;
if (mostHated != null && mostHated.relation === Relation.Hostile) {
this.enemy = mostHated.player;
this.enemyUpdated = this.game.ticks();
}
}
// Sanity check, don't attack our allies or teammates
if (this.enemy && this.player.isFriendly(this.enemy)) {
this.enemy = null;
}
return this.enemy;
}
selectRandomEnemy(): Player | TerraNullius | null {
const neighbors = this.player.neighbors();
for (const neighbor of this.random.shuffleArray(neighbors)) {
if (neighbor.isPlayer()) {
if (this.player.isFriendly(neighbor)) continue;
if (neighbor.type() == PlayerType.FakeHuman) {
if (this.random.chance(2)) {
continue;
}
}
}
return neighbor;
}
return null;
}
sendAttack(target: Player | TerraNullius) {
if (target.isPlayer() && this.player.isOnSameTeam(target)) return;
const troops = this.player.troops() * this.attackRatio;
if (troops < 1) return;
this.game.addExecution(
new AttackExecution(
troops,
this.player.id(),
target.isPlayer() ? target.id() : null,
),
);
}
}
function shouldAcceptAllianceRequest(player: Player, request: AllianceRequest) {
const notTraitor = !request.requestor().isTraitor();
const noMalice = player.relation(request.requestor()) >= Relation.Neutral;
const requestorIsMuchLarger =
request.requestor().numTilesOwned() > player.numTilesOwned() * 3;
const notTooManyAlliances =
requestorIsMuchLarger || request.requestor().alliances().length < 3;
return notTraitor && noMalice && notTooManyAlliances;
}
+2
View File
@@ -315,6 +315,7 @@ export interface Player {
// State & Properties
isAlive(): boolean;
isTraitor(): boolean;
markTraitor(): void;
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
lastTileChange(): Tick;
@@ -348,6 +349,7 @@ export interface Player {
// Units
units(...types: UnitType[]): Unit[];
unitsIncludingConstruction(type: UnitType): Unit[];
buildableUnits(tile: TileRef): BuildableUnit[];
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
buildUnit(
type: UnitType,
+1 -1
View File
@@ -518,7 +518,7 @@ export class GameImpl implements Game {
);
}
if (!other.isTraitor()) {
(breaker as PlayerImpl).isTraitor_ = true;
breaker.markTraitor();
}
const breakerSet = new Set(breaker.alliances());
+84 -13
View File
@@ -20,6 +20,7 @@ import {
AllianceRequest,
AllPlayers,
Attack,
BuildableUnit,
Cell,
EmojiMessage,
GameMode,
@@ -70,7 +71,7 @@ export class PlayerImpl implements Player {
private _defensivePosture: "retreat" | "balanced" | "hold" = "balanced";
isTraitor_ = false;
markedTraitorTick = -1;
private embargoes: Set<PlayerID> = new Set();
@@ -246,7 +247,7 @@ export class PlayerImpl implements Player {
const ns: Set<Player | TerraNullius> = new Set();
for (const border of this.borderTiles()) {
for (const neighbor of this.mg.map().neighbors(border)) {
if (this.mg.map().isLake(neighbor)) {
if (this.mg.map().isLand(neighbor)) {
const owner = this.mg.map().ownerID(neighbor);
if (owner != this.smallID()) {
ns.add(
@@ -375,7 +376,14 @@ export class PlayerImpl implements Player {
}
isTraitor(): boolean {
return this.isTraitor_;
return (
this.markedTraitorTick >= 0 &&
this.mg.ticks() - this.markedTraitorTick <
this.mg.config().traitorDuration()
);
}
markTraitor(): void {
this.markedTraitorTick = this.mg.ticks();
}
createAllianceRequest(recipient: Player): AllianceRequest {
@@ -730,7 +738,22 @@ export class PlayerImpl implements Player {
return b;
}
canBuild(unitType: UnitType, targetTile: TileRef): TileRef | false {
public buildableUnits(tile: TileRef): BuildableUnit[] {
const validTiles = this.validStructureSpawnTiles(tile);
return Object.values(UnitType).map((u) => {
return {
type: u,
canBuild: this.canBuild(u, tile, validTiles) != false,
cost: this.mg.config().unitInfo(u).cost(this),
} as BuildableUnit;
});
}
canBuild(
unitType: UnitType,
targetTile: TileRef,
validTiles: TileRef[] | null = null,
): TileRef | false {
// prevent the building of nukes and nuke related buildings
if (this.mg.config().disableNukes()) {
if (
@@ -762,7 +785,7 @@ export class PlayerImpl implements Player {
case UnitType.MIRVWarhead:
return targetTile;
case UnitType.Port:
return this.portSpawn(targetTile);
return this.portSpawn(targetTile, validTiles);
case UnitType.Warship:
return this.warshipSpawn(targetTile);
case UnitType.Shell:
@@ -777,7 +800,7 @@ export class PlayerImpl implements Player {
case UnitType.SAMLauncher:
case UnitType.City:
case UnitType.Construction:
return this.landBasedStructureSpawn(targetTile);
return this.landBasedStructureSpawn(targetTile, validTiles);
default:
assertNever(unitType);
}
@@ -803,7 +826,7 @@ export class PlayerImpl implements Player {
return spawns[0].tile();
}
portSpawn(tile: TileRef): TileRef | false {
portSpawn(tile: TileRef, validTiles: TileRef[]): TileRef | false {
const spawns = Array.from(
this.mg.bfs(
tile,
@@ -815,10 +838,15 @@ export class PlayerImpl implements Player {
(a, b) =>
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
);
if (spawns.length == 0) {
return false;
const validTileSet = new Set(
validTiles ?? this.validStructureSpawnTiles(tile),
);
for (const t of spawns) {
if (validTileSet.has(t)) {
return t;
}
}
return spawns[0];
return false;
}
warshipSpawn(tile: TileRef): TileRef | false {
@@ -836,11 +864,54 @@ export class PlayerImpl implements Player {
return spawns[0].tile();
}
landBasedStructureSpawn(tile: TileRef): TileRef | false {
if (this.mg.owner(tile) != this) {
landBasedStructureSpawn(
tile: TileRef,
validTiles: TileRef[] | null = null,
): TileRef | false {
const tiles = validTiles ?? this.validStructureSpawnTiles(tile);
if (tiles.length == 0) {
return false;
}
return tile;
return tiles[0];
}
private validStructureSpawnTiles(tile: TileRef): TileRef[] {
if (this.mg.owner(tile) != this) {
return [];
}
const searchRadius = 15;
const searchRadiusSquared = searchRadius ** 2;
const types = Object.values(UnitType).filter((unitTypeValue) => {
return this.mg.config().unitInfo(unitTypeValue).territoryBound;
});
const nearbyUnits = this.mg
.nearbyUnits(tile, searchRadius * 2, types)
.map((u) => u.unit);
const nearbyTiles = this.mg.bfs(tile, (gm, t) => {
return (
this.mg.euclideanDistSquared(tile, t) < searchRadiusSquared &&
gm.ownerID(t) == this.smallID()
);
});
const validSet: Set<TileRef> = new Set(nearbyTiles);
const minDistSquared = this.mg.config().structureMinDist() ** 2;
for (const t of nearbyTiles) {
for (const unit of nearbyUnits) {
if (this.mg.euclideanDistSquared(unit.tile(), t) < minDistSquared) {
validSet.delete(t);
break;
}
}
}
const valid = Array.from(validSet);
valid.sort(
(a, b) =>
this.mg.euclideanDistSquared(a, tile) -
this.mg.euclideanDistSquared(b, tile),
);
return valid;
}
transportShipSpawn(targetTile: TileRef): TileRef | false {
+1 -1
View File
@@ -141,7 +141,7 @@ export class UnitImpl implements Unit {
this._active = false;
this.mg.addUpdate(this.toUpdate());
this.mg.removeUnit(this);
if (displayMessage) {
if (displayMessage && this.type() != UnitType.MIRVWarhead) {
this.mg.displayMessage(
`Your ${this.type()} was destroyed`,
MessageType.ERROR,
+8 -8
View File
@@ -81,25 +81,25 @@ export class MapPlaylist {
// Big Maps are those larger than ~2.5 mil pixels
case PlaylistType.BigMaps:
return {
Europe: 3,
NorthAmerica: 2,
Europe: 2,
NorthAmerica: 1,
Africa: 2,
Britannia: 1,
GatewayToTheAtlantic: 2,
Australia: 1,
Iceland: 1,
SouthAmerica: 3,
Australia: 2,
Iceland: 2,
SouthAmerica: 1,
KnownWorld: 2,
};
case PlaylistType.SmallMaps:
return {
World: 1,
World: 4,
Mena: 2,
Pangaea: 1,
Asia: 1,
Mars: 1,
BetweenTwoSeas: 3,
Japan: 3,
BetweenTwoSeas: 2,
Japan: 2,
BlackSea: 1,
FaroeIslands: 2,
};
+31 -56
View File
@@ -1,4 +1,4 @@
import { NukeExecution } from "../src/core/execution/NukeExecution";
import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution";
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
import {
Game,
@@ -7,32 +7,13 @@ import {
PlayerType,
UnitType,
} from "../src/core/game/Game";
import { TileRef } from "../src/core/game/GameMap";
import { setup } from "./util/Setup";
import { constructionExecution } from "./util/utils";
import { constructionExecution, executeTicks } from "./util/utils";
let game: Game;
let attacker: Player;
let defender: Player;
function attackerBuildsNuke(
source: TileRef,
target: TileRef,
initialize = true,
) {
game.addExecution(
new NukeExecution(UnitType.AtomBomb, attacker.id(), target, source),
);
if (initialize) {
game.executeNextTick();
game.executeNextTick();
}
}
function defenderBuildsSam(x: number, y: number) {
constructionExecution(game, defender.id(), x, y, UnitType.SAMLauncher);
}
describe("SAM", () => {
beforeEach(async () => {
game = await setup("Plains", { infiniteGold: true, instantBuild: true });
@@ -69,62 +50,56 @@ describe("SAM", () => {
});
test("one sam should take down one nuke", async () => {
defenderBuildsSam(1, 1);
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 1));
executeTicks(game, 3);
game.executeNextTick();
game.executeNextTick();
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
});
test("sam should only get one nuke at a time", async () => {
defenderBuildsSam(1, 1);
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1), false);
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 1));
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2));
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2);
game.executeNextTick();
game.executeNextTick();
executeTicks(game, 3);
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
});
test("sam should cooldown as long as configured", async () => {
defenderBuildsSam(1, 1);
expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeFalsy();
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
expect(sam.isCooldown()).toBeFalsy();
const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2));
game.executeNextTick();
game.executeNextTick();
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
executeTicks(game, 3);
expect(nuke.isActive()).toBeFalsy();
for (let i = 0; i < game.config().SAMCooldown() - 2; i++) {
game.executeNextTick();
expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeTruthy();
expect(sam.isCooldown()).toBeTruthy();
}
game.executeNextTick();
expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeFalsy();
executeTicks(game, 2);
expect(sam.isCooldown()).toBeFalsy();
});
test("two sams should not target twice same nuke", async () => {
defenderBuildsSam(1, 1);
defenderBuildsSam(1, 2);
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
const sam1 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1));
const sam2 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 2));
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2));
const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 2));
expect(defender.units(UnitType.SAMLauncher)).toHaveLength(2);
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
executeTicks(game, 3);
game.executeNextTick();
game.executeNextTick();
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
const sams = defender.units(UnitType.SAMLauncher);
// Only one sam must have shot
expect(
(sams[0].isCooldown() && !sams[1].isCooldown()) ||
(sams[1].isCooldown() && !sams[0].isCooldown()),
).toBe(true);
expect(nuke.isActive()).toBeFalsy();
expect([sam1, sam2].filter((s) => s.isCooldown())).toHaveLength(1);
});
});
+6
View File
@@ -28,3 +28,9 @@ export function constructionExecution(
game.executeNextTick();
}
}
export function executeTicks(game: Game, numTicks: number): void {
for (let i = 0; i < numTicks; i++) {
game.executeNextTick();
}
}