parent
41476c80e5
commit
35f31b12ba
@ -0,0 +1,107 @@ |
||||
""" Manage the ASOP. """ |
||||
|
||||
import os |
||||
|
||||
from flask import jsonify, render_template_string, send_from_directory, safe_join, url_for, abort |
||||
|
||||
from asl_rulebook2.webapp import app |
||||
from asl_rulebook2.webapp.utils import load_data_file |
||||
|
||||
_asop = None |
||||
_asop_dir = None |
||||
_asop_section_content = None |
||||
user_css_url = None |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def init_asop( startup_msgs, logger ): |
||||
"""Initialize the ASOP.""" |
||||
|
||||
# initiailize |
||||
global _asop, _asop_dir, _asop_section_content, user_css_url |
||||
_asop, _asop_section_content = {}, {} |
||||
|
||||
# get the data directory |
||||
data_dir = app.config.get( "DATA_DIR" ) |
||||
if not data_dir: |
||||
return None, None |
||||
base_dir = os.path.join( data_dir, "asop/" ) |
||||
if not os.path.isdir( base_dir ): |
||||
return None, None |
||||
_asop_dir = base_dir |
||||
fname = os.path.join( base_dir, "asop.css" ) |
||||
if os.path.isfile( fname ): |
||||
user_css_url = url_for( "get_asop_file", path="asop.css" ) |
||||
|
||||
# load the ASOP index |
||||
fname = os.path.join( base_dir, "index.json" ) |
||||
_asop = load_data_file( fname, "ASCOP index", False, logger, startup_msgs.error ) |
||||
if not _asop: |
||||
return None, None |
||||
|
||||
# load the ASOP content |
||||
for chapter in _asop.get( "chapters", [] ): |
||||
# load the chapter preamble |
||||
preamble = _render_template( chapter["chapter_id"] + "-0.html" ) |
||||
if preamble: |
||||
chapter["preamble"] = preamble |
||||
# load the content for each section |
||||
for section_no, section in enumerate( chapter.get( "sections", [] ) ): |
||||
section_id = "{}-{}".format( chapter["chapter_id"], 1+section_no ) |
||||
section[ "section_id" ] = section_id |
||||
content = _render_template( section_id + ".html" ) |
||||
_asop_section_content[ section_id ] = content |
||||
|
||||
return _asop, _asop_section_content |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/asop" ) |
||||
def get_asop(): |
||||
"""Return the ASOP.""" |
||||
return jsonify( _asop ) |
||||
|
||||
@app.route( "/asop/intro" ) |
||||
def get_asop_intro(): |
||||
"""Return the ASOP intro.""" |
||||
resp = _render_template( "intro.html" ) |
||||
if not resp: |
||||
return "No ASOP intro." |
||||
return resp |
||||
|
||||
@app.route( "/asop/footer" ) |
||||
def get_asop_footer(): |
||||
"""Return the ASOP footer.""" |
||||
resp = _render_template( "footer.html" ) |
||||
if not resp: |
||||
abort( 404 ) |
||||
return resp |
||||
|
||||
@app.route( "/asop/section/<section_id>" ) |
||||
def get_asop_section( section_id ): |
||||
"""Return the specified ASOP section.""" |
||||
content = _asop_section_content.get( section_id ) |
||||
if not content: |
||||
abort( 404 ) |
||||
return content |
||||
|
||||
@app.route( "/asop/<path:path>" ) |
||||
def get_asop_file( path ): |
||||
"""Return a user-defined ASOP file.""" |
||||
return send_from_directory( _asop_dir, path ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _render_template( fname ): |
||||
"""Render an ASOP template.""" |
||||
if not _asop_dir: |
||||
return None |
||||
fname = safe_join( _asop_dir, fname ) |
||||
if not os.path.isfile( fname ): |
||||
return None |
||||
args = { |
||||
"ASOP_BASE_URL": url_for( "get_asop_file", path="" ), |
||||
} |
||||
args.update( _asop.get( "template_args", {} ) ) |
||||
with open( fname, "r" ) as fp: |
||||
return render_template_string( fp.read(), **args ) |
@ -0,0 +1,178 @@ |
||||
import { gMainApp, gASOPChapterIndex, gASOPSectionIndex, gEventBus } from "./MainApp.js" ; |
||||
import { getASOPChapterIdFromSectionId, wrapMatches, isChildOf } from "./utils.js" ; |
||||
|
||||
let gSectionContentOverrides = {} ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
gMainApp.component( "asop", { |
||||
|
||||
data() { return { |
||||
isActive: false, |
||||
title: null, |
||||
preamble: null, |
||||
sections: [], isSingleSection: false, |
||||
chapterId: null, |
||||
} ; }, |
||||
|
||||
template: ` |
||||
<div v-if=isActive :data-chapterid=chapterId id="asop" class="asop" > |
||||
<div v-html=title class="title" /> |
||||
<div v-html=preamble class="preamble" /> |
||||
<div v-if="sections.length > 0" class="sections" :class="{single: isSingleSection}" ref="sections" > |
||||
<div v-for="s in sections" :key=s class="section" v-html=s /> |
||||
</div> |
||||
</div> |
||||
`,
|
||||
|
||||
created() { |
||||
|
||||
// handle the ASOP being entered/exited
|
||||
gEventBus.on( "tab-activated", (tabbedPages, tabId) => { |
||||
if ( ! isChildOf( tabbedPages.$el, $("#nav")[0], false ) ) |
||||
return ; |
||||
this.isActive = (tabId == "asop") ; |
||||
} ) ; |
||||
|
||||
// handle events in the nav pane
|
||||
gEventBus.on( "asop-chapter-expanded", this.showASOPChapter ) ; |
||||
gEventBus.on( "show-asop-section", this.showASOPSection ) ; |
||||
gEventBus.on( "show-asop-entry-sr", this.showASOPSectionSearchResult ) ; |
||||
|
||||
// remove search highlights when a new search is done
|
||||
gEventBus.on( "search", () => { |
||||
gSectionContentOverrides = {} ; |
||||
} ) ; |
||||
}, |
||||
|
||||
mounted() { |
||||
// start off with the intro
|
||||
this.showIntro() ; |
||||
}, |
||||
|
||||
updated() { |
||||
// scroll to the top of the sections each time
|
||||
if ( this.$refs.sections ) |
||||
this.$refs.sections.scrollTop = 0 ; |
||||
}, |
||||
|
||||
methods: { |
||||
|
||||
showIntro() { |
||||
// show the ASOP intro
|
||||
this.title = "Advanced Sequence Of Play" ; |
||||
this.preamble = null ; |
||||
this.sections = [] ; |
||||
this.isSingleSection = true ; |
||||
this.chapterId = "intro" ; |
||||
$.get( gGetASOPIntroUrl, (resp) => { //eslint-disable-line no-undef
|
||||
this.sections = [ this.fixupContent( resp ) ] ; |
||||
} ).fail( (xhr, status, errorMsg) => { |
||||
// NOTE: We show the error in the content, not as a notification balloon.
|
||||
this.sections = [ "Couldn't get the ASOP intro." + " <div class='pre'>" + errorMsg + "</div>" ] ; |
||||
} ) ; |
||||
} , |
||||
|
||||
showASOPChapter( chapter, isClick ) { |
||||
if ( ! isClick ) |
||||
return ; |
||||
// prepare to show the ASOP chapter (with all sections combined)
|
||||
this.title = this.makeTitle( chapter, chapter.caption ) ; |
||||
this.preamble = this.fixupContent( chapter.preamble ) ; |
||||
this.sections = chapter.sections ? Array( chapter.sections.length ) : [] ; |
||||
this.isSingleSection = false ; |
||||
this.chapterId = chapter.chapter_id ; |
||||
if ( this.sections.length == 0 ) |
||||
return ; |
||||
// show each section
|
||||
let addSectionContent = (sectionNo, content) => { |
||||
this.sections[ sectionNo ] = |
||||
"<div class='caption'>" + chapter.sections[sectionNo].caption + "</div>" |
||||
+ this.fixupContent( content ) ; |
||||
} ; |
||||
chapter.sections.forEach( (section, sectionNo) => { |
||||
// check if there is an override for the next section
|
||||
let sectionId = chapter.chapter_id + "-" + (1+sectionNo) ; |
||||
let contentOverride = gSectionContentOverrides[ sectionId ] ; |
||||
if ( contentOverride ) { |
||||
// yup - just use that
|
||||
addSectionContent( sectionNo, contentOverride ) ; |
||||
} else { |
||||
// nope - download the section from the backend
|
||||
new Promise( (resolve, reject) => { |
||||
let url = gGetASOPSectionUrl.replace( "SECTION_ID", sectionId ) ; //eslint-disable-line no-undef
|
||||
$.get( url, (resp) => { |
||||
addSectionContent( sectionNo, resp ) ; |
||||
resolve() ; |
||||
} ).fail( (xhr, status, errorMsg) => { |
||||
// NOTE: We show the error in the content, not as a notification balloon.
|
||||
this.sections[ sectionNo ] = |
||||
"Couldn't get ASOP section <tt>" + sectionId + "</tt>." |
||||
+ " <div class='pre'>" + errorMsg + "</div>" ; |
||||
reject() ; |
||||
} ) ; |
||||
} ) ; |
||||
} |
||||
} ) ; |
||||
}, |
||||
|
||||
showASOPSection( chapter, section ) { |
||||
this.doShowASOPSection( chapter, section, "" ) ; |
||||
// show the specified ASOP section
|
||||
let sectionId = section.section_id ; |
||||
let url = gGetASOPSectionUrl.replace( "SECTION_ID", sectionId ) ; //eslint-disable-line no-undef
|
||||
$.get( url, (resp) => { |
||||
this.doShowASOPSection( chapter, section, resp ) ; |
||||
} ).fail( (xhr, status, errorMsg) => { |
||||
// NOTE: We show the error in the content, not as a notification balloon.
|
||||
this.sections = [ |
||||
"Couldn't get ASOP section <tt>"+sectionId+"</tt>." + " <div class='pre'>" + errorMsg + "</div>" |
||||
] ; |
||||
} ) ; |
||||
}, |
||||
|
||||
showASOPSectionSearchResult( sectionId, content ) { |
||||
// show the specified ASOP section
|
||||
let chapterId = getASOPChapterIdFromSectionId( sectionId ) ; |
||||
let chapter = gASOPChapterIndex[ chapterId ] ; |
||||
if ( ! chapter ) { |
||||
console.log( "INTERNAL ERROR: Can't find parent chapter for section ID: " + sectionId ) ; |
||||
return ; |
||||
} |
||||
let section = gASOPSectionIndex[ sectionId ] ; |
||||
if ( ! section ) { |
||||
console.log( "INTERNAL ERROR: Can't find section ID: " + sectionId ) ; |
||||
return ; |
||||
} |
||||
gSectionContentOverrides[ sectionId ] = content ; |
||||
this.doShowASOPSection( chapter, section, content ) ; |
||||
}, |
||||
|
||||
doShowASOPSection( chapter, section, content ) { |
||||
// show the specified ASOP section
|
||||
this.title = this.makeTitle( chapter, section.caption ) ; |
||||
this.preamble = this.fixupContent( chapter.preamble ) ; |
||||
let contentOverride = gSectionContentOverrides[ section.section_id ] ; |
||||
this.sections = [ this.fixupContent( contentOverride || content ) ] ; |
||||
this.isSingleSection = true ; |
||||
this.chapterId = chapter.chapter_id ; |
||||
}, |
||||
|
||||
makeTitle( chapter, caption ) { |
||||
// generate a chapter title
|
||||
if ( chapter.sniper_phase ) |
||||
caption += "<sup><span title='Sniper Attacks/Checks are possible during this phase.'>†</span></sup>" ; |
||||
return caption ; |
||||
}, |
||||
|
||||
fixupContent( content ) { |
||||
return wrapMatches( |
||||
content, |
||||
new RegExp( /\[EXC: .*?\]/g ), |
||||
"<span class='exc'>", "</span>" |
||||
) ; |
||||
}, |
||||
|
||||
}, |
||||
|
||||
} ) ; |
@ -0,0 +1,39 @@ |
||||
/* NOTE: This file defines general CSS for the ASOP. Any user-defined settings that are specific |
||||
* to the user's ASOP files can be defined in a file called asop.css, in the ASOP directory. |
||||
* |
||||
* "#asop" refers to the ASOP popup window that overlays the content pane, but anything |
||||
* that styles the ASOP content itself should be referenced via ".asop" (since ASOP content |
||||
* can also appear as a search result in the left sidebar). |
||||
* */ |
||||
|
||||
#asop { |
||||
position: absolute ; top: 5px ; left: 5px ; bottom: 5px ; right: 5px ; z-index: 20 ; |
||||
display: flex ; flex-direction: column ; |
||||
border: 1px solid #444 ; border-radius: 5px ; background: white ; |
||||
padding: 10px ; |
||||
} |
||||
|
||||
#nav .tabbed-page[data-tabid="asop"] .wrapper { display: flex ; flex-direction: column ; } |
||||
#nav .tabbed-page[data-tabid="asop"] .accordian { flex-grow: 1 ; overflow-y: auto ; } |
||||
#nav .tabbed-page[data-tabid="asop"] .accordian-pane .title { background: #f6edda ; border: 1px solid #e5cea0 ; font-size: 110% ; } |
||||
#nav .tabbed-page[data-tabid="asop"] .accordian-pane[data-chapterid="pre-game"] .title { background: #fff6e2 ; } |
||||
#nav .tabbed-page[data-tabid="asop"] .accordian .entry { margin: 6px 0 ; } |
||||
#asop-footer { border-top: 1px solid #ccc ; margin-top: 5px ; padding: 5px ; } |
||||
|
||||
#asop>.title { background: #f6edda ; border: 1px solid #e5cea0 } |
||||
#asop[data-chapterid="pre-game"]>.title { background: #fff6e2 ; } |
||||
|
||||
.asop .pre { margin-left: 30px ; font-family: monospace ; } |
||||
.asop .exc { font-style: italic ; color: #666 ; } |
||||
.asop .hilite { padding: 0 2px ; background: #ffa ; } |
||||
|
||||
.asop .title { border: 1px solid #666 ; border-radius: 2px ; background: #eee ; font-size: 125% ; font-weight: bold ; padding: 2px 5px ; } |
||||
|
||||
.asop .preamble { margin-bottom: 5px ; padding: 2px 5px ; font-size: 90% ; font-style: italic ; } |
||||
.asop .preamble i, #asop .preamble em { font-style: normal ; } |
||||
|
||||
.asop .sections { flex-grow: 1 ; overflow-y: auto ; } |
||||
.asop .section { margin-top: 10px ; border: 1px solid #444 ; border-radius: 5px; padding: 10px ; } |
||||
.asop .section:first-of-type { margin-top: 0 ; } |
||||
.asop .sections.single .section { border: none ; padding: 0 5px ; } |
||||
.asop .section .caption { font-weight: bold ; } |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=A> 7.11A </div> <ul> |
||||
<li> May Transfer SW/Guns/Prisoners (A4.431; A20.5) {Ski-use dr; E4.21}. |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A> 7.13A </div> <ul> |
||||
<li> Boat/Non-Aground LC in Heavy Surf makes any required (un)Beaching DR (G13.442/G13.4423). |
||||
</ul> </div> |
@ -0,0 +1,12 @@ |
||||
<div class="entry"> <div class=A> 7.21A </div> <ul> |
||||
<li> Good Order Infantry, not pinned or TI, may advance (A4.7): <ul> |
||||
<li> PAATC; A11.6 |
||||
<li> vs. Difficult Terrain = CX/Panji MC; A4.72, G9.41 |
||||
</ul> |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A> 7.26A </div> <ul> |
||||
<li> All ⅝" Parachutes on-board are removed, and replaced by their contents (E9.6). |
||||
</ul> </div> |
@ -0,0 +1,17 @@ |
||||
<div class="entry"> <div class=B> 5.11B </div> <ul> |
||||
<li> During Mild Breeze, place Drifting (i.e. gray) Dispersed SMOKE downwind of: <ul> |
||||
<li> each Blaze |
||||
<li> each white SMOKE counter that has none (A24.61) |
||||
</ul> |
||||
[EXC: NA in cave; G11.851] |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A> 5.13A </div> <ul> |
||||
<li> During Gusts (B25.651): <ul> |
||||
<li> Remove Dispersed SMOKE. |
||||
<li> Flip remaining SMOKE counters to Dispersed side. |
||||
</ul> |
||||
[EXC: NA in Cave; G11.8] |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=A> 5.21A </div> <ul> |
||||
<li> Place all Glider contents <span class=exc>[EXC: vehicle/Gun and its PRC/Crew]</span> on-board (E8.4). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A> 5.23A </div> <ul> |
||||
<li> Each berserk unit that eliminated all Known enemy units (at least one) in its Location with halved TPBF, returns to Good Order (A15.46). |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=B> 5.31B </div> <ul> |
||||
<li> Resolve Blaze Spread (B25.6; B25.651) every Player Turn <i>after</i> initial appearance. |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=B> 5.33B </div> <ul> |
||||
<li> If night scenario, also remove all First Fire, Final Fire, and Gunflash counters (E1.8). |
||||
</ul> </div> |
@ -0,0 +1,24 @@ |
||||
.asop .A { |
||||
padding-left: 1.5em ; background: url(images/attacker.png) no-repeat ; background-size: 1.1em ; |
||||
margin-top: 0.5em ; font-weight: bold ; |
||||
} |
||||
.asop .D { |
||||
padding-left: 1.5em ; background: url(images/defender.png) no-repeat ; background-size: 1.1em ; |
||||
margin-top: 0.5em ; font-weight: bold ; |
||||
} |
||||
.asop .B { |
||||
padding-left: 1.5em ; background: url(images/both-players.png) no-repeat ; background-size: 1.1em ; |
||||
margin-top: 0.5em ; font-weight: bold ; |
||||
} |
||||
|
||||
.asop img.icon { height: 1em ; margin-bottom: -2px ; } |
||||
.asop .content-removed { margin: 5px 0 ; font-style: italic ; color: #aaa ; } |
||||
|
||||
.asop p { margin-top: 0.5em ; } |
||||
.asop p:first-of-type { margin-top: 0 ; } |
||||
.asop li { margin-top: 0.25em ; } |
||||
.asop li li { margin-top: 0.1em ; } |
||||
.asop li li li { margin-top: 0.1em ; } |
||||
|
||||
#asop-footer { font-size: 80% ; font-style: italic ; } |
||||
#asop-footer img { height: 1.2em ; margin-right: 0.25em ; } |
@ -0,0 +1 @@ |
||||
Perform all Steps listed under <i>"... LOCATION'S CCPh" </i>in any one CC/Melee Location first, then in the next such Location, etc. |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=B> 8.11B </div> <ul> |
||||
<li> Place on-board, beneath a "?", all hidden items, then reveal Strength Factors of all concealed units (eliminating Dummies) (A11.19). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=B> 8.16B </div> <ul> |
||||
<li> Declare each SMC's solo status, or pair it with another SMC or MMC (A11.14); ATTACKER first (A11.12). |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=B> 8.21B </div> <ul> |
||||
<li> Declare first/next sequential CC attack (A11.3-.34) or, ATTACKER first (A11.12; G13.495), all simultaneous CC attacks, if no sequential CC exists. |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=B> 8.25B </div> <ul> |
||||
<li> May Interrogate new Prisoners (E2.1; G1.621; G18.71). |
||||
</ul> </div> |
@ -0,0 +1,5 @@ |
||||
<div class="entry"> <div class=B> 8.31B </div> <ul> |
||||
<li> Automatic capture of unescorted, abandoned vehicles (A21.2). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
@ -0,0 +1,13 @@ |
||||
<div class="entry"> <div class=B> 8.41B </div> <ul> |
||||
<li> Declare and resolve (sequentially; ATTACKER first) all Aerial Combat (E7.22-.226). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=B> 8.45B </div> <ul> |
||||
<li> If night, remove all: <ul> |
||||
<li> Starshells (E1.923) |
||||
<li> IR (E1.933) |
||||
<li> Acquisition not Illuminated by Blaze/Flame (E1.74) |
||||
</ul> |
||||
</ul> </div> |
@ -0,0 +1,11 @@ |
||||
<div class="entry"> <div class=D> 4.11D </div> <ul> |
||||
<li> May fire ordnance Dispersed SMOKE (C8.5)/MTR IR (E1.91; E1.93-.932). <ul> |
||||
<li> Resolve ensuing WP NMC (A24.31). |
||||
</ul> |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=D> 4.14D </div> <ul> |
||||
<li> Check for Column Disbandment (E11.533) and Reverse Slopes (G14.66-.661). |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=D> 4.21D </div> <ul> |
||||
<li> May designate Spotters for MTR's that had no original Spotter (C9.3). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=D> 4.22D </div> <ul> |
||||
<li> May (un)limber Guns (C10.21; it and crew become TI if unlimbering). |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=D> 4.31D </div> <ul> |
||||
<li> May change CA of Guns presently able to fire without using Intensive Fire (C3.22). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=D> 4.32D </div> <ul> |
||||
<li> In daytime scenario, remove all First and Final Fire counters (A3.4; E1.8). |
||||
</ul> </div> |
@ -0,0 +1,13 @@ |
||||
{# The contents of this file will appear in the nav panel, underneath the list of ASOP chapters. |
||||
You can just delete it, if you don't want it. |
||||
#} |
||||
|
||||
<div style="display:flex;align-items:center;"> |
||||
<img src="{{ASOP_BASE_URL}}/images/attacker.png"> ATTACKER |
||||
<img src="{{ASOP_BASE_URL}}/images/defender.png"> DEFENDER |
||||
<img src="{{ASOP_BASE_URL}}/images/both-players.png"> Both |
||||
</div> |
||||
|
||||
<div style="margin-top:0.5em;"> |
||||
† Sniper Attacks/Checks are possible during this phase (A14.1; A14.4; E1.72; E1.76; G12.603; G14.261; <i>RB</i> SSR CG8, <i>ABtF</i> SSR CG8, <i>BRT</i> SSR CG8, <i>KGP</i> SSR CG16, and <i>PB</i> SSR CG13). |
||||
</div> |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 5.0 KiB |
@ -0,0 +1,78 @@ |
||||
{ |
||||
|
||||
"chapters": [ |
||||
|
||||
{ "caption": "Pre-Game Sequence", "chapter_id": "pre-game", |
||||
"sections": [ |
||||
{ "caption": "First steps" }, |
||||
{ "caption": "First side set up" }, |
||||
{ "caption": "Interlude" }, |
||||
{ "caption": "Second side set up" }, |
||||
{ "caption": "Final steps" } |
||||
] }, |
||||
{ "caption": "1. Rally Phase", "chapter_id": "rally", |
||||
"sections": [ |
||||
{ "caption": "1.1: START of RPh" }, |
||||
{ "caption": "1.2: DURING RPh" }, |
||||
{ "caption": "1.3: END of RPh" } |
||||
] }, |
||||
{ "caption": "2. Prep Fire Phase", "chapter_id": "prep-fire", |
||||
"sniper_phase": true, |
||||
"sections": [ |
||||
{ "caption": "2.1: START of PFPh" }, |
||||
{ "caption": "2.2: DURING PFPh" }, |
||||
{ "caption": "2.3: END of PFPh" } |
||||
] }, |
||||
{ "caption": "3. Movement Phase", "chapter_id": "movement", |
||||
"sniper_phase": true, |
||||
"sections": [ |
||||
{ "caption": "3.1: START of the MPh" }, |
||||
{ "caption": "3.2: START of ITS MPh" }, |
||||
{ "caption": "3.3: DURING ITS MPh" }, |
||||
{ "caption": "3.4: END of ITS MPh" }, |
||||
{ "caption": "3.5: END of the MPh" } |
||||
] }, |
||||
{ "caption": "4. Defensive Fire Phase", "chapter_id": "defensive-fire", |
||||
"sniper_phase": true, |
||||
"sections": [ |
||||
{ "caption": "4.1: START of DFPh" }, |
||||
{ "caption": "4.2: DURING DFPh" }, |
||||
{ "caption": "4.3: END of DFPh" } |
||||
] }, |
||||
{ "caption": "5. Advancing Fire Phase", "chapter_id": "advancing-fire", |
||||
"sniper_phase": true, |
||||
"sections": [ |
||||
{ "caption": "5.1: START of AFPh" }, |
||||
{ "caption": "5.2: DURING AFPh" }, |
||||
{ "caption": "5.3: END of AFPh" } |
||||
] }, |
||||
{ "caption": "6. Rout Phase", "chapter_id": "rout", |
||||
"sections": [ |
||||
{ "caption": "6.1: START of RtPh" }, |
||||
{ "caption": "6.2: DURING RtPh" }, |
||||
{ "caption": "6.3: END of RtPh" } |
||||
] }, |
||||
{ "caption": "7. Advance Phase", "chapter_id": "advance", |
||||
"sections": [ |
||||
{ "caption": "7.1: START of AFPh" }, |
||||
{ "caption": "7.2: DURING AFPh" } |
||||
] }, |
||||
{ "caption": "8. Close Combat Phase", "chapter_id": "close-combat", |
||||
"sections": [ |
||||
{ "caption": "8.1: START of LOCATION's CCPh" }, |
||||
{ "caption": "8.2: DURING LOCATION's CCPh" }, |
||||
{ "caption": "8.3: END of LOCATION's CCPh" }, |
||||
{ "caption": "8.4: END of CCPh" } |
||||
] }, |
||||
|
||||
{ "chapter_id": "empty", |
||||
"_comment_": "This is used for testing porpoises only." |
||||
} |
||||
|
||||
], |
||||
|
||||
"template_args": { |
||||
"CONTENT_REMOVED": "<div class='content-removed'> ...content removed... </div>" |
||||
} |
||||
|
||||
} |
@ -0,0 +1,10 @@ |
||||
<p> As listed in the Advanced Sequence of Play (ASOP), each phase is usually broken down into three main parts: the START, DURING, END, and several discrete Steps. In each Step Number (e.g. "1.11A"), the players involved are specified as: |
||||
<ul> |
||||
<li class=A> ATTACKER |
||||
<li class=D> DEFENDER |
||||
<li class=B> Both |
||||
</ul> |
||||
|
||||
<p> {{CONTENT_REMOVED|safe}} |
||||
|
||||
<p> Certain mutually exclusive actions may be listed in the same Step, despite the fact that they cannot be conducted by the same unit, and many restrictions normally applicable to the listed actions are left unmentioned. In both cases, the normal rules pertaining to such actions still apply.</p> |
@ -0,0 +1,5 @@ |
||||
<p>The MPh Sequence of Play is expressed separately in terms of <i>THE</i> MPh, and of each moving unit/stack's MPh ... |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<p> ... must be completed before any Glider/Parachute may start <i>ITS</i> MPh. |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=A> 3.11A </div> <ul> |
||||
<li> May designate new mortar Spotter, for one eliminated or not in Good Order (C9.3). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A> 3.13A </div> <ul> |
||||
<li> Place all Gliders, blue side up (i.e, in Aerial Locations), on-board in their ILH (E8.2). |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=A> 3.21A </div> <ul> |
||||
<li> Prepare to move any currently berserk unit/stack required to charge (A15.43); then go to Step 3.31A <span class="exc">[EXC: if no such berserk unit can charge, go to Step 3.22A]</span>. |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A> 3.23A </div> <ul> |
||||
<li> Prepare to conduct Glider/Parachute movement; go to Step 3.37D <span class="exc">[EXC: if no Aerial Glider exists, go to Step 3.34A]</span>. |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=A> 3.31A </div> <ul> |
||||
<li> Berserk unit charges, if so required (A15.43-.431; A15.45; G13.491). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=D> 3.38D </div> <ul> |
||||
<li> Then go to Step 3.43A. |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=A>3.41A</div> <ul> |
||||
<li> Non-Bypassing Good Order Infantry/Cavalry may Search (A12.152; E1.95/E1.953; G1.63) {Casualties; A12.154}; becomes TI - Defensive First/Subsequent First/FPF allowed. |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A>3.43A</div> <ul> |
||||
<li> Then go to Step 3.5. |
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=A> 3.51A </div> <ul> |
||||
<li> Each vehicle unable to leave, and each Glider/Parachute that landed in, terrain Blaze Location is eliminated (B25.4; E8.232; E9.42). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=B> 3.53B </div> <ul> |
||||
<li> Remove all Residual FP (A8.2; A9.223), and ½" SMOKE (A24.11 <span class=exc>[EXC: G11.85]</span>) counters. |
||||
</ul> </div> |
@ -0,0 +1,3 @@ |
||||
<p> Follow in the order given. Not all will apply to every scenario. Items pertinent only to a DYO scenario are marked with a <img src="{{ASOP_BASE_URL}}/images/dyo.png" class="icon">. |
||||
|
||||
<p> Should the order of actions given in the body of the rules conflict with this Sequence, the latter takes precedence, <em>except</em> in the case of a CG Refit Phase. |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <ul> |
||||
|
||||
<li> Agree upon which (if any) optional/house rules will be in effect (A16, B10.211, C13.311, E1-E2, E4-E12, footnote A18/C5/C9, Incremental IFT (A7.37), etc.). |
||||
|
||||
<li> {{CONTENT_REMOVED|safe}} |
||||
|
||||
<li> <img src="{{ASOP_BASE_URL}}/images/dyo.png" class="icon"> Determine EC (B25.5, F11.4, G16.3, or O11.618; see also E3.3, E3.4, E3.6, E3.713, E3.72, E3.73, E3.74, and/or F11.6111, R9.62163). |
||||
|
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <ul> |
||||
|
||||
<li> First side (or the side "defending the beach"; G13.95) commences setup. |
||||
|
||||
<li> {{CONTENT_REMOVED|safe}} |
||||
|
||||
<li> First side (or the side "defending the beach"; G13.95) completes setup. |
||||
|
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <ul> |
||||
|
||||
<li> Scenario Attacker makes one Recon dr, if allowed (E1.23). |
||||
|
||||
<li> {{CONTENT_REMOVED|safe}} |
||||
|
||||
<li> <img src="{{ASOP_BASE_URL}}/images/dyo.png" class="icon"> Determine Surf (G13.98; see also G13.448), if applicable. |
||||
|
||||
</ul> </div> |
@ -0,0 +1,7 @@ |
||||
<div class="entry"> <ul> |
||||
|
||||
<li>Second side commences setup (also repeat all Steps above marked with <img src="{{ASOP_BASE_URL}}/images/both-players.png" class="icon">). |
||||
|
||||
<li>Second side completes setup. |
||||
|
||||
</ul> </div> |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <ul> |
||||
|
||||
<li> Record all allowed NOBA Ocean hexes (G14.62). |
||||
|
||||
<li> {{CONTENT_REMOVED|safe}} |
||||
|
||||
<li> Scenario Attacker determines Creeping-Barrage timing (E12.72), if applicable, then conducts (E12.72-.74) all "pre-Game Turns" if/as required. |
||||
|
||||
</ul> </div> |
@ -0,0 +1,18 @@ |
||||
<!-- NOTE: ASOP entries should be in separate <div> blocks, with class "entry". |
||||
The Prep Fire data files use different ways of setting these blocks up, for testing porpoises only. |
||||
If you are using these files as a basis for setting up a real ASOP, you can replace them with |
||||
the more normal: |
||||
<div class="entry"> |
||||
... |
||||
</div> |
||||
--> |
||||
|
||||
<div foo="bar" class="entry" this="that" > <div class=A> 2.11A </div> <ul> |
||||
<li> Remove his Dispersed SMOKE (checking for any Napalm terrain-Blaze/weapon destruction; G17.41); then flip his SMOKE counters to their Dispersed side (A24.4). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class='entry'> <div class=A> 2.15A </div> <ul> |
||||
<li> Check for Column Disbandment (E11.533) and Reverse Slopes (G14.66-.661). |
||||
</ul> </div> |
@ -0,0 +1,21 @@ |
||||
<!-- NOTE: ASOP entries should be in separate <div> blocks, with class "entry". |
||||
The Prep Fire data files use different ways of setting these blocks up, for testing porpoises only. |
||||
If you are using these files as a basis for setting up a real ASOP, you can replace them with |
||||
the more normal: |
||||
<div class="entry"> |
||||
... |
||||
</div> |
||||
--> |
||||
|
||||
<div class = "foo entry bar"> <div class=A> 2.21A </div> <ul> |
||||
<li> Infantry MMC may become TI and: <ul> |
||||
<li> Mop Up (A12.153) {Casualties; A12.154}, or |
||||
<li> attempt to entrench (A25.21; B27.11; F.1B; G3.5; G13.3; G13.82), placing Labor counter if unsuccessful. |
||||
</ul> |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class = ' entry '> <div class=A> 2.23A </div> <ul> |
||||
<li> May (un)limber Guns (C10.21; it and crew become TI, if unlimbering). |
||||
</ul> </div> |
@ -0,0 +1,8 @@ |
||||
<div class="entry"> <div class=A>2.31A</div> <ul> |
||||
<li> May change CA of Guns presently able to fire without using Intensive Fire (C3.22). |
||||
<li> May designate/cancel AA mode of weapons that can/does thusly change CA (E7.5). |
||||
</ul> </div> |
||||
|
||||
<div class="_entry_" style="font-size:80%;font-style:italic;color:#aaa;"> |
||||
This text is here for testing porpoises only, and should not be detected by the search engine (because it's not part of an "entry" div). |
||||
</div> |
@ -0,0 +1 @@ |
||||
Only one action (attempt) allowed per unit per RPh [EXC: repairing > one SW/Gun (A9.72); leader rallying > one unit (A10.7); Recovery (A4.44) is not an action by a broken unit]. |
@ -0,0 +1,16 @@ |
||||
<div class="entry"> <div class=A> 1.11A </div> <ul> |
||||
<li> Roll for any provisional (SSR) reinforcements (including Air Support; E7.2). |
||||
<li> Set up, off-board, all forces due to enter this Player Turn (A2.51-.52). <ul> |
||||
<li> DD tanks; D16.8 |
||||
<li> Cloaking; E1.41 |
||||
<li> Gliders; E8.1 |
||||
<li> Parachutes; E9.1-.11 |
||||
<li> LC; G1.664/G14.23 |
||||
</ul> |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=B> 1.14B </div> <ul> |
||||
<li> May attempt to Recover SW/Guns in same Location (A4.44; D6.31; G.5) {Ski-use dr; E4.21}. |
||||
</ul> </div> |
@ -0,0 +1,12 @@ |
||||
<div class="entry"> <div class=A> 1.21A </div> <ul> |
||||
<li> May (attempt to) Deploy Good Order squads, if Good Order leader present, and/or Unarmed/Guards/Finns/Carrier HS/USMC 7-6-8's without leader (A1.31; G17.11). |
||||
<li> Infantry MMC may attempt to Scrounge abandoned vehicles or non-burning wrecks (D10.5). <ul> |
||||
<li> Place Scrounged and TI markers. |
||||
</ul> |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=A>1.24A</div> <ul> |
||||
<li> Determine final Drop Point for each Para Wing, then place all Sticks (i.e. Parachutes; E9.12) on-board, in Aerial Locations. |
||||
</ul> </div> |
@ -0,0 +1,10 @@ |
||||
<div class="entry"> <div class=B> 1.31B </div> <ul> |
||||
<li> Roll for Shocked/UK AFV recuperation (C7.42). <ul> |
||||
<li> Remove or flip marker/AFV, as appropriate. |
||||
</ul> |
||||
<li> May/must remove DM markers from eligible broken units (A10.62). |
||||
</ul> </div> |
||||
|
||||
<div class="entry"> <div class=B> 1.32B </div> <ul> |
||||
<li> May claim Wall Advantage (ATTACKER first). |
||||
</ul> </div> |
@ -0,0 +1 @@ |
||||
ATTACKER first, then DEFENDER (A3.6). |
@ -0,0 +1,9 @@ |
||||
<div class="entry"> <div class=B> 6.11B </div> <ul> |
||||
<li> Units may Voluntarily Break (A10.41). |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
||||
|
||||
<div class="entry"> <div class=B> 6.12B </div> <ul> |
||||
<li> Disrupted units in/ADJACENT to enemy Infantry/Cavalry Location (might) Surrender (A19.12) {Interrogation; E2.1}. |
||||
</ul> </div> |
@ -0,0 +1,7 @@ |
||||
<div class="entry"> <div class=B> 6.21B </div> <ul> |
||||
<li> Conduct all routs (A10.5-.52; A19.12; E1.54; G14.41). <ul> |
||||
<li> Leaders may accompany routing units (A10.711). |
||||
</ul> |
||||
</ul> </div> |
||||
|
||||
{{CONTENT_REMOVED|safe}} |
@ -0,0 +1,3 @@ |
||||
<div class="entry"> <div class=B> 6.31B </div> <ul> |
||||
<li> Eliminate all Infantry unable to leave terrain Blaze Locations (B25.4). |
||||
</ul> </div> |
@ -0,0 +1,289 @@ |
||||
""" Test the ASOP. """ |
||||
|
||||
import os |
||||
import json |
||||
|
||||
from asl_rulebook2.webapp.tests.test_search import do_search |
||||
from asl_rulebook2.webapp.tests.utils import init_webapp, select_tabbed_page, \ |
||||
wait_for, wait_for_elem, find_child, find_children, unload_elem, unload_sr_text, get_image_filename, has_class |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_asop_nav( webdriver, webapp ): |
||||
"""Test the ASOP nav.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "asop" ) |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# load the ASOP |
||||
fname = os.path.join( os.path.dirname(__file__), "fixtures/asop/asop/index.json" ) |
||||
with open( fname, "r" ) as fp: |
||||
asop_index = json.load( fp ) |
||||
|
||||
# check the nav |
||||
select_tabbed_page( "#nav", "asop" ) |
||||
nav = _unload_nav( False ) |
||||
chapters = asop_index["chapters"] |
||||
for chapter_no, chapter in enumerate( chapters ): |
||||
chapters[ chapter_no ] = { |
||||
key: val for key, val in chapter.items() |
||||
if key != "sniper_phase" and not key.startswith("_") |
||||
} |
||||
chapter.pop( "sniper_phase", None ) |
||||
assert nav == asop_index["chapters"] |
||||
|
||||
# check the footer |
||||
footer = find_child( "#asop-footer" ) |
||||
assert "Sniper Attacks/Checks are possible" in footer.text |
||||
images = [ |
||||
get_image_filename( c, full=True ) |
||||
for c in find_children( "img", footer ) |
||||
] |
||||
assert images == [ |
||||
"/asop/images/attacker.png", |
||||
"/asop/images/defender.png", |
||||
"/asop/images/both-players.png" |
||||
] |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_asop_content( webdriver, webapp ): |
||||
"""Test the ASOP content.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "asop" ) |
||||
init_webapp( webapp, webdriver ) |
||||
select_tabbed_page( "#nav", "asop" ) |
||||
nav = _unload_nav( True ) |
||||
|
||||
def load_asop_file( fname, as_json ): |
||||
"""Load an ASOP data file.""" |
||||
fname = os.path.join( base_dir, fname ) |
||||
if not os.path.isfile( fname ): |
||||
return None |
||||
with open( fname, "r" ) as fp: |
||||
return json.load( fp ) if as_json else fp.read() |
||||
|
||||
# load the ASOP index |
||||
base_dir = os.path.join( os.path.dirname(__file__), "fixtures/asop/asop/" ) |
||||
asop_index = load_asop_file( "index.json", True ) |
||||
|
||||
def check_chapter( chapter_no, callbacks ): |
||||
"""Check the specified ASOP chapter.""" |
||||
|
||||
# open the chapter |
||||
expected_chapter = asop_index["chapters"][ chapter_no ] |
||||
nav[ chapter_no ][ "elem" ].click() |
||||
expected = len( expected_chapter["sections"] ) |
||||
wait_for( 2, lambda: len( find_children( "#asop .sections .section" ) ) == expected ) |
||||
|
||||
# check the title |
||||
title = find_child( "#asop .title" ).text |
||||
expected = expected_chapter[ "caption" ] |
||||
if expected_chapter.get( "sniper_phase" ): |
||||
expected += "\u2020" |
||||
assert title == expected |
||||
chapter_id = find_child( "#asop" ).get_attribute( "data-chapterid" ) |
||||
assert chapter_id == expected_chapter["chapter_id"] |
||||
|
||||
# check the preamble |
||||
expected_preamble = load_asop_file( "{}-0.html".format( chapter_id ), False ) |
||||
preamble = find_child( "#asop .preamble" ).text |
||||
if preamble: |
||||
# NOTE: We only check the first few characters since the data file is processed as a template, |
||||
# so it may not match exactly what's in the UI. |
||||
assert preamble[0:20] in expected_preamble |
||||
else: |
||||
assert preamble == "" |
||||
|
||||
# do any chapter-specific checks |
||||
func = callbacks.get( "check_{}".format( chapter_id.replace("-","_") ) ) |
||||
if func: |
||||
func() |
||||
|
||||
# check each section (in the combined view) |
||||
expected_sections = expected_chapter[ "sections" ] |
||||
section_elems = find_children( "#asop .sections .section" ) |
||||
assert len(expected_sections) == len(section_elems) |
||||
for section_no in range( len(expected_sections) ): |
||||
|
||||
# check the section's caption |
||||
caption = find_child( ".caption", section_elems[section_no] ).text |
||||
assert caption == expected_sections[section_no]["caption"] |
||||
|
||||
# check the section's content |
||||
section_id = "{}-{}".format( chapter_id, 1+section_no ) |
||||
expected_content = load_asop_file( section_id+".html", False ) |
||||
content = find_child( "ul", section_elems[section_no] ).text |
||||
assert content[0:20] in expected_content |
||||
|
||||
# check each individual section |
||||
for section_no, nav_section in enumerate( nav[chapter_no]["sections"] ): |
||||
|
||||
# click on the section in the nav pane |
||||
find_child( "a", nav_section["elem"] ).click() |
||||
|
||||
# wait for the section's content to appear |
||||
expected = expected_sections[ section_no ][ "caption" ] |
||||
if expected_chapter.get( "sniper_phase" ): |
||||
expected += "\u2020" |
||||
wait_for( 2, lambda: find_child("#asop .title").text == expected ) |
||||
|
||||
# check the preamble |
||||
# NOTE: The preamble is part of the parent chapter, and so should remain unchanged. |
||||
preamble = find_child( "#asop .preamble" ).text |
||||
if preamble: |
||||
assert preamble[0:20] in expected_preamble |
||||
else: |
||||
assert preamble == "" |
||||
|
||||
# check the section's content |
||||
section_id = "{}-{}".format( chapter_id, 1+section_no ) |
||||
expected_content = load_asop_file( section_id+".html", False ) |
||||
sections = find_children( "#asop .sections .section" ) |
||||
assert len(sections) == 1 |
||||
content = find_child( "ul", sections[0] ).text |
||||
assert content[0:20] in expected_content |
||||
|
||||
# do any section-specific checks |
||||
func = callbacks.get( "check_{}_section".format( chapter_id.replace("-","_") ) ) |
||||
if func: |
||||
func( 1+section_no ) |
||||
|
||||
def check_pre_game(): #pylint: disable=possibly-unused-variable |
||||
# check the DYO image in the preamble |
||||
assert get_image_filename( find_child("#asop .preamble img.icon"), full=True ) == "/asop/images/dyo.png" |
||||
# check the images in the sections |
||||
images = set( |
||||
get_image_filename( c, full=True ) |
||||
for c in find_children( "#asop .sections .section img.icon" ) |
||||
) |
||||
assert images == set( [ |
||||
"/asop/images/dyo.png", "/asop/images/both-players.png" |
||||
] ) |
||||
def check_pre_game_section( section_no ): #pylint: disable=possibly-unused-variable |
||||
if section_no == 1: |
||||
# check the DYO image in the preamble |
||||
assert get_image_filename( find_child("#asop .preamble img.icon"), full=True ) == "/asop/images/dyo.png" |
||||
|
||||
def check_rally(): #pylint: disable=possibly-unused-variable |
||||
# check the EXC block in the preamble |
||||
assert has_class( find_child( "#asop .preamble span" ), "exc" ) |
||||
|
||||
def check_movement(): #pylint: disable=possibly-unused-variable |
||||
# there should be 3 EXC blocks |
||||
elems = [ |
||||
c for c in find_children( "#asop .sections .section span" ) |
||||
if has_class( c, "exc" ) |
||||
] |
||||
elems = set( e.text for e in elems ) # why are we seeing everything twice?! :-/ |
||||
assert len(elems) == 3 |
||||
def check_movement_section( section_no ): #pylint: disable=possibly-unused-variable |
||||
# check an EXC block |
||||
if section_no == 5: |
||||
assert has_class( find_child( "#asop .sections .section span" ), "exc" ) |
||||
|
||||
# check each chapter |
||||
for i in range( 0, 8+1 ): |
||||
check_chapter( i, locals() ) |
||||
|
||||
# check error handling |
||||
nav[ 9 ][ "elem" ].click() |
||||
sections = find_children( "#asop .sections .section" ) |
||||
assert len(sections) == 0 |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_asop_entries( webdriver, webapp ): |
||||
"""Test searching for individual ASOP entries.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "asop" ) |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
def do_test( query_string, expected ): |
||||
|
||||
# do the search |
||||
results = do_search( query_string ) |
||||
|
||||
# check the search results |
||||
assert len(results) == len(expected) |
||||
for i in range( len(results) ): |
||||
assert expected[i][0] in results[i]["content"] |
||||
assert expected[i][1]in results[i]["content"] |
||||
|
||||
# make sure we can click through to the ASOP |
||||
sr_elems = find_children( "#search-results .asop-entry-sr" ) |
||||
for sr_no, sr_elem in enumerate( sr_elems ): |
||||
|
||||
# click on the search result |
||||
find_child( ".caption", sr_elem ).click() |
||||
wait_for_elem( 2, "#asop" ) |
||||
|
||||
# check the contents of the ASOP popup |
||||
entries = find_children( "#asop .sections .section .entry" ) |
||||
assert len(entries) == 1 |
||||
assert expected[sr_no][0] in unload_sr_text( entries[0] ) |
||||
|
||||
# check the nav pane |
||||
panes = [ |
||||
c for c in find_children( "#nav .accordian-pane" ) |
||||
if find_child( ".entries", c ).value_of_css_property( "display" ) != "none" |
||||
] |
||||
assert len(panes) == 1 |
||||
assert panes[0].get_attribute( "data-chapterid" ) == expected[sr_no][2] |
||||
|
||||
# return back to the Search nav pane |
||||
select_tabbed_page( "#nav", "search" ) |
||||
|
||||
# do the tests |
||||
do_test( "napalm", [ |
||||
[ "2.11A", "checking for any ((Napalm)) terrain-Blaze/weapon", "prep-fire" ] |
||||
] ) |
||||
do_test( "reverse", [ |
||||
[ "2.15A", "((Reverse)) Slopes", "prep-fire" ], |
||||
[ "4.14D", "((Reverse)) Slopes", "defensive-fire" ], |
||||
] ) |
||||
do_test( '"mop up"', [ |
||||
[ "2.21A", "((Mop Up)) (A12.153)", "prep-fire" ] |
||||
] ) |
||||
do_test( '"crew become TI"', [ |
||||
[ "2.23A", "it and ((crew become TI))", "prep-fire" ], |
||||
[ "4.22D", "it and ((crew become TI))", "defensive-fire" ], |
||||
] ) |
||||
|
||||
# search for something that is not part of an "entry" div |
||||
results = do_search( "porpoise" ) |
||||
assert results is None |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _unload_nav( include_elems ): |
||||
"""Unload the ASOP nav.""" |
||||
|
||||
chapters = [] |
||||
for chapter_elem in find_children( "#nav .accordian-pane" ): |
||||
|
||||
# unload the next chapter |
||||
chapter = { |
||||
"chapter_id": chapter_elem.get_attribute( "data-chapterid" ), |
||||
} |
||||
unload_elem( chapter, "caption", find_child(".title",chapter_elem) ) |
||||
if include_elems: |
||||
chapter[ "elem" ] = chapter_elem |
||||
|
||||
# unload the chapter's sections |
||||
chapter_elem.click() # nb: panes must be open to unload their sections :-/ |
||||
sections = [] |
||||
for section_elem in find_children( ".entry", chapter_elem ): |
||||
sections.append( { |
||||
"caption": section_elem.text, |
||||
} ) |
||||
if include_elems: |
||||
sections[-1]["elem"] = section_elem |
||||
if sections: |
||||
chapter[ "sections" ] = sections |
||||
chapters.append( chapter ) |
||||
|
||||
return chapters |
Loading…
Reference in new issue