Compare commits
310 Commits
@ -0,0 +1 @@ |
||||
logging.yaml |
@ -1,4 +1,5 @@ |
||||
[Debug] |
||||
|
||||
TEST_VASL_MODS = /test-data/vasl-vmods/ |
||||
TEST_VASL_EXTNS_DIR = /test-data/vasl-extensions/ |
||||
; NOTE: These need to be mapped in if you want to run the test suite against a container. |
||||
TEST_VASSAL_ENGINES = /test-data/vassal/ |
||||
TEST_VASL_MODS = /test-data/vasl-mods/ |
||||
|
@ -1,31 +0,0 @@ |
||||
version: 1 |
||||
|
||||
formatters: |
||||
standard: |
||||
format: "%(asctime)s.%(msecs)03d | %(message)s" |
||||
datefmt: "%H:%M:%S" |
||||
|
||||
handlers: |
||||
console: |
||||
class: "logging.StreamHandler" |
||||
formatter: "standard" |
||||
stream: "ext://sys.stdout" |
||||
file: |
||||
class: "logging.FileHandler" |
||||
formatter: "standard" |
||||
filename: "/tmp/vasl-templates.log" |
||||
mode: "w" |
||||
|
||||
loggers: |
||||
werkzeug: |
||||
level: "WARNING" |
||||
handlers: [ "console" ] |
||||
vasl_mod: |
||||
level: "WARNING" |
||||
handlers: [ "console", "file" ] |
||||
update_vsav: |
||||
level: "WARNING" |
||||
handlers: [ "console", "file" ] |
||||
control_tests: |
||||
level: "DEBUG" |
||||
handlers: [ "console", "file" ] |
@ -1,6 +1,5 @@ |
||||
[Site Config] |
||||
|
||||
FLASK_HOST = 0.0.0.0 |
||||
IS_CONTAINER = 1 |
||||
|
||||
WEBDRIVER_PATH = /usr/bin/geckodriver |
||||
|
@ -1,8 +1,15 @@ |
||||
#!/bin/sh |
||||
|
||||
# set up the display (so we can run a webdriver) |
||||
# set up the display (so we can run VASSAL and a webdriver) |
||||
export ENV=10 |
||||
export DISPLAY=:10.0 |
||||
Xvfb :10 -ac 1>/tmp/xvfb.log 2>/tmp/xvfb.err & |
||||
|
||||
# run the webapp server |
||||
python /app/vasl_templates/webapp/run_server.py |
||||
# IMPORTANT! This script runs as PID 1, which is the only process that will receive signals, |
||||
# so we must replace it with the Python webserver process if it is to receive e.g. SIGTERM, |
||||
# which we must handle if "docker stop" and scaling down in Kubernetes is to work (otherwise |
||||
# things timeout and we get SIGKILL'ed). |
||||
exec python3 /app/vasl_templates/webapp/run_server.py \ |
||||
--addr 0.0.0.0 \ |
||||
--force-init-delay 30 |
||||
|
@ -1 +1,189 @@ |
||||
{"SCENARIO_NAME":"Hill 621","SCENARIO_ID":"ASL E","ROAR_ID":129,"SCENARIO_LOCATION":"Near Minsk, Russia","SCENARIO_DATE":"1944-06-29","SCENARIO_WIDTH":"","VICTORY_CONDITIONS_WIDTH":"240px","SSR_WIDTH":"300px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"The Russians win at Game End if they Control ≥ five Level 3 hill hexes on Board 2.","PLAYER_1":"russian","PLAYER_1_ELR":"4","PLAYER_1_SAN":"3","PLAYER_2":"german","PLAYER_2_ELR":"3","PLAYER_2_SAN":"4","SSR":["EC are Moderate, with no wind at start.","After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.","The Germans receive one module of 80+mm Battalion Mortar OBA (HE and Smoke) with the radio in the initial OB.","The Germans receive one module of 100+mm OBA (HE and Smoke) with the Turn 4 reinforcements."],"OB_VEHICLES_1":[{"name":"T-34 M43"},{"name":"SU-152"},{"name":"SU-122"},{"name":"ZIS-5"}],"OB_VEHICLES_2":[{"name":"PzKpfw IVH"},{"name":"PzKpfw IIIN"},{"name":"StuG IIIG (L)"},{"name":"StuH 42"},{"name":"SPW 250/1"},{"name":"SPW 251/1"},{"name":"SPW 251/sMG"}],"OB_ORDNANCE_2":[{"name":"7.5cm PaK 40"},{"name":"5cm PaK 38"}],"SCENARIO_NOTES":[{"caption":"Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).","width":"300px"}],"OB_SETUPS_1":[{"caption":"Set up on any whole hex of Board 3","width":""},{"caption":"Enter on Turn 2 on any single road hex <br>\non the east edge of Board 3","width":""},{"caption":"Enter on Turn 5 on any single road hex <br>\non the east edge of Board 3","width":""}],"OB_SETUPS_2":[{"caption":"Set up in any whole hex of Board 4","width":""},{"caption":"Enter on Turn 1 on any single road hex <br>\non any edge of Board 2","width":""},{"caption":"Enter on Turn 2 on any single road hex <br>\non the north <i>or</i> south edge of Board 4","width":""},{"caption":"Enter on Turn 4 on any single road hex <br>\non the west edge of Board 2","width":""},{"caption":"Enter on Turn 5 on any single road hex <br>\nalong the north, south or west edge of Board 2","width":""},{"caption":"Enter on Turn 8 along <br>\nthe west edge of Board 2","width":""}],"OB_NOTES_1":[],"OB_NOTES_2":[{"caption":"80+mm Battalion Mortar <br> OBA (HE/Smoke)","width":""},{"caption":"100+mm OBA (HE/Smoke)","width":""}]} |
||||
{ |
||||
"COMPASS": "right", |
||||
"SCENARIO_DATE": "1944-07-01", |
||||
"SCENARIO_WIDTH": "", |
||||
"ASA_ID": "56512", |
||||
"ROAR_ID": "129", |
||||
"PLAYERS_WIDTH": "", |
||||
"VICTORY_CONDITIONS_WIDTH": "", |
||||
"SSR_WIDTH": "400px", |
||||
"OB_VEHICLES_WIDTH_1": "", |
||||
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", |
||||
"OB_ORDNANCE_WIDTH_1": "", |
||||
"OB_ORDNANCE_MA_NOTES_WIDTH_1": "300px", |
||||
"OB_VEHICLES_WIDTH_2": "", |
||||
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", |
||||
"OB_ORDNANCE_WIDTH_2": "", |
||||
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", |
||||
"SCENARIO_NAME": "Hill 621", |
||||
"SCENARIO_ID": "ASL E", |
||||
"SCENARIO_LOCATION": "Near Minsk, Russia", |
||||
"PLAYER_1_DESCRIPTION": "Retreating elements of 170th Infantry Division", |
||||
"PLAYER_2_DESCRIPTION": "Elements of 5th Guards Army", |
||||
"VICTORY_CONDITIONS": "The Russians win at Game End if they Control<br>≥ five Level 3 hill hexes on Board 2.", |
||||
"SCENARIO_THEATER": "ETO", |
||||
"PLAYER_1": "russian", |
||||
"PLAYER_1_ELR": "4", |
||||
"PLAYER_1_SAN": "3", |
||||
"PLAYER_2": "german", |
||||
"PLAYER_2_ELR": "3", |
||||
"PLAYER_2_SAN": "4", |
||||
"TURN_TRACK": { |
||||
"NTURNS": "10", |
||||
"WIDTH": "", |
||||
"VERTICAL": false, |
||||
"SHADING": "", |
||||
"REINFORCEMENTS_1": "2,5", |
||||
"REINFORCEMENTS_2": "1,2,4,5,8", |
||||
"SWAP_PLAYERS": false |
||||
}, |
||||
"SSR": [ |
||||
"EC are Moderate, with no wind at start.", |
||||
"After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.", |
||||
"The Germans receive one module of 80+mm Battalion Mortar OBA (HE and Smoke) with the radio in the initial OB.", |
||||
"The Germans receive one module of 100+mm OBA (HE and Smoke) with the Turn 4 reinforcements." |
||||
], |
||||
"OB_VEHICLES_1": [ |
||||
{ |
||||
"id": "ru/v:025", |
||||
"seq_id": 1, |
||||
"name": "T-34 M43" |
||||
}, |
||||
{ |
||||
"id": "ru/v:047", |
||||
"seq_id": 2, |
||||
"name": "SU-152" |
||||
}, |
||||
{ |
||||
"id": "ru/v:046", |
||||
"seq_id": 3, |
||||
"name": "SU-122" |
||||
}, |
||||
{ |
||||
"id": "ru/v:068", |
||||
"seq_id": 4, |
||||
"name": "ZIS-5" |
||||
} |
||||
], |
||||
"OB_VEHICLES_2": [ |
||||
{ |
||||
"id": "ge/v:027", |
||||
"seq_id": 1, |
||||
"name": "PzKpfw IVH" |
||||
}, |
||||
{ |
||||
"id": "ge/v:019", |
||||
"seq_id": 2, |
||||
"name": "PzKpfw IIIN" |
||||
}, |
||||
{ |
||||
"id": "ge/v:038", |
||||
"seq_id": 3, |
||||
"name": "StuG IIIG (L)" |
||||
}, |
||||
{ |
||||
"id": "ge/v:039", |
||||
"seq_id": 4, |
||||
"name": "StuH 42" |
||||
}, |
||||
{ |
||||
"id": "ge/v:065", |
||||
"seq_id": 5, |
||||
"name": "SPW 250/1" |
||||
}, |
||||
{ |
||||
"id": "ge/v:071", |
||||
"seq_id": 6, |
||||
"name": "SPW 251/1" |
||||
}, |
||||
{ |
||||
"id": "ge/v:072", |
||||
"seq_id": 7, |
||||
"name": "SPW 251/sMG" |
||||
} |
||||
], |
||||
"OB_ORDNANCE_2": [ |
||||
{ |
||||
"id": "ge/o:009", |
||||
"seq_id": 1, |
||||
"name": "7.5cm PaK 40" |
||||
}, |
||||
{ |
||||
"id": "ge/o:007", |
||||
"seq_id": 2, |
||||
"name": "5cm PaK 38" |
||||
} |
||||
], |
||||
"SCENARIO_NOTES": [ |
||||
{ |
||||
"caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).", |
||||
"width": "300px", |
||||
"id": 1 |
||||
} |
||||
], |
||||
"OB_SETUPS_1": [ |
||||
{ |
||||
"caption": "Set up on any whole hex of Board 3", |
||||
"width": "", |
||||
"id": 1 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 2 on any single road hex <br>\non the east edge of Board 3", |
||||
"width": "", |
||||
"id": 2 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 5 on any single road hex <br>\non the east edge of Board 3", |
||||
"width": "", |
||||
"id": 3 |
||||
} |
||||
], |
||||
"OB_SETUPS_2": [ |
||||
{ |
||||
"caption": "Set up in any whole hex of Board 4", |
||||
"width": "", |
||||
"id": 1 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 1 on any single road hex <br>\non any edge of Board 2", |
||||
"width": "", |
||||
"id": 2 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 2 on any single road hex <br>\non the north <i>or</i> south edge of Board 4", |
||||
"width": "", |
||||
"id": 3 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 4 on any single road hex <br>\non the west edge of Board 2", |
||||
"width": "", |
||||
"id": 4 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 5 on any single road hex <br>\nalong the north, south or west edge of Board 2", |
||||
"width": "", |
||||
"id": 5 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 8 along <br>\nthe west edge of Board 2", |
||||
"width": "", |
||||
"id": 6 |
||||
} |
||||
], |
||||
"OB_NOTES_1": [], |
||||
"OB_NOTES_2": [ |
||||
{ |
||||
"caption": "80+mm Battalion Mortar <br> OBA (HE/Smoke)", |
||||
"width": "", |
||||
"id": 1 |
||||
}, |
||||
{ |
||||
"caption": "100+mm OBA (HE/Smoke)", |
||||
"width": "", |
||||
"id": 2 |
||||
} |
||||
], |
||||
"_app_version": "v1.10", |
||||
"_last_update_time": "2022-09-12T02:46:18.035Z", |
||||
"_creation_time": "2020-09-27T03:46:56.089Z" |
||||
} |
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.8 MiB |
After Width: | Height: | Size: 109 KiB |
@ -1 +1,126 @@ |
||||
{"SCENARIO_NAME":"Hube's Pocket","SCENARIO_ID":"ASL G","ROAR_ID":131,"SCENARIO_LOCATION":"Near Buchach, Southern Russia","SCENARIO_DATE":"1944-04-05","SCENARIO_WIDTH":"","VICTORY_CONDITIONS_WIDTH":"300px","SSR_WIDTH":"330px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"The Germans win immediately by exiting ≥ 10 vehicles <br>\noff the west edge in either one or two Convoys (see SSR 4).","PLAYER_1":"german","PLAYER_1_ELR":"4","PLAYER_1_SAN":"2","PLAYER_2":"russian","PLAYER_2_ELR":"3","PLAYER_2_SAN":"2","SSR":["The SPW 251/sMG inherent HS is a 3-4-8.","German inherent crews have a morale of 9.","No German unit may enter any hex of Board 4 prior to Turn 2.","All units of the 1st Panzer Army must enter in Convoy (E11.) on/after Turn 5 (some, none, or all may enter each Turn) along any single road hex along the east edge."],"OB_VEHICLES_1":[{"name":"PzKpfw IVH"},{"name":"PzKpfw VG"},{"name":"SPW 251/sMG"},{"name":"SPW 251/1"},{"name":"Buessing-NAG 4500"},{"name":"Opel 6700 (Blitz)"},{"name":"SdKfz 7"}],"OB_VEHICLES_2":[{"name":"T-34/85"},{"name":"T-34 M43"}],"SCENARIO_NOTES":[{"caption":"Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).","width":""}],"OB_SETUPS_1":[{"caption":"Enter on Turn 1 along the west edge of Boards 2/5 (see SSR 3)","width":""},{"caption":"Enter per SSR 4","width":"200px"}],"OB_SETUPS_2":[{"caption":"Enter on Turn 1 along the north edge","width":""}],"OB_NOTES_1":[],"OB_NOTES_2":[]} |
||||
{ |
||||
"COMPASS": "down", |
||||
"SCENARIO_DATE": "1944-04-06", |
||||
"SCENARIO_WIDTH": "", |
||||
"ASA_ID": "56514", |
||||
"ROAR_ID": "131", |
||||
"PLAYERS_WIDTH": "", |
||||
"VICTORY_CONDITIONS_WIDTH": "", |
||||
"SSR_WIDTH": "330px", |
||||
"OB_VEHICLES_WIDTH_1": "", |
||||
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", |
||||
"OB_ORDNANCE_WIDTH_1": "", |
||||
"OB_ORDNANCE_MA_NOTES_WIDTH_1": "300px", |
||||
"OB_VEHICLES_WIDTH_2": "", |
||||
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", |
||||
"OB_ORDNANCE_WIDTH_2": "", |
||||
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", |
||||
"SCENARIO_NAME": "Hube's Pocket", |
||||
"SCENARIO_ID": "ASL G", |
||||
"SCENARIO_LOCATION": "Near Buchach, Southern Russia", |
||||
"PLAYER_1_DESCRIPTION": "Advance elements of 5th Tank Corps", |
||||
"PLAYER_2_DESCRIPTION": "10th SS Panzer Division \"Frundsberg\" and the First Panzer Army", |
||||
"VICTORY_CONDITIONS": "The Germans win immediately by exiting ≥ 10 vehicles <br>\noff the west edge in either one or two Convoys (see SSR 4).", |
||||
"SCENARIO_THEATER": "ETO", |
||||
"PLAYER_1": "german", |
||||
"PLAYER_1_ELR": "4", |
||||
"PLAYER_1_SAN": "2", |
||||
"PLAYER_2": "russian", |
||||
"PLAYER_2_ELR": "3", |
||||
"PLAYER_2_SAN": "2", |
||||
"TURN_TRACK": { |
||||
"NTURNS": "14", |
||||
"WIDTH": "5", |
||||
"VERTICAL": false, |
||||
"SHADING": "", |
||||
"REINFORCEMENTS_1": "1,5", |
||||
"REINFORCEMENTS_2": "1", |
||||
"SWAP_PLAYERS": true |
||||
}, |
||||
"SSR": [ |
||||
"The SPW 251/sMG inherent HS is a 3-4-8.", |
||||
"German inherent crews have a morale of 9.", |
||||
"No German unit may enter any hex of Board 4 prior to Turn 2.", |
||||
"All units of the 1st Panzer Army must enter in Convoy (E11.) on/after Turn 5 (some, none, or all may enter each Turn) along any single road hex along the east edge." |
||||
], |
||||
"OB_VEHICLES_1": [ |
||||
{ |
||||
"id": "ge/v:027", |
||||
"seq_id": 1, |
||||
"name": "PzKpfw IVH" |
||||
}, |
||||
{ |
||||
"id": "ge/v:030", |
||||
"seq_id": 2, |
||||
"name": "PzKpfw VG" |
||||
}, |
||||
{ |
||||
"id": "ge/v:072", |
||||
"seq_id": 3, |
||||
"name": "SPW 251/sMG" |
||||
}, |
||||
{ |
||||
"id": "ge/v:071", |
||||
"seq_id": 4, |
||||
"name": "SPW 251/1" |
||||
}, |
||||
{ |
||||
"id": "ge/v:116", |
||||
"seq_id": 5, |
||||
"name": "Buessing-NAG 4500" |
||||
}, |
||||
{ |
||||
"id": "ge/v:115", |
||||
"seq_id": 6, |
||||
"name": "Opel 6700 (Blitz)" |
||||
}, |
||||
{ |
||||
"id": "ge/v:118", |
||||
"seq_id": 7, |
||||
"name": "SdKfz 7" |
||||
} |
||||
], |
||||
"OB_VEHICLES_2": [ |
||||
{ |
||||
"id": "ru/v:027", |
||||
"seq_id": 1, |
||||
"name": "T-34/85" |
||||
}, |
||||
{ |
||||
"id": "ru/v:025", |
||||
"seq_id": 2, |
||||
"name": "T-34 M43" |
||||
} |
||||
], |
||||
"SCENARIO_NOTES": [ |
||||
{ |
||||
"caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).", |
||||
"width": "", |
||||
"id": 1 |
||||
} |
||||
], |
||||
"OB_SETUPS_1": [ |
||||
{ |
||||
"caption": "Enter on Turn 1 along the west edge of Boards 2/5 (see SSR 3)", |
||||
"width": "", |
||||
"id": 1 |
||||
}, |
||||
{ |
||||
"caption": "Enter per SSR 4", |
||||
"width": "200px", |
||||
"id": 2 |
||||
} |
||||
], |
||||
"OB_SETUPS_2": [ |
||||
{ |
||||
"caption": "Enter on Turn 1 along the north edge", |
||||
"width": "", |
||||
"id": 1 |
||||
} |
||||
], |
||||
"OB_NOTES_1": [], |
||||
"OB_NOTES_2": [], |
||||
"_app_version": "v1.10", |
||||
"_last_update_time": "2022-09-12T02:22:47.511Z", |
||||
"_creation_time": "2020-09-27T04:11:07.200Z" |
||||
} |
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 79 KiB |
@ -1 +1,173 @@ |
||||
{"SCENARIO_NAME":"The Streets Of Stalingrad","SCENARIO_ID":"ASL C","ROAR_ID":127,"SCENARIO_LOCATION":"Stalingrad, Russia","SCENARIO_DATE":"1942-10-04","SCENARIO_WIDTH":"","VICTORY_CONDITIONS_WIDTH":"400px","SSR_WIDTH":"500px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"Victory is based upon satisfying the Victory Conditions of Scenarios A and B:\n<ul style=\"margin:0 0 10px 10px;\">\n<li> If each side fulfills one Victory Condition, the game is a draw.\n<li> If a player fulfills one Victory Condition and draws the other, he wnis.\n<li> A decisive victory is achieved when a player fulfills both Victory Conditions.\n</ul>\n\n<p> <b>Scenario A:</b> The Russians win at Game End if they Control ≥ 2 more buildings initially occupied by the Germans than they lose of their own initially-held stone buildings to German Control, and/or have a favorable 3:1 ratio of unbroken squad-equivalents.\n<p> <b>Scenario B:</b> At Game End, the player with undisputed control of at least 6 hexes of building X3 wins. A hex containing a Melee is controlled by neither player. If only one player has an unbroken unit in the building at Game End, that player is the winner. Any other result is a draw.\n</ul>","PLAYER_1":"russian","PLAYER_1_ELR":"3","PLAYER_1_SAN":"6","PLAYER_2":"german","PLAYER_2_ELR":"4","PLAYER_2_SAN":"6","SSR":["Roll a die to determine who moves first.","Set up the forces of Scenario A prior to placing the units of Scenario B.","Each non-prisoner Russian unit is Fanatic (A10.8) while in building X3.","Building X3 is a Factory.","German armor may delay entry one Game Turn and thereafter enter on any southern or eastern mapboard hex.","Prior to play, both players may agree that if the game is a draw by the standard victory conditions, then the Russian loses unless he has a favorable 3:1 ratio of unbroken squads at the end of play."],"OB_VEHICLES_1":[{"name":"T-34 M43"},{"name":"T-34 M41"}],"OB_VEHICLES_2":[{"name":"StuG IIIG"},{"name":"StuG IIIB"}],"SCENARIO_NOTES":[{"caption":"Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).","width":""}],"OB_SETUPS_1":[{"caption":"Set up in building N4","width":""},{"caption":"Set up in building J2","width":""},{"caption":"Set up in building M2","width":""},{"caption":"Set up in building N2","width":""},{"caption":"Set up in building F3","width":"180px"},{"caption":"Set up first in building X3","width":"190px"},{"caption":"Set up last in buildings P8, P5, Q4 and R1","width":""},{"caption":"Enter on Turn 2 on I1","width":""}],"OB_SETUPS_2":[{"caption":"Set up in building F5","width":""},{"caption":"Set up in building K5","width":""},{"caption":"Set up in building I7","width":""},{"caption":"Set up in building M7","width":"170px"},{"caption":"Set up in building M9","width":""},{"caption":"Set up in buildings AA4, CC3 and/or Y8","width":""},{"caption":"Set up in buildings U3, T4, R7 and/or T7","width":""},{"caption":"Set up in buildings Y8, CC7 and/or AA4","width":""},{"caption":"Enter on Turn 3 on Y10 <br>\nand/or GG5-GG6","width":""}],"OB_NOTES_1":[],"OB_NOTES_2":[]} |
||||
{ |
||||
"COMPASS": "up", |
||||
"SCENARIO_DATE": "1942-10-06", |
||||
"SCENARIO_WIDTH": "", |
||||
"ASA_ID": "56510", |
||||
"ROAR_ID": "127", |
||||
"PLAYERS_WIDTH": "", |
||||
"VICTORY_CONDITIONS_WIDTH": "450px", |
||||
"SSR_WIDTH": "500px", |
||||
"OB_VEHICLES_WIDTH_1": "", |
||||
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", |
||||
"OB_ORDNANCE_WIDTH_1": "", |
||||
"OB_ORDNANCE_MA_NOTES_WIDTH_1": "300px", |
||||
"OB_VEHICLES_WIDTH_2": "", |
||||
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", |
||||
"OB_ORDNANCE_WIDTH_2": "", |
||||
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", |
||||
"SCENARIO_NAME": "The Streets Of Stalingrad", |
||||
"SCENARIO_ID": "ASL C", |
||||
"SCENARIO_LOCATION": "Stalingrad, Russia", |
||||
"PLAYER_1_DESCRIPTION": "308th Rifle Division / 295th Rifle Division / 2nd Battalion, 37th Guards Division", |
||||
"PLAYER_2_DESCRIPTION": "389th Infantry Division", |
||||
"VICTORY_CONDITIONS": "Victory is based upon satisfying the Victory Conditions of Scenarios A and B:\n<ul style=\"margin:0 0 10px 10px;\">\n<li> If each side fulfills one Victory Condition, the game is a draw.\n</li><li> If a player fulfills one Victory Condition and draws the other, he wnis.\n</li><li> A decisive victory is achieved when a player fulfills both Victory Conditions.\n</li></ul>\n\n<p> <b>Scenario A:</b> The Russians win at Game End if they Control ≥ 2 more buildings initially occupied by the Germans than they lose of their own initially-held stone buildings to German Control, and/or have a favorable 3:1 ratio of unbroken squad-equivalents.\n</p><p> <b>Scenario B:</b> At Game End, the player with undisputed control of at least 6 hexes of building X3 wins. A hex containing a Melee is controlled by neither player. If only one player has an unbroken unit in the building at Game End, that player is the winner. Any other result is a draw.\n</p>", |
||||
"SCENARIO_THEATER": "ETO", |
||||
"PLAYER_1": "russian", |
||||
"PLAYER_1_ELR": "3", |
||||
"PLAYER_1_SAN": "6", |
||||
"PLAYER_2": "german", |
||||
"PLAYER_2_ELR": "4", |
||||
"PLAYER_2_SAN": "6", |
||||
"TURN_TRACK": { |
||||
"NTURNS": "7", |
||||
"WIDTH": "", |
||||
"VERTICAL": false, |
||||
"SHADING": "", |
||||
"REINFORCEMENTS_1": "2", |
||||
"REINFORCEMENTS_2": "3", |
||||
"SWAP_PLAYERS": false |
||||
}, |
||||
"SSR": [ |
||||
"Roll a die to determine who moves first.", |
||||
"Set up the forces of Scenario A prior to placing the units of Scenario B.", |
||||
"Each non-prisoner Russian unit is Fanatic (A10.8) while in building X3.", |
||||
"Building X3 is a Factory.", |
||||
"German armor may delay entry one Game Turn and thereafter enter on any southern or eastern mapboard hex.", |
||||
"Prior to play, both players may agree that if the game is a draw by the standard victory conditions, then the Russian loses unless he has a favorable 3:1 ratio of unbroken squads at the end of play." |
||||
], |
||||
"OB_VEHICLES_1": [ |
||||
{ |
||||
"id": "ru/v:025", |
||||
"seq_id": 1, |
||||
"name": "T-34 M43" |
||||
}, |
||||
{ |
||||
"id": "ru/v:023", |
||||
"seq_id": 2, |
||||
"name": "T-34 M41" |
||||
} |
||||
], |
||||
"OB_VEHICLES_2": [ |
||||
{ |
||||
"id": "ge/v:037", |
||||
"seq_id": 1, |
||||
"name": "StuG IIIG" |
||||
}, |
||||
{ |
||||
"id": "ge/v:036", |
||||
"seq_id": 2, |
||||
"name": "StuG IIIB" |
||||
} |
||||
], |
||||
"SCENARIO_NOTES": [ |
||||
{ |
||||
"caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).", |
||||
"width": "", |
||||
"id": 1 |
||||
} |
||||
], |
||||
"OB_SETUPS_1": [ |
||||
{ |
||||
"caption": "Set up in building N4", |
||||
"width": "", |
||||
"id": 1 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building J2", |
||||
"width": "", |
||||
"id": 2 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building M2", |
||||
"width": "", |
||||
"id": 3 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building N2", |
||||
"width": "", |
||||
"id": 4 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building F3", |
||||
"width": "180px", |
||||
"id": 5 |
||||
}, |
||||
{ |
||||
"caption": "Set up first in building X3", |
||||
"width": "190px", |
||||
"id": 6 |
||||
}, |
||||
{ |
||||
"caption": "Set up last in buildings P8, P5, Q4 and R1", |
||||
"width": "", |
||||
"id": 7 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 2 on I1", |
||||
"width": "", |
||||
"id": 8 |
||||
} |
||||
], |
||||
"OB_SETUPS_2": [ |
||||
{ |
||||
"caption": "Set up in building F5", |
||||
"width": "", |
||||
"id": 1 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building K5", |
||||
"width": "", |
||||
"id": 2 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building I7", |
||||
"width": "", |
||||
"id": 3 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building M7", |
||||
"width": "170px", |
||||
"id": 4 |
||||
}, |
||||
{ |
||||
"caption": "Set up in building M9", |
||||
"width": "", |
||||
"id": 5 |
||||
}, |
||||
{ |
||||
"caption": "Set up in buildings AA4, CC3 and/or Y8", |
||||
"width": "", |
||||
"id": 6 |
||||
}, |
||||
{ |
||||
"caption": "Set up in buildings U3, T4, R7 and/or T7", |
||||
"width": "", |
||||
"id": 7 |
||||
}, |
||||
{ |
||||
"caption": "Set up in buildings Y8, CC7 and/or AA4", |
||||
"width": "", |
||||
"id": 8 |
||||
}, |
||||
{ |
||||
"caption": "Enter on Turn 3 on Y10 <br>\nand/or GG5-GG6", |
||||
"width": "", |
||||
"id": 9 |
||||
} |
||||
], |
||||
"OB_NOTES_1": [], |
||||
"OB_NOTES_2": [], |
||||
"_app_version": "v1.10", |
||||
"_last_update_time": "2022-09-12T02:58:13.237Z", |
||||
"_creation_time": "2020-09-27T04:44:48.473Z" |
||||
} |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 902 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 3.4 KiB |
@ -0,0 +1,107 @@ |
||||
#!/usr/bin/env python3 |
||||
""" Freeze the vasl-templates loader program. |
||||
|
||||
This script is called by the main freeze script. |
||||
""" |
||||
|
||||
import sys |
||||
import os |
||||
import shutil |
||||
import tempfile |
||||
import getopt |
||||
|
||||
from PyInstaller.__main__ import run as run_pyinstaller |
||||
from PIL import Image |
||||
|
||||
APP_ICON = os.path.join( |
||||
os.path.abspath( os.path.dirname( __file__ ) ), |
||||
"../vasl_templates/webapp/static/images/app.ico" |
||||
) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def main( args ): |
||||
"""Main processing.""" |
||||
|
||||
# parse the command-line options |
||||
output_fname = "./loader" |
||||
work_dir = os.path.join( tempfile.gettempdir(), "freeze-loader" ) |
||||
cleanup = True |
||||
opts,args = getopt.getopt( args, "o:w:", ["output=","work=","no-clean"] ) |
||||
for opt, val in opts: |
||||
if opt in ["-o","--output"]: |
||||
output_fname = val.strip() |
||||
elif opt in ["-w","--work"]: |
||||
work_dir = val.strip() |
||||
elif opt in ["--no-clean"]: |
||||
cleanup = False |
||||
else: |
||||
raise RuntimeError( "Unknown argument: {}".format( opt ) ) |
||||
|
||||
# freeze the loader program |
||||
freeze_loader( output_fname, work_dir, cleanup ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def freeze_loader( output_fname, work_dir, cleanup ): |
||||
"""Freeze the loader program.""" |
||||
|
||||
with tempfile.TemporaryDirectory() as dist_dir: |
||||
|
||||
# initialize |
||||
base_dir = os.path.abspath( os.path.dirname( __file__ ) ) |
||||
assets_dir = os.path.join( base_dir, "assets" ) |
||||
|
||||
# convert the app icon to an image |
||||
if not os.path.isdir( work_dir ): |
||||
os.makedirs( work_dir ) |
||||
app_icon_fname = os.path.join( work_dir, "app-icon.png" ) |
||||
_convert_app_icon( app_icon_fname ) |
||||
|
||||
# initialize |
||||
app_name = "loader" |
||||
args = [ |
||||
"--distpath", dist_dir, |
||||
"--workpath", work_dir, |
||||
"--specpath", work_dir, |
||||
"--onefile", |
||||
"--name", app_name, |
||||
] |
||||
args.extend( [ |
||||
"--add-data", app_icon_fname + os.pathsep + "assets/", |
||||
"--add-data", os.path.join(assets_dir,"loading.gif") + os.pathsep + "assets/" |
||||
] ) |
||||
if sys.platform == "win32": |
||||
args.append( "--noconsole" ) |
||||
args.extend( [ "--icon", APP_ICON ] ) |
||||
args.append( os.path.join( base_dir, "main.py" ) ) |
||||
|
||||
# freeze the program |
||||
run_pyinstaller( args ) |
||||
|
||||
# save the generated artifact |
||||
fname = app_name+".exe" if sys.platform == "win32" else app_name |
||||
shutil.move( |
||||
os.path.join( dist_dir, fname ), |
||||
output_fname |
||||
) |
||||
|
||||
# clean up |
||||
if cleanup: |
||||
shutil.rmtree( work_dir ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def _convert_app_icon( save_fname ): |
||||
"""Convert the app icon to an image.""" |
||||
# NOTE: Tkinter's PhotoImage doesn't handle .ico files, so we convert the app icon |
||||
# to an image, then insert it into the PyInstaller-generated executable (so that |
||||
# we don't have to bundle Pillow into the release). |
||||
img = Image.open( APP_ICON ) |
||||
img = img.convert( "RGBA" ).resize( (48, 48) ) |
||||
img.save( save_fname, "png" ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
if __name__ == "__main__": |
||||
main( sys.argv[1:] ) |
@ -0,0 +1,207 @@ |
||||
""" Load the main vasl-templates program. |
||||
|
||||
vasl-templates can be slow to start (especially on Windows), since it has to unpack the PyInstaller-generated EXE, |
||||
then startup Qt. We want to show a splash screen while all this happening, but we can't just put it in vasl-templates, |
||||
since it would only happen *after* all the slow stuff has finished :-/ So, we have this stub program that shows |
||||
a splash screen, launches the main vasl-templates program, and waits for it to finish starting up. |
||||
""" |
||||
|
||||
import sys |
||||
import os |
||||
import subprocess |
||||
import threading |
||||
import itertools |
||||
import urllib.request |
||||
from urllib.error import URLError |
||||
import time |
||||
import configparser |
||||
|
||||
# NOTE: It's important that this program start up quickly (otherwise it becomes pointless), |
||||
# so we use tkinter, instead of PyQt (and also avoid bundling a 2nd copy of PyQt :-/). |
||||
import tkinter |
||||
import tkinter.messagebox |
||||
|
||||
if getattr( sys, "frozen", False ): |
||||
BASE_DIR = sys._MEIPASS #pylint: disable=no-member,protected-access |
||||
else: |
||||
BASE_DIR = os.path.abspath( os.path.dirname( __file__ ) ) |
||||
|
||||
STARTUP_TIMEOUT = 60 # how to long to wait for vasl-templates to start (seconds) |
||||
|
||||
main_window = None |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def main( args ): |
||||
"""Load the main vasl-templates program.""" |
||||
|
||||
# initialize Tkinter |
||||
global main_window |
||||
main_window = tkinter.Tk() |
||||
main_window.option_add( "*Dialog.msg.font", "Helvetica 12" ) |
||||
|
||||
# load the app icon |
||||
# NOTE: This image file doesn't exist in source control, but is created dynamically from |
||||
# the main app icon by the freeze script, and inserted into the PyInstaller-generated executable. |
||||
# We do things this way so that we don't have to bundle Pillow into the release. |
||||
app_icon = tkinter.PhotoImage( |
||||
file = make_asset_path( "app-icon.png" ) |
||||
) |
||||
|
||||
# locate the main vasl-templates executable |
||||
fname = os.path.join( os.path.dirname( sys.executable ), "vasl-templates-main" ) |
||||
if sys.platform == "win32": |
||||
fname += ".exe" |
||||
if not os.path.isfile( fname ): |
||||
show_error_msg( "Can't find the main vasl-templates program.", withdraw=True ) |
||||
return -1 |
||||
|
||||
# launch the main vasl-templates program |
||||
try: |
||||
proc = subprocess.Popen( itertools.chain( [fname], args ) ) #pylint: disable=consider-using-with |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
show_error_msg( "Can't start vasl-templates:\n\n{}".format( ex ), withdraw=True ) |
||||
return -2 |
||||
|
||||
# get the webapp port number |
||||
port = 5010 |
||||
fname = os.path.join( os.path.dirname( fname ), "config/app.cfg" ) |
||||
if os.path.isfile( fname ): |
||||
config_parser = configparser.ConfigParser() |
||||
config_parser.optionxform = str # preserve case for the keys :-/ |
||||
config_parser.read( fname ) |
||||
args = dict( config_parser.items( "System" ) ) |
||||
port = args.get( "FLASK_PORT_NO", port ) |
||||
|
||||
# create the splash window |
||||
create_window( app_icon ) |
||||
|
||||
# start a background thread to check on the main vasl-templates process |
||||
threading.Thread( |
||||
target = check_startup, |
||||
args = ( proc, port ) |
||||
).start() |
||||
|
||||
# run the main loop |
||||
main_window.mainloop() |
||||
|
||||
return 0 |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def create_window( app_icon ): |
||||
"""Create the splash window.""" |
||||
|
||||
# create the splash window |
||||
main_window.geometry( "275x64" ) |
||||
main_window.title( "vasl-templates loader" ) |
||||
main_window.overrideredirect( 1 ) # nb: "-type splash" doesn't work on Windows :-/ |
||||
main_window.eval( "tk::PlaceWindow . center" ) |
||||
main_window.wm_attributes( "-topmost", 1 ) |
||||
main_window.tk.call( "wm", "iconphoto", main_window._w, app_icon ) #pylint: disable=protected-access |
||||
main_window.protocol( "WM_DELETE_WINDOW", lambda: None ) |
||||
|
||||
# add the app icon |
||||
label = tkinter.Label( main_window, image=app_icon ) |
||||
label.grid( row=0, column=0, rowspan=2, padx=8, pady=8 ) |
||||
|
||||
# add the caption |
||||
label = tkinter.Label( main_window, text="Loading vasl-templates...", font=("Helvetica",12) ) |
||||
label.grid( row=0, column=1, padx=5, pady=(8,0) ) |
||||
|
||||
# add the "loading" image (we have to animate it ourself :-/) |
||||
anim_label = tkinter.Label( main_window ) |
||||
anim_label.grid( row=1, column=1, sticky=tkinter.N, padx=0, pady=0 ) |
||||
fname = make_asset_path( "loading.gif" ) |
||||
nframes = 13 |
||||
frames = [ |
||||
tkinter.PhotoImage( file=fname, format="gif -index {}".format( i ) ) |
||||
for i in range(nframes) |
||||
] |
||||
frame_index = 0 |
||||
def next_frame(): |
||||
nonlocal frame_index |
||||
frame = frames[ frame_index ] |
||||
frame_index = ( frame_index + 1 ) % nframes |
||||
anim_label.configure( image=frame ) |
||||
main_window.after( 75, next_frame ) |
||||
main_window.after( 0, next_frame ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def check_startup( proc, port ): |
||||
"""Check the startup of the main vasl-templates process.""" |
||||
|
||||
def do_check(): |
||||
|
||||
# check if we've waited for too long |
||||
if time.time() - start_time > STARTUP_TIMEOUT: |
||||
# yup - give up |
||||
raise RuntimeError( "Couldn't start vasl-templates." ) |
||||
|
||||
# check if the main vasl-templates process has gone away |
||||
if proc.poll() is not None: |
||||
raise RuntimeError( "The vasl-templates program ended unexpectedly." ) |
||||
|
||||
# check if the webapp is responding |
||||
url = "http://localhost:{}/ping".format( port ) |
||||
try: |
||||
with urllib.request.urlopen( url ) as resp: |
||||
_ = resp.read() |
||||
except URLError: |
||||
# no response - the webapp is probably still starting up |
||||
return False |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
raise RuntimeError( "Couldn't communicate with vasl-templates:\n\n{}".format( ex ) ) from ex |
||||
|
||||
# the main vasl-templates program has started up and is responsive - our job is done! |
||||
if sys.platform == "win32": |
||||
# FUDGE! There is a short amount of time between the webapp server starting and |
||||
# the main window appearing. We delay here for a bit, to try to synchronize |
||||
# our window fading out with the main vasl-templates window appearing. |
||||
time.sleep( 1 ) |
||||
return True |
||||
|
||||
def on_done( msg ): |
||||
if msg: |
||||
show_error_msg( msg, withdraw=True ) |
||||
fade_out( main_window, main_window.quit ) |
||||
|
||||
# run the main loop |
||||
start_time = time.time() |
||||
while True: |
||||
try: |
||||
if do_check(): |
||||
on_done( None ) |
||||
break |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
on_done( str(ex) ) |
||||
return |
||||
time.sleep( 0.25 ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def fade_out( target, on_done ): |
||||
"""Fade out the target window.""" |
||||
alpha = target.attributes( "-alpha" ) |
||||
if alpha > 0: |
||||
alpha -= 0.1 |
||||
target.attributes( "-alpha", alpha ) |
||||
target.after( 50, lambda: fade_out( target, on_done ) ) |
||||
else: |
||||
on_done() |
||||
|
||||
def make_asset_path( fname ): |
||||
"""Generate the path to an asset file.""" |
||||
return os.path.join( BASE_DIR, "assets", fname ) |
||||
|
||||
def show_error_msg( error_msg, withdraw=False ): |
||||
"""Show an error dialog.""" |
||||
if withdraw: |
||||
main_window.withdraw() |
||||
tkinter.messagebox.showinfo( "vasl-templates loader error", error_msg ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
if __name__ == "__main__": |
||||
sys.exit( main( sys.argv[1:] ) ) |
@ -1,6 +1,7 @@ |
||||
pytest==3.6.0 |
||||
tabulate==0.8.2 |
||||
lxml==4.2.4 |
||||
pylint==1.9.2 |
||||
pytest-pylint==0.9.0 |
||||
pyinstaller==3.6 |
||||
pytest==7.4.2 |
||||
grpcio-tools==1.58.0 |
||||
tabulate==0.9.0 |
||||
lxml==4.9.3 |
||||
pylint==2.17.5 |
||||
pytest-pylint==0.19.0 |
||||
pyinstaller==5.13.2 |
||||
|
@ -1,7 +1,10 @@ |
||||
# python 3.6.8 |
||||
# python 3.11.4 |
||||
|
||||
flask==1.0.2 |
||||
pyyaml==5.3.1 |
||||
pillow==7.0.0 |
||||
selenium==3.12.0 |
||||
click==6.7 |
||||
flask==2.3.3 |
||||
pyyaml==6.0.1 |
||||
# NOTE: Pillow 9.5.0 is the last version that provides 32-bit wheels. |
||||
pillow==9.5.0 |
||||
selenium==4.12.0 |
||||
waitress==2.1.2 |
||||
appdirs==1.4.4 |
||||
click==8.1.7 |
||||
|
@ -0,0 +1,64 @@ |
||||
#!/usr/bin/env python3 |
||||
""" Check how scenarios at the ASL Scenario Archive are connected to those at ROAR. """ |
||||
|
||||
import sys |
||||
import json |
||||
|
||||
from vasl_templates.webapp.scenarios import _match_roar_scenario, \ |
||||
_asa_scenarios, _build_asa_scenario_index, _roar_scenarios, _build_roar_scenario_index |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def asa_string( s ): |
||||
"""Return an ASL Scenario Archive scenario as a string.""" |
||||
return "[{}] {} ({})".format( |
||||
s["scenario_id"], s.get("title"), s.get("sc_id") |
||||
) |
||||
|
||||
def roar_string( s ): |
||||
"""Return ROAR scenario as a string.""" |
||||
return "[{}] {} ({})".format( |
||||
s["roar_id"], s.get("name"), s.get("scenario_id") |
||||
) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
# load the ASL Scenario Archive scenarios |
||||
fname = sys.argv[1] |
||||
with open( fname, "r", encoding="utf-8" ) as fp: |
||||
asa_data = json.load( fp ) |
||||
_build_asa_scenario_index( _asa_scenarios, asa_data, None ) |
||||
|
||||
# load the ROAR scenarios |
||||
fname = sys.argv[2] |
||||
with open( fname, "r", encoding="utf-8" ) as fp: |
||||
roar_data = json.load( fp ) |
||||
_build_roar_scenario_index( _roar_scenarios, roar_data, None ) |
||||
|
||||
# try to connect each ASA scenario to ROAR |
||||
exact_matches, multiple_matches, unmatched = [], [], [] |
||||
for scenario in asa_data["scenarios"]: |
||||
matches = _match_roar_scenario( scenario ) |
||||
if not matches: |
||||
unmatched.append( scenario ) |
||||
elif len(matches) == 1: |
||||
exact_matches.append( scenario ) |
||||
else: |
||||
multiple_matches.append( [ scenario, matches ] ) |
||||
|
||||
# output the results |
||||
print( "ASL Scenario Archive scenarios: {}".format( len(asa_data["scenarios"]) ) ) |
||||
print() |
||||
print( "Exact matches: {}".format( len(exact_matches) ) ) |
||||
print() |
||||
print( "Multiple matches: {}".format( len(multiple_matches) ) ) |
||||
if multiple_matches: |
||||
for scenario,matches in multiple_matches: |
||||
print( " {}:".format( asa_string(scenario) ) ) |
||||
for match in matches: |
||||
print( " - {}".format( roar_string( match ) ) ) |
||||
print() |
||||
print( "Unmatched: {}".format( len(unmatched) ) ) |
||||
if unmatched: |
||||
for scenario in unmatched: |
||||
print( " {}".format( asa_string(scenario) ) ) |
@ -0,0 +1,268 @@ |
||||
#!/usr/bin/env python3 |
||||
"""Dump the log file analysis reports generated by the VASSAL shim.""" |
||||
|
||||
import os |
||||
import itertools |
||||
|
||||
import click |
||||
import tabulate |
||||
|
||||
from vasl_templates.webapp.lfa import parse_analysis_report, DEFAULT_LFA_DICE_HOTNESS_WEIGHTS |
||||
|
||||
EXPECTED_DISTRIB = { |
||||
"DR": { 2: 2.8, 3: 5.6, 4: 8.3, 5: 11.1, 6: 13.9, 7: 16.7, 8: 13.9, 9: 11.1, 10: 8.3, 11: 5.6, 12: 2.8 }, |
||||
"dr": { 1: 16.7, 2: 16.7, 3: 16.7, 4: 16.7, 5: 16.7, 6: 16.7 }, |
||||
} |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@click.command() |
||||
@click.option( "--file","-f","fname", required=True, help="Log file analysis report." ) |
||||
@click.option( "--players/--no-players","-p", help="Dump the players." ) |
||||
@click.option( "--events/--no-events","-e", help="Dump the events." ) |
||||
@click.option( "--roll-type","-r","roll_type", help="Roll type filter (e.g. IFT or MC)." ) |
||||
@click.option( "--window","-w","window_size", default=1, help="Moving average window size." ) |
||||
def main( fname, players, events, roll_type, window_size ): |
||||
"""Dump a Log File Analysis report (generated by the VASSAL shim).""" |
||||
|
||||
# initialize |
||||
if not os.path.isfile( fname ): |
||||
raise RuntimeError( "Can't find the report file: {}".format( fname ) ) |
||||
|
||||
# parse the report |
||||
report = parse_analysis_report( fname ) |
||||
|
||||
# dump each log file |
||||
for log_file in report["logFiles"]: |
||||
|
||||
# output a header for the next log file |
||||
print( "=== {} {}".format( log_file["filename"], 80*"=" )[ :80 ] ) |
||||
print() |
||||
|
||||
# dump the scenario details |
||||
scenario_name = log_file["scenario"].get( "scenarioName" ) |
||||
if scenario_name: |
||||
print( "Scenario: {}".format( scenario_name ), end="" ) |
||||
scenario_id = log_file["scenario"].get( "scenarioId" ) |
||||
if scenario_id: |
||||
print( " ({})".format( scenario_id ), end="" ) |
||||
print() |
||||
|
||||
# dump the players |
||||
if players: |
||||
print( "Players:" ) |
||||
max_id_len = max( len(k) for k in report["players"] ) |
||||
fmt = "- {:%d} = {}" % max_id_len |
||||
for player_id,player_name in report["players"].items(): |
||||
print( fmt.format( player_id, player_name ) ) |
||||
|
||||
# dump the DR/dr distributions |
||||
dump_distrib( report["players"], log_file, roll_type ) |
||||
print() |
||||
|
||||
# dump the time-plot |
||||
if events or roll_type: |
||||
dump_time_plot( report["players"], log_file, roll_type, window_size ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def dump_distrib( players, log_file, roll_type ): #pylint: disable=too-many-locals,too-many-branches,too-many-statements |
||||
"""Dump the DR/dr distributions.""" |
||||
|
||||
# initialize |
||||
stats = { p: { |
||||
"DR": { "nRolls": 0, "rollTotal": 0 }, |
||||
"dr": { "nRolls": 0, "rollTotal": 0 }, |
||||
} for p in players |
||||
} |
||||
distrib = { p: { |
||||
"DR": { k: 0 for k in range(2,12+1) }, |
||||
"dr": { k: 0 for k in range(1,6+1) }, |
||||
} for p in players |
||||
} |
||||
|
||||
# process events |
||||
for evt in log_file["events"]: |
||||
|
||||
# check if we should process the next event |
||||
if evt["eventType"] != "roll": |
||||
continue |
||||
if roll_type and evt["rollType"].lower() != roll_type.lower(): |
||||
continue |
||||
|
||||
# update the stats |
||||
player_id = evt["playerId"] |
||||
key = "DR" if isinstance( evt["rollValue"], list ) else "dr" |
||||
stats[ player_id ][ key ][ "nRolls" ] += 1 |
||||
val = roll_total( evt["rollValue"] ) |
||||
stats[ player_id ][ key ][ "rollTotal" ] += val |
||||
distrib[ player_id ][ key ][ val ] += 1 |
||||
|
||||
# calculate averages |
||||
avg = lambda x, y: x / y if y != 0 else 0 |
||||
for player_id in players: |
||||
for key in ["DR","dr"]: |
||||
stats[ player_id ][ key ][ "rollAverage" ] = avg( |
||||
stats[player_id][key].pop("rollTotal"), |
||||
stats[player_id][key]["nRolls"] |
||||
) |
||||
|
||||
# calculate chi-squared and hotness |
||||
for player_id in players: |
||||
for key in ["DR","dr"]: |
||||
stats[ player_id ][ key ][ "chiSquared" ] = chi_squared( |
||||
distrib[player_id][ key ], |
||||
EXPECTED_DISTRIB[ key ] |
||||
) |
||||
stats[ player_id ][ key ][ "hotness" ] = hotness( |
||||
distrib[player_id][ key ], |
||||
EXPECTED_DISTRIB[ key ], |
||||
DEFAULT_LFA_DICE_HOTNESS_WEIGHTS[ key ], |
||||
) |
||||
|
||||
# output the results |
||||
for key in ["dr","DR"]: |
||||
print() |
||||
print( "=== {} distribution ===".format( key ) ) |
||||
vals = range(2,12+1) if key == "DR" else range(1,6+1) |
||||
results = [ itertools.chain( [""], vals, ["total","average","chi2","hotness"] ) ] |
||||
total_rolls = sum( stats[p][key]["nRolls"] for p in players ) |
||||
for player_id,player_name in players.items(): |
||||
# add a row for the stats |
||||
row = [ player_name ] |
||||
has_vals = False |
||||
for val in vals: |
||||
nRolls = distrib[player_id][key][val] |
||||
if nRolls != 0: |
||||
row.append( nRolls ) |
||||
has_vals = True |
||||
else: |
||||
row.append( "" ) |
||||
val2 = stats[player_id][key]["nRolls"] |
||||
val2a = val2 / total_rolls if total_rolls != 0 else 0 |
||||
row.append( "{} ({}%)".format( val2, int(100*val2a+0.5) ) ) |
||||
row.append( fpfmt( stats[player_id][key]["rollAverage"], 1 ) ) |
||||
row.append( fpfmt( stats[player_id][key]["chiSquared"], 3 ) ) |
||||
results.append( row ) |
||||
# add a row for the averages |
||||
if has_vals: |
||||
row = [ "" ] |
||||
for val in vals: |
||||
nRolls = distrib[player_id][key][val] |
||||
if nRolls: |
||||
val2 = avg( distrib[player_id][key][val], stats[player_id][key]["nRolls"] ) |
||||
row.append( fpfmt( 100*val2, 1 ) ) |
||||
else: |
||||
row.append( "" ) |
||||
results.append( row ) |
||||
# add a row for the dice hotness |
||||
row = [ "" ] |
||||
partials = stats[ player_id ][ key ][ "hotness" ] |
||||
if partials: |
||||
for val in partials: |
||||
row.append( fpfmt( val, 3 ) ) |
||||
row.extend( [ "", "", "" ] ) |
||||
row.append( fpfmt( sum(partials), 3 ) ) |
||||
results.append( row ) |
||||
print( tabulate.tabulate( results, headers="firstrow" ) ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def dump_time_plot( players, log_file, roll_type, window_size ): |
||||
"""Dump the time-plot values.""" |
||||
|
||||
# initialize |
||||
rolls = [] |
||||
windows = { p: [] for p in players } |
||||
|
||||
def dump_rolls(): |
||||
"""Dump the buffered ROLL events.""" |
||||
print( tabulate.tabulate( rolls, tablefmt="plain" ) ) |
||||
|
||||
def onTurnTrack( evt ): #pylint: disable=unused-variable,possibly-unused-variable |
||||
"""Process a TURN TRACK event.""" |
||||
nonlocal rolls |
||||
if rolls: |
||||
dump_rolls() |
||||
rolls = [] |
||||
print() |
||||
print( "--- {} Turn {} {} ---".format( evt["side"], evt["turnNo"], evt["phase"] ) ) |
||||
print() |
||||
|
||||
def onRoll( evt ) : #pylint: disable=unused-variable,possibly-unused-variable |
||||
"""Process a ROLL event""" |
||||
# check if we should process this ROLL event |
||||
if roll_type: |
||||
if evt["rollType"].lower() != roll_type.lower(): |
||||
return |
||||
player_id = evt[ "playerId" ] |
||||
if window_size == 1: |
||||
# add the raw roll |
||||
if isinstance( evt["rollValue"], int ): |
||||
val = evt["rollValue"] |
||||
else: |
||||
val = ", ".join( str(v) for v in evt["rollValue"] ) |
||||
rolls.append( [ players[player_id], evt["rollType"], val ] ) |
||||
else: |
||||
# add the moving average |
||||
windows[ player_id ].append( roll_total( evt["rollValue"] ) ) |
||||
if len(windows[player_id]) < window_size: |
||||
return |
||||
val = sum( windows[player_id] ) / len(windows[player_id]) |
||||
del windows[player_id][0] |
||||
rolls.append( [ players[player_id], val ] ) |
||||
|
||||
# process events |
||||
print( "=== EVENTS ===" ) |
||||
print() |
||||
for evt in log_file["events"]: |
||||
eventType = evt["eventType"] |
||||
locals()[ "on" + eventType[0].upper() + eventType[1:] ]( evt ) |
||||
if rolls: |
||||
dump_rolls() |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def chi_squared( observed, expected ): |
||||
"""Calculate the chi-squared for a set of values.""" |
||||
nRolls = sum( observed.values() ) |
||||
if nRolls == 0: |
||||
return None |
||||
assert observed.keys() == expected.keys() |
||||
return sum( |
||||
( observed[val]/nRolls - expected[val]/100 ) ** 2 / (expected[val]/100) |
||||
for val in expected |
||||
) |
||||
|
||||
def hotness( observed, expected, weights ): |
||||
"""Calculate the hotness for a set of values.""" |
||||
nRolls = sum( observed.values() ) |
||||
if nRolls == 0: |
||||
return None |
||||
assert observed.keys() == expected.keys() == weights.keys() |
||||
partials = [] |
||||
sign = lambda val: -1 if val < 0 else +1 |
||||
for val in expected: |
||||
diff = observed[val]/nRolls - expected[val]/100 |
||||
partials.append( sign(diff) * diff**2 * weights[val] / (expected[val]/100) ) |
||||
return partials |
||||
|
||||
def roll_total( roll ): |
||||
"""Calculate the total of a roll.""" |
||||
if isinstance( roll, list ): |
||||
assert all( isinstance(r,int) for r in roll ) |
||||
return sum( roll ) |
||||
else: |
||||
assert isinstance( roll, int ) |
||||
return roll |
||||
|
||||
def fpfmt( val, nDigits ): |
||||
"""Format a floating point number.""" |
||||
if val is None: |
||||
return "-" |
||||
return ("{:.%df}" % nDigits).format( val ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
if __name__ == "__main__": |
||||
main() #pylint: disable=no-value-for-parameter |
@ -0,0 +1,83 @@ |
||||
#!/usr/bin/env python3 |
||||
""" Prepare the piece info for a VASL module. |
||||
|
||||
The main program used to identify 5/8" counters by reading a module's buildFile and checking the height |
||||
attribute of the PieceSlot nodes, but it turns out this is the wrong thing to do (this field actually |
||||
controls the size of the piece's entry in the counter palette): |
||||
https://github.com/vasl-developers/vasl/issues/1195 |
||||
|
||||
For each version of VASL supported, run vassal-shim (getPieceInfo command) to analyze the module's |
||||
buildFile and get the correct counter sizes. Then pass the output into this script, to generate |
||||
the final data file that should be saved in the $/data/vasl-$VERSION/ directory, where it will |
||||
be read by the main program. |
||||
|
||||
NOTE: Introducing this process opens the possibility of also extracting the image file paths |
||||
within the .vmod file, instead of the current messy parsing of the PieceSlot CDATA... :-/ |
||||
""" |
||||
|
||||
import sys |
||||
import os |
||||
import json |
||||
|
||||
import xml.etree.ElementTree as ET |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
# initialize |
||||
report = {} |
||||
|
||||
# figure out which GPID's we're interested in |
||||
gpids = set() |
||||
def get_gpids( vo_type ): |
||||
"""Get the GPID's from our data files.""" |
||||
dname = os.path.join( os.path.dirname(__file__), "../webapp/data", vo_type ) |
||||
for root,_,fnames in os.walk( dname ): |
||||
for fname in fnames: |
||||
if os.path.splitext( fname )[1] != ".json": |
||||
continue |
||||
fname = os.path.join( root, fname ) |
||||
with open( fname, "r", encoding="utf-8" ) as fp: |
||||
entries = json.load( fp ) |
||||
for entry in entries: |
||||
entry_gpid = entry.get( "gpid" ) |
||||
if not entry_gpid: |
||||
continue |
||||
if isinstance( entry_gpid, list ): |
||||
gpids.update( str(g) for g in entry_gpid ) |
||||
else: |
||||
gpids.add( str( entry_gpid ) ) |
||||
get_gpids( "vehicles" ) |
||||
get_gpids( "ordnance" ) |
||||
|
||||
# parse the piece info generated by vassal-shim |
||||
doc = ET.parse( sys.stdin ) |
||||
for piece_info in doc.getroot(): |
||||
gpid = piece_info.attrib["gpid"] |
||||
if gpid not in gpids: |
||||
continue |
||||
info = {} |
||||
# check if the next piece is small |
||||
# FUDGE! We used to check for <= 48, but what we get is GamePiece.boundingBox(), which is |
||||
# the click zone for the counter, not the actual size of the counter's image :-/ |
||||
if int( piece_info.attrib["height"] ) <= 55: |
||||
info["is_small"] = True |
||||
if info: |
||||
report[ gpid ] = info |
||||
|
||||
# FUDGE! These are from extensions - it's not worth trying to figure these out programtically. |
||||
report[ "adf:1948" ] = { "is_small": True } # BFP Blood & Jungle: Dutch Brandt 47mm Mortar |
||||
report[ "adf:75" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Type 89 Heavy Grenade Launcher |
||||
report[ "adf:77" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Type 97 Automatic Gun |
||||
report[ "adf:76" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Year-11 Flat-Trajectory INF Gun |
||||
report[ "adf:1407" ] = { "is_small": True } # BFP Poland In Flames: German 2cm Tankbusche S-18 |
||||
report[ "08d:75" ] = { "is_small": True } # Fight For Seoul: American M20(L) 75mm Recoilless Rifle |
||||
|
||||
# output the final report |
||||
print( "{" ) |
||||
lines = [] |
||||
for gpid, piece_info in report.items(): |
||||
lines.append( "\"{}\": {}".format( |
||||
gpid, json.dumps( piece_info ) |
||||
) ) |
||||
print( ",\n".join( lines ) ) |
||||
print( "}" ) |
@ -0,0 +1,10 @@ |
||||
[Debug] |
||||
|
||||
; Set this if you want to run the test suite (allows the webapp server to be controlled using gRPC). |
||||
; CONTROL_TESTS_PORT = -1 |
||||
|
||||
; Set this to a directory containing the VASSAL releases to run the test suite with. |
||||
; TEST_VASSAL_ENGINES = ... |
||||
|
||||
; Set this to a directory containing the VASL modules (.vmod files) to run the test suite with. |
||||
; TEST_VASL_MODS = ... |
@ -0,0 +1,48 @@ |
||||
{ |
||||
|
||||
"_comment_": "This section maps theaters from those at the ASL Scenario Archive to ours.", |
||||
"_comment2_": "CBI is handled in getEffectiveTheater().", |
||||
"theater-mappings": { |
||||
"WTO": "ETO", |
||||
"MTO": "ETO", |
||||
"Normandy": "ETO", |
||||
"KW": "Korea" |
||||
}, |
||||
|
||||
"_comment_": "This section maps player nationalities from those at the ASL Scenario Archive (must be lower-case) to our nationality ID's.", |
||||
"nat-mappings": { |
||||
"australian": "anzac", |
||||
"belgians": "belgian", |
||||
"canada": "british~canadian", |
||||
"canadian": "british~canadian", |
||||
"china cmd": "chinese~gmd", |
||||
"commonwealth": "anzac", |
||||
"filipinos": "filipino", |
||||
"finland": "finnish", |
||||
"free french": "free-french", |
||||
"germany": "german", |
||||
"gurkha": "british", |
||||
"gurkhas": "british", |
||||
"ina": "indonesian", |
||||
"japan": "japanese", |
||||
"kpa": "kfw-kpa", |
||||
"nkpa": "kfw-kpa", |
||||
"north korea": "kfw-kpa", |
||||
"philippine": "filipino", |
||||
"poland": "polish", |
||||
"republic of korea": "kfw-rok", |
||||
"rok": "kfw-rok", |
||||
"rumanian": "romanian", |
||||
"russia": "russian", |
||||
"russians": "russian", |
||||
"slovak": "slovakian", |
||||
"siamese": "thai", |
||||
"soviet": "russian", |
||||
"ss": "german", |
||||
"u.s.": "american", |
||||
"usmc": "american", |
||||
"yugoslav": "yugoslavian", |
||||
"vichy": "french" |
||||
} |
||||
|
||||
} |
@ -0,0 +1,40 @@ |
||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<style> {{CSS:common}} </style> |
||||
</head> |
||||
|
||||
<table> |
||||
|
||||
<tr> |
||||
<td colspan="2" style=" |
||||
background: {{OB_COLOR}} ; |
||||
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||
padding: 2px 5px ; |
||||
font-size: 105% ; font-weight: bold ; |
||||
"> |
||||
{{INCLUDE:player_flag_large}}Anti-Tank Magnetic Mines |
||||
|
||||
<tr> |
||||
<td style="padding:2px 5px;"> |
||||
CC Attack -2 DRM |
||||
|
||||
<tr> |
||||
<td style="padding:2px 5px;"> |
||||
<b>ATMM check</b>: dr ≤ {%if SCENARIO_YEAR < 1944%} 2 {%else%} 3 {%endif%} (△) <br> |
||||
<table style="margin-left:10px;"> |
||||
<tr> |
||||
<td style="width:25px;"> +1 <td> HS |
||||
<tr> |
||||
<td> +1 <td> 1st Line |
||||
<tr> |
||||
<td> +1 <td> CX |
||||
<tr> |
||||
<td> +1 <td> vs. non-armored vehicle |
||||
</table> |
||||
original 6 = pinned (CCV reduced by 1) <br> |
||||
|
||||
</table> |
||||
|
||||
</html> |
@ -0,0 +1,5 @@ |
||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||
|
||||
<img src="{{IMAGES_BASE_URL}}/compass/{{COMPASS}}.png"> |
||||
|
||||
</html> |
@ -0,0 +1,41 @@ |
||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||
|
||||
<!-- vasl-templates:name Booby Traps --> |
||||
<!-- vasl-templates:description Data chart for Booby Traps. --> |
||||
|
||||
<!-- player = {{PLAYER_DROPLIST:|Player}} |
||||
<!-- boards = {{BOARDS*:/8|Board(s)}} --> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<style> |
||||
.header { |
||||
background: {{PLAYER_COLORS[PLAYER_DROPLIST][0]}} ; |
||||
border-bottom: 1px solid {{PLAYER_COLORS[PLAYER_DROPLIST][2]}} ; |
||||
padding: 2px 5px ; |
||||
font-size: 105% ; font-weight: bold ; |
||||
} |
||||
.header .level { font-size: 90% ; font-style: italic ; } |
||||
{{CSS:common}} |
||||
</style> |
||||
</head> |
||||
|
||||
<table> |
||||
|
||||
<tr> |
||||
<td class="header"> |
||||
<img src="{{PLAYER_FLAGS[PLAYER_DROPLIST]}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}"> Booby Traps <span class="level">(Level {{LEVEL:A::B::C/3|Level}})</span> |
||||
|
||||
<tr> |
||||
<td style="padding:2px 5px;"> |
||||
<b> {%if BOARDS%} Boards: {{BOARDS}} {%else%} Entire map {%endif%} </b> |
||||
<ul> |
||||
<li> Original TC |
||||
{% if LEVEL == "A" %} ≥ 11 |
||||
{% elif LEVEL == "B" %} 11 |
||||
{% elif LEVEL == "C" %} 12 |
||||
{%else%} ??? {%endif%} |
||||
<li> Search Casualties |
||||
</ul> |
||||
|
||||
</table> |
@ -0,0 +1,87 @@ |
||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||
|
||||
<!-- vasl-templates:name Kampfgruppe Scherer --> |
||||
<!-- vasl-templates:description Data charts for Grenade Bundles and Molotov Cocktails. --> |
||||
<!-- {{TYPE:Grenade Bundles::Molotov Cocktails/10|Data chart}} --> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<style> {{CSS:common}} </style> |
||||
</head> |
||||
|
||||
<table> |
||||
|
||||
<tr> |
||||
<td colspan="2" style=" |
||||
background: {{PLAYER_COLORS["german"][0]}} ; |
||||
border-bottom: 1px solid {{PLAYER_COLORS["german"][2]}} ; |
||||
padding: 2px 5px ; |
||||
font-size: 105% ; font-weight: bold ; |
||||
"> |
||||
{# Some versions of Java require <img> tags to have the width and height specified!?! #} |
||||
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}"> {%endif%}{{TYPE}} |
||||
|
||||
{% if TYPE == "Grenade Bundles" %} |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
CC Attack -2 DRM |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
<b>ATMM check</b>: dr ≤ 3 (△) |
||||
<table style="margin-left:10px;"> |
||||
<tr> |
||||
<td style="width:20px;"> +1 |
||||
<td> HS/crew |
||||
<tr> |
||||
<td> +2 |
||||
<td> SMC |
||||
<tr> |
||||
<td> +1 |
||||
<td> CX |
||||
<tr> |
||||
<td> +1 |
||||
<td> vs. non-armored vehicle |
||||
</table> |
||||
original 6 = pinned (CCV reduced by 1) |
||||
|
||||
{% elif TYPE == "Molotov Cocktails" %} |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
Against AFV only. |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
<b>MOL check</b>: dr ≤ 3 (△) |
||||
<table style="margin-left:10px;"> |
||||
<tr> |
||||
<td style="width:20px;"> +1 |
||||
<td> HS/crew |
||||
<tr> |
||||
<td> +2 |
||||
<td> SMC |
||||
<tr> |
||||
<td> +1 |
||||
<td> CX |
||||
</table> |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
<b>IFT DR original colored dr</b>: |
||||
<ul> |
||||
<li> 1 = Flame in target Location |
||||
<li> 6 = thrower breaks, Flame in their Location |
||||
</ul> |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
<b>Kindling Attempt</b>: +2 DRM |
||||
|
||||
{%endif%} |
||||
|
||||
</table> |
||||
|
||||
</html> |
||||
|
@ -1,49 +0,0 @@ |
||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||
|
||||
<!-- vasl-templates:name KGS Grenade Bundles --> |
||||
<!-- vasl-templates:description Data chart for Grenade Bundles in <i>Kampfgruppe Scherer</i>. --> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<style> {{CSS:common}} </style> |
||||
</head> |
||||
|
||||
<table> |
||||
|
||||
<tr> |
||||
<td colspan="2" style=" |
||||
background: {{PLAYER_COLORS["german"][0]}} ; |
||||
border-bottom: 1px solid {{PLAYER_COLORS["german"][2]}} ; |
||||
padding: 2px 5px ; |
||||
font-size: 105% ; font-weight: bold ; |
||||
"> |
||||
{# Some versions of Java require <img> tags to have the width and height specified!?! #} |
||||
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}" {{PLAYER_FLAG_SIZE}}> {%endif%}Grenade Bundles |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
-2 CC Attack DRM |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
ATMM check: dr ≤ 3 (△) |
||||
<table style="margin-left:10px;"> |
||||
<tr> |
||||
<td style="width:20px;"> +1 |
||||
<td> HS/crew |
||||
<tr> |
||||
<td> +2 |
||||
<td> SMC |
||||
<tr> |
||||
<td> +1 |
||||
<td> CX |
||||
<tr> |
||||
<td> +1 |
||||
<td> vs. non-armored vehicle |
||||
</table> |
||||
original 6 = pinned (CCV reduced by 1) |
||||
|
||||
</table> |
||||
|
||||
</html> |
||||
|
@ -1,57 +0,0 @@ |
||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||
|
||||
<!-- vasl-templates:name KGS Molotov Cocktails --> |
||||
<!-- vasl-templates:description Data chart for Molotov Cocktails in <i>Kampfgruppe Scherer</i>. --> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<style> {{CSS:common}} </style> |
||||
</head> |
||||
|
||||
<table> |
||||
|
||||
<tr> |
||||
<td colspan="2" style=" |
||||
background: {{PLAYER_COLORS["german"][0]}} ; |
||||
border-bottom: 1px solid {{PLAYER_COLORS["german"][2]}} ; |
||||
padding: 2px 5px ; |
||||
font-size: 105% ; font-weight: bold ; |
||||
"> |
||||
{# Some versions of Java require <img> tags to have the width and height specified!?! #} |
||||
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}" {{PLAYER_FLAG_SIZE}}> {%endif%}Molotov Cocktails |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
vs. AFV only |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
MOL check: dr ≤ 3 (△) |
||||
<table style="margin-left:10px;"> |
||||
<tr> |
||||
<td style="width:20px;"> +1 |
||||
<td> HS/crew |
||||
<tr> |
||||
<td> +2 |
||||
<td> SMC |
||||
<tr> |
||||
<td> +1 |
||||
<td> CX |
||||
</table> |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
IFT DR original colored dr: |
||||
<ul> |
||||
<li> 1 = Flame in target Location |
||||
<li> 6 = thrower breaks, Flame in their Location |
||||
</ul> |
||||
|
||||
<tr> |
||||
<td style="padding:3px 5px 0 5px;"> |
||||
Kindling Attempt: +2 DRM |
||||
|
||||
</table> |
||||
|
||||
</html> |
||||
|
@ -1,2 +1,2 @@ |
||||
{# Some versions of Java require <img> tags to have the width and height specified!?! #} |
||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}" {{PLAYER_FLAG_SIZE}}> {%endif%} |
||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}?prefh={{PLAYER_FLAG_SIZE}}" width="{{PLAYER_FLAG_SIZE}}" height="{{PLAYER_FLAG_SIZE}}"> {%endif%} |
||||
|
@ -0,0 +1,2 @@ |
||||
{# Some versions of Java require <img> tags to have the width and height specified!?! #} |
||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}"> {%endif%} |
@ -0,0 +1,116 @@ |
||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<style> {{CSS:common}} </style> |
||||
|
||||
<style> |
||||
|
||||
td { |
||||
width: 50px ; min-width: 50px ; |
||||
height: {%if TURN_TRACK_PREVIEW_MODE%} 50px {%else%} 43px {%endif%} ; |
||||
padding: 2px ; |
||||
border: 1px solid black ; |
||||
} |
||||
{% set RESET_TD = "min-width: unset ; height: unset ; padding: 0 ; border: none" %} |
||||
td.turn-no { |
||||
{{RESET_TD}} ; width: unset ; |
||||
text-align: center ; vertical-align: center ; font-size: 18px ; font-weight: bold ; |
||||
} |
||||
td.no-reinforce { {{RESET_TD}} ; width: 13px ; } |
||||
{# NOTE: We do the reinforcement flags as CSS backgrounds, since VASSAL is incredibly slow downloading normal images. #} |
||||
td.reinforce1 { {{RESET_TD}} ; width: 13px ; background: url("{{TURN_TRACK_FLAG_1}}") top left no-repeat ; vertical-align: top ; } |
||||
td.reinforce2 { {{RESET_TD}} ; width: 13px ; background: url("{{TURN_TRACK_FLAG_2}}") bottom right no-repeat ; vertical-align: bottom ; } |
||||
td.half-turn { |
||||
background: url("{{TURN_TRACK_HALF_TURN_IMAGE_URL}}") bottom right no-repeat ; |
||||
background-size: contain ; {# nb: doesn't work in VASSAL, the image file needs to be the correct size :-/ #} |
||||
} |
||||
|
||||
{% if TURN_TRACK_PREVIEW_MODE %} |
||||
body { margin: 0 ; } |
||||
body ::selection {} |
||||
body ::moz-selection {} |
||||
body { user-select: none ; } |
||||
.reinforce1, .reinforce2 { opacity: 0 ; } |
||||
.flag-click { width: 13px ; height: 13px ; cursor: pointer ; } |
||||
.shading-click { cursor: pointer ; } |
||||
{%endif%} |
||||
|
||||
</style> |
||||
|
||||
</head> |
||||
|
||||
{% if TURN_TRACK_PREVIEW_MODE %} |
||||
<script> |
||||
// notify the parent window of clicks |
||||
function onFlagClick( turnNo, playerNo ) { |
||||
window.parent.postMessage( { |
||||
type: "FlagClick", |
||||
turnNo: turnNo, uiPlayerNo: playerNo |
||||
}, "*" ) ; |
||||
} |
||||
function onShadingClick( turnNo ) { |
||||
window.parent.postMessage( { |
||||
type: "ShadingClick", |
||||
turnNo: turnNo |
||||
}, "*" ) ; |
||||
} |
||||
</script> |
||||
{%endif%} |
||||
|
||||
<table class="turn-track"> |
||||
|
||||
{% for row in TURN_TRACK_SQUARES %} |
||||
<tr> |
||||
|
||||
{% for turnSquare in row %} |
||||
<td id="turn-square-{{turnSquare[0]}}" |
||||
{%if turnSquare[0] == TURN_TRACK_HALF_TURN%} class="half-turn" {%endif%} |
||||
{% if turnSquare[3] %} style="background-color:{{turnSquare[3]}};" {%endif%} |
||||
> |
||||
|
||||
<table style="width:100%;height:100%;"> <tr> |
||||
|
||||
<td id="flag-{{turnSquare[0]}}_1" width="100%" |
||||
class = {% if turnSquare[1] %} "reinforce1" {%else%} "no-reinforce" {%endif%} |
||||
> |
||||
{% if TURN_TRACK_PREVIEW_MODE %} |
||||
<div class="flag-click" |
||||
onclick = "onFlagClick( {{turnSquare[0]}}, 1 )" |
||||
> </div> |
||||
{%endif%} |
||||
</td> |
||||
|
||||
<td class="turn-no"> |
||||
{% if TURN_TRACK_PREVIEW_MODE %} |
||||
<div class="shading-click" |
||||
onclick = "onShadingClick( {{turnSquare[0]}} )" |
||||
> |
||||
{{turnSquare[0]}} |
||||
</div> |
||||
{%else%} |
||||
{{turnSquare[0]}} |
||||
{%endif%} |
||||
</td> |
||||
|
||||
<td id="flag-{{turnSquare[0]}}_2" width="100%" |
||||
class = {% if turnSquare[2] and turnSquare[0] != TURN_TRACK_HALF_TURN %} "reinforce2" {%else%} "no-reinforce" {%endif%} |
||||
> |
||||
{% if TURN_TRACK_PREVIEW_MODE and turnSquare[0] != TURN_TRACK_HALF_TURN %} |
||||
<div class="flag-click" |
||||
onclick = "onFlagClick( {{turnSquare[0]}}, 2 )" |
||||
> </div> |
||||
{%endif%} |
||||
</td> |
||||
|
||||
</tr> </table> |
||||
|
||||
</td> |
||||
{%endfor%} |
||||
|
||||
</tr> |
||||
{%endfor%} |
||||
|
||||
</table> |
||||
|
||||
</html> |
@ -0,0 +1,44 @@ |
||||
{ |
||||
|
||||
"extensionId": "08d", |
||||
"version": "0.0", |
||||
"displayName": "Fight For Seoul", |
||||
"displayNameAbbrev": "FfS", |
||||
|
||||
"american": { |
||||
|
||||
"vehicles": [ |
||||
|
||||
{ "name": "POA-CWS-H5", |
||||
"_comment_": "This was copied from kfw-uro/v:005. We can't use copy_from since K:FW is also an extension, and we don't control load order.", |
||||
"type": "MTv", |
||||
"CS#": 6, |
||||
"capabilities2": { "C": 5, "sM": 8 }, |
||||
"note_number": "5\u2020", |
||||
"notes": [ "C", "M" ], |
||||
"comments": [ "TCA restrictions", "CE: MA, SA Fire NA", "Fire MA & SA NA" ], |
||||
"id": "ffs/v:000", |
||||
"gpid": "08d:15" |
||||
} |
||||
|
||||
], |
||||
|
||||
"ordnance": [ |
||||
|
||||
{ "name": "M20(L) 75mm Recoilless Rifle", |
||||
"_comment_": "This was copied from kfw-un-common/o:004. We can't use copy_from since K:FW is also an extension, and we don't control load order.", |
||||
"type": "RCL", |
||||
"capabilities": [ "H\u2020" ], |
||||
"capabilities2": { "WP": 7 }, |
||||
"comments": [ "∞ H", "Crewed" ], |
||||
"note_number": "25\u2020", |
||||
"notes": [ "K", "M", "O", "P", "R" ], |
||||
"id": "ffs/o:000", |
||||
"gpid": "08d:75" |
||||
} |
||||
|
||||
] |
||||
|
||||
} |
||||
|
||||
} |