texlive[66713] Master/texmf-dist: wargame (30mar23)

commits+karl at tug.org commits+karl at tug.org
Thu Mar 30 22:06:53 CEST 2023


Revision: 66713
          http://tug.org/svn/texlive?view=revision&revision=66713
Author:   karl
Date:     2023-03-30 22:06:53 +0200 (Thu, 30 Mar 2023)
Log Message:
-----------
wargame (30mar23)

Modified Paths:
--------------
    trunk/Master/texmf-dist/doc/latex/wargame/README.md
    trunk/Master/texmf-dist/doc/latex/wargame/compat.pdf
    trunk/Master/texmf-dist/doc/latex/wargame/symbols.pdf
    trunk/Master/texmf-dist/doc/latex/wargame/tutorial/Makefile
    trunk/Master/texmf-dist/doc/latex/wargame/tutorial/export.tex
    trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.pdf
    trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.sty
    trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.tex
    trunk/Master/texmf-dist/doc/latex/wargame/tutorial/patch.py
    trunk/Master/texmf-dist/doc/latex/wargame/wargame.pdf
    trunk/Master/texmf-dist/source/latex/wargame/Makefile
    trunk/Master/texmf-dist/source/latex/wargame/chit/core.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/elements.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/misc.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/shape.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/board.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/coord.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/core.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/extra.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/paths.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/ridges.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/shape.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/beach.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/city.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/light_woods.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/mountains.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/rough.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/swamp.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/town.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/village.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/woods.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/terrain.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/towns.dtx
    trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/frames/friendly.dtx
    trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/shape.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/core.dtx
    trunk/Master/texmf-dist/source/latex/wargame/utils/wgexport.py
    trunk/Master/texmf-dist/source/latex/wargame/utils/wgsvg2tikz.py
    trunk/Master/texmf-dist/source/latex/wargame/wargame.dtx
    trunk/Master/texmf-dist/source/latex/wargame/wargame.ins
    trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.chit.code.tex
    trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.hex.code.tex
    trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.natoapp6c.code.tex
    trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.util.code.tex
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.beach.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.city.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.light_woods.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.mountains.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.rough.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.swamp.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.town.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.village.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wargame.woods.pdf
    trunk/Master/texmf-dist/tex/latex/wargame/wgexport.cls
    trunk/Master/texmf-dist/tex/latex/wargame/wgexport.py
    trunk/Master/texmf-dist/tex/latex/wargame/wgsvg2tikz.py

Added Paths:
-----------
    trunk/Master/texmf-dist/source/latex/wargame/chit/battle.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/dice.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/modifiers.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/oob.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/stack.dtx
    trunk/Master/texmf-dist/source/latex/wargame/chit/table.dtx
    trunk/Master/texmf-dist/source/latex/wargame/hex/split.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/bb.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/compound.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/export.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/icons.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/misc.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/randomid.dtx
    trunk/Master/texmf-dist/source/latex/wargame/util/tikz.dtx

Modified: trunk/Master/texmf-dist/doc/latex/wargame/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/latex/wargame/README.md	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/doc/latex/wargame/README.md	2023-03-30 20:06:53 UTC (rev 66713)
@@ -1,8 +1,6 @@
-A package to make Hex'n'Counter wargames in LaTeX
-=================================================
+# A package to make Hex'n'Counter wargames in LaTeX
 
-Version 0.3.2
--------------
+## Version 0.5
 
 This package can help make classic [Hex'n'Counter
 wargames](https://en.wikipedia.org/wiki/Wargame) using LaTeX. The
@@ -25,14 +23,12 @@
 -   The package support exporting the game to a
     [VASSAL](#vassal-support%20'Section%20below') module
 
-Sources
--------
+## Sources
 
 The sources of the package are kept at
 [GitLab](https://gitlab.com/wargames_tex/wargame_tex)
 
-Download from GitLab
---------------------
+## Download from GitLab
 
 -   [Zip file of package and support
     files](https://gitlab.com/wargames_tex/wargame_tex/-/jobs/artifacts/master/download?job=dist)
@@ -67,14 +63,12 @@
 
     make install DESTDIR=/usr/local/share/texmf
 
-Download from CTAN
-------------------
+## Download from CTAN
 
 The package is available from
 [CTAN](https://ctan.org/tex-archive/macros/latex/contrib/wargame) in the
-directory `/macros/latex/contrib/wargame`. As of this writing, the
-package has not hit any of the major distributions (e.g.,
-[TeXLive](https://tug.org/texlive/index.html)).
+directory `/macros/latex/contrib/wargame`. The package is part of the
+CTAN distribution [TeXLive](https://tug.org/texlive/index.html).
 
 ### From TDS zip archive
 
@@ -170,8 +164,7 @@
 
     sudo apt install poppler-utils python3-pil
 
-Tutorial
---------
+## Tutorial
 
 See the [tutorial](tutorial/README.md "Link works on GitLab only") page
 for more
@@ -178,8 +171,7 @@
 ([here](https://ctan.org/tex-archive/macros/latex/contrib/wargame/doc/tutorial/)
 if you browse from CTAN).
 
-Examples
---------
+## Examples
 
 Below are some print'n'play board wargames made with this package. These
 are not original games but rather revamps of existing games. All credits
@@ -192,6 +184,9 @@
     -   Western Front
         -   [D-Day](https://gitlab.com/wargames_tex/dday_tex) (Avalon
             Hill classic)
+        -   [Smithsonian
+            D-Day](https://gitlab.com/wargames_tex/sdday_tex) (Avalon
+            Hill)
         -   [Paul Koenig's Market
             Garden](https://gitlab.com/wargames_tex/pkmg_tex)
         -   [Paul Koenig's
@@ -208,9 +203,10 @@
     -   [Strike Force One](https://gitlab.com/wargames_tex/sfo_tex)
     -   [Kriegspiel](https://gitlab.com/wargames_tex/kriegspiel_tex)
         (Avalon Hill classic)
+    -   [Port Stanley](https://gitlab.com/wargames_tex/portstanley_tex)
+        (from the *Wargamer* magazine)
 
-VASSAL support
---------------
+## VASSAL support
 
 The packages has the script [`wgexport.py`](utils/wgexport.py) to
 generate a draft [VASSAL](https://vassalengine.org) module from the
@@ -231,9 +227,15 @@
 Moscow](https://gitlab.com/wargames_tex/bfm_tex) project, and of course
 in the [tutorial](tutorial/ "Link works on GitLab only")
 
-NATO App6
----------
+The sources of this script is kept in a different project
+[`pywargame`](https://gitlab.com/wargames_tex/pywargame) also on GitLab.
+The script is generated in that package. The `pywargame` package allows
+one to manipulate VASSAL modules from Python, or to read (and convert)
+[CyberBoard](http://cyberboard.norsesoft.com/)
+([GitHub](https://github.com/CyberBoardPBEM/cbwindows)) scenarios.
 
+## NATO App6
+
 The package supports
 
 -   All air, equipment, installation, land, sea surface, sub surface,
@@ -257,8 +259,7 @@
 -   Other LaTeX package for making NATO symbology
     [XMilSymb](https://github.com/ralphieraccoon/MilSymb)
 
-Copyright and license
----------------------
+## Copyright and license
 
 (c) 2022 Christian Holm Christensen
 
@@ -277,7 +278,7 @@
 according to the *ShareAlike* clause of CC-BY-SA-4.0. That is, you can
 license your materials *any way you want*.
 
-However, that copyright rights on games is not as prohibitive as you may
+However, copyright rights on games is not as prohibitive as you may
 think (see [this thread on
 BGG](https://boardgamegeek.com/thread/493249/)). What you *can*
 copyright is original, artistic expression. That is, the copyrighted

Modified: trunk/Master/texmf-dist/doc/latex/wargame/compat.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/latex/wargame/symbols.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/latex/wargame/tutorial/Makefile
===================================================================
--- trunk/Master/texmf-dist/doc/latex/wargame/tutorial/Makefile	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/doc/latex/wargame/tutorial/Makefile	2023-03-30 20:06:53 UTC (rev 66713)
@@ -8,7 +8,7 @@
 		   -shell-escape
 EXPORT		:= ../utils/wgexport.py
 EXPORT_FLAGS	:=
-
+PATCH		:= patch.py
 ifdef VERBOSE	
 MUTE		:=
 REDIR		:=
@@ -22,15 +22,15 @@
 
 %.aux:%.tex
 	@echo "LATEX $< -> $@"
-	$(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
+	$(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
 
 %.pdf:%.aux
 	@echo "LATEX $*.tex -> $@ (via $*.aux)"
-	$(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR)
+	$(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR)
 
 %.pdf:%.tex
 	@echo "LATEX $< -> $@"
-	$(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
+	$(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
 
 %.json:%.pdf
 
@@ -39,11 +39,12 @@
 game.aux:game.tex game.sty export.tex 
 export.pdf:export.tex game.sty 
 
-Game.vmod:export.pdf export.json patch.py game.pdf
+Game.vmod:export.pdf export.json $(PATCH) game.pdf
 	@echo "$(EXPORT) $< -> $@"
-	$(MUTE)$(EXPORT) export.pdf export.json -p patch.py -r game.pdf \
+	$(MUTE)$(EXPORT) export.pdf export.json -p $(PATCH) -r game.pdf \
 		-d "Example module from LaTeX PnP game" \
-		-t "LaTeX wargame tutorial" -v 0.1 -o $@
+		-t "LaTeX wargame tutorial" -v 0.1 -o $@ \
+		$(EXPORT_FLAGS)
 
 clean:
 	rm -f *~ *.log *.out *.aux *.pdf *.vmod *.json

Modified: trunk/Master/texmf-dist/doc/latex/wargame/tutorial/export.tex
===================================================================
--- trunk/Master/texmf-dist/doc/latex/wargame/tutorial/export.tex	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/doc/latex/wargame/tutorial/export.tex	2023-03-30 20:06:53 UTC (rev 66713)
@@ -72,7 +72,7 @@
 % For that we will use the macro \verb+\doublechitimages+ (if we had
 % single sided counters, we would use \verb+\chitimages+).  This macro
 % takes two arguments: The list of counter definitions and the faction
-% of the chits.  So lets go ahead and make those for factions A and B,
+% of the chits.  Let's go ahead and make those for factions A and B,
 % and the special faction ``Markers'' which will only be the game turn
 % counter.
 %
@@ -79,7 +79,7 @@
 %    \begin{macrocode}
 \doublechitimages[A]{\alla}
 \doublechitimages[B]{\allb}
-\doublechitimages[Markers]{{game turn}}
+\doublechitimages[Markers]{{game turn chit}}
 %    \end{macrocode}
 %
 % Again, the macros we made for the counters comes in handy.  Note
@@ -87,6 +87,51 @@
 % VASSAL module.  We will, in fact, use them one more time to create
 % the counter sheet.
 %
+% To automatically make ``battle markers'' --- markers that identify
+% individual battles by placing a numbered marker on top of the
+% combatants, we can use the macro \verb+battlemarkers+.  It takes one
+% argument, which is the number of unique markers to make and add.
+% The markers are round yellow circles with a number in
+% them\footnote{This can of course be customised as everything else
+% can.}.
+%
+%    \begin{macrocode}
+\battlemarkers{12}
+%    \end{macrocode}
+%
+% Furthermore, we can add ``odds'' and battle result markers.   Odds
+% markers can be placed via the battle markers context menu, and then
+% later be replaced by result markers via the context menu of the odds
+% markers.  This is done via the macros \verb+\oddsmarkers+ and
+% \verb+\resultmarkers+.  Both macros accept a list of options (odds
+% and results, respectively) with a possible background colour to use
+% in the markers. 
+% 
+%    \begin{macrocode}
+\oddsmarkers{%
+  1:3/red!25!white,%
+  1:2/red!15!white,%
+  1:1/orange!15!white,%
+  2:1/white,%
+  3:1/green!15!white,%
+  4:1/green!25!white}
+\resultmarkers{
+  AE/red!50!white,
+  AR/red!25!white,
+  EX/white,
+  DR/green!10!white,
+  DE/green!25!white}
+%    \end{macrocode}
+%
+% By default, odds markers must be placed ``by-hand'', but via the
+% VASSAL preferences it is possible to calculate the odds and show
+% them when the battle is declared.  By default, this calculation will
+% only be concerned with the combat factors of the involved units, and
+% features such as terrain is ignored.  However, with a bit of Python
+% skill, one can flesh out the implementation of the game rules in the
+% optional patch (Python) script.  We will not do that here, as a it
+% is a little beyond the scope of this tutorial.
+%
 % Right, so that made our counter images and the associated meta
 % data.  Next thing is to add our board.  For that, we use the
 % environment \texttt{boardimage}.  Inside that environment we must
@@ -116,7 +161,7 @@
 %    \end{macrocode}
 %    
 % We have two charts that we would also like to be put in: The CRT and
-% the TE.  These \emph{must} be put into \Tikz{} pictures, and we
+% the TEC.  These \emph{must} be put into \Tikz{} pictures, and we
 % precede those with the macro \verb+\info+. This macro takes three
 % arguments: The name of the next image, the category, and
 % sub-category.   Again, the name is more or less free form, and the
@@ -153,7 +198,7 @@
 \end{tikzpicture}
 %    \end{macrocode}
 %    
-% We can a splash page image.  We have not defined such an image in
+% We can add splash page image.  We have not defined such an image in
 % the \texttt{game} package, but we could have, so we will make it
 % here. Again, it should be a \Tikz{} picture. For this, we need to
 % use the category \texttt{front}.
@@ -172,68 +217,67 @@
 % the like.   We will add a few such icons here, mainly to show how
 % the \texttt{patch.py} script works.
 %
-% First, a button icon for the pool window.
+% The \textsf{wgexport.cls} class provides a number of icons we may
+% use for this.  These are
 %
+% \begin{center}
+%   \begin{tikzpicture}
+%     \pic          {flip icon};
+%     \pic at (2.0,0) {restore icon};
+%     \pic at (3.5,0) {eliminate icon};
+%     \pic at (5.0,0) {pool icon};
+%     \pic at (6.5,0) {oob icon={black}{black}};
+%   \end{tikzpicture}
+% \end{center}
+% 
+%
+% We will add these as pictures to our export PDF. First, the image
+% for the pool of units. 
+%
 %    \begin{macrocode}
 \info{pool-icon}{icon}{}
-\begin{tikzpicture}[scale=.15]
-  \draw[scale line widths,line width=4mm]
-  (0,1)--(0,-1) (-.7,.2)--(.7,.2);
+\begin{tikzpicture}[transform shape,scale=.4]
+  \pic{pool icon};
 \end{tikzpicture}
 %    \end{macrocode}
 %
-% The category \texttt{icon} has no special meaning.  The icon will
-% look like \tikz[scale=.3]{\draw[scale line widths,line width=4mm]
-% (0,1)--(0,-1) (-.7,.2)--(.7,.2);}
+% The category \texttt{icon} has no special meaning.  
 %
 % We also want to add a custom icon for the OOB button.  VASSAL has a
 % button icon for the piece inventory which is really quite
-% appropriate, but alas it is already in use, so we will make our own
-% (albeit similar).
+% appropriate, but alas it is already in use, so we will make our
+% own. Here, we use the \TikZ{} picture \texttt{oob icon} provided by
+% \textsf{wgexport}.  This picture needs two arguments: the left hand
+% and right hand sides fill colours.  We will use our background
+% colours. 
 %
 %    \begin{macrocode}
 \info{oob-icon}{icon}{}
-\begin{tikzpicture}[scale=.6]
-  \draw[fill=friendly] (.1,0)    rectangle(.5,-.1);
-  \draw[fill=friendly] (.1,-.15) rectangle(.3,-.25);
-  \draw[fill=friendly] (.1,-.3)  rectangle(.2,-.4);
-  \draw[fill=hostile]  (-.1,0)   rectangle(-.3,-.25);
+\begin{tikzpicture}[transform shape,scale=.4]
+  \pic{oob icon={a-bg}{b-bg}};
 \end{tikzpicture}
 %    \end{macrocode}
-%
-% This looks like
-% \tikz{
-%   \draw[fill=friendly] (.1,0)    rectangle(.5,-.1);
-%   \draw[fill=friendly] (.1,-.15) rectangle(.3,-.25);
-%   \draw[fill=friendly] (.1,-.3)  rectangle(.2,-.4);
-%   \draw[fill=hostile]  (-.1,0)   rectangle(-.3,-.25);}
 %   
-% Normally the \texttt{wgexport.py} script used the \emph{undo} mage
+% Normally the \texttt{wgexport.py} script uses the \emph{undo} image
 % for the flip button.  However, that may be a bit confusing, so we
-% will use a large ``F''
-% instead. \tikz{\node[color=blue!75!black,font=\sffamily\bfseries\LARGE]{F}}
+% will use a custom image for that.  We will use the pictures provided
+% by \textsf{wgexport}.
 %
-% And similarly for the ``Eliminate''\tikz{\node[color=red!75!black,font=\sffamily\bfseries\LARGE]{E}}
-% 
-% and ``Restore'' buttons. \tikz{\node[color=green!75!black,font=\sffamily\bfseries\LARGE]{R}}
 %
 %    \begin{macrocode}
 \info{flip-icon}{icon}{}
-\begin{tikzpicture}
-  \node[color=blue!75!black,inner sep=0pt,
-  font=\sffamily\bfseries\large]{F};
+\begin{tikzpicture}[transform shape,scale=.4]
+  \pic{flip icon};
 \end{tikzpicture}
 %
 \info{eliminate-icon}{icon}{}
-\begin{tikzpicture}
-  \node[color=red!75!black,inner sep=0pt,
-  font=\sffamily\bfseries\large]{E};
+\begin{tikzpicture}[transform shape,scale=.4]
+  \pic{eliminate icon};
 \end{tikzpicture}
 %
 \info{restore-icon}{icon}{}
-\begin{tikzpicture}
-  \node[color=green!75!black,inner sep=0pt,
-  font=\sffamily\bfseries\large]{R};
+\begin{tikzpicture}[transform shape,scale=.4]
+  \pic{restore icon};
 \end{tikzpicture}
 %    \end{macrocode}
 %
@@ -262,7 +306,7 @@
 % course Un*x-like syntax.  For other OSs, consult your \TeX{}
 % distribution's documentation for how to find files in the \TeX{}
 % installation.
-
+%
 % You can open the module in the VASSAL editor and see what was
 % generated.  On the board, all areas marked with the \texttt{zone
 % scope} (or \texttt{zone path}) should be defined as zones.  In the
@@ -328,10 +372,9 @@
 % done the heavy work for us. Not least be because we have taken care
 % to add in \texttt{zone} styles where needed. 
 %
-% We will 
-% put all the counters on the OOB chart, and adjust some grids - most
-% notably the OOB and turn track grids.  Other than that, we will not
-% do much.  
+% We will put all the counters on the OOB chart, and adjust some grids
+% - most notably the OOB and turn track grids.  Other than that, we
+% will not do much.
 % 
 % \begin{verbatim}
 % `kpsewhich wgexport.py` export.pdf export.json \
@@ -344,37 +387,61 @@
 % Now for the script:
 %
 % \begin{verbatim}
-% def patch(build,data,vmod,verbose=False):
-%     # We need to import the export module :-)
-%     import wgexport as wg
+% # --- We may need to import the export module ---
+% #
+% # from wgexport import *
 % 
-%     # Get the game and last free GPID
-%     game = wg.get_game(build)
-%     gpid = int(game.getAttribute('nextPieceSlotId'))
+% def patch(build,data,vmod,verbose=False,**kwargs):
+%     # --- Get the game ---
+%     game = build.getGame()
 % 
-%     # Get all boards 
-%     maps = wg.get_maps(build)
+%     # --- Get the maps --- 
+%     maps  = game.getMaps()
+% 
+%     # --- Get the main board ---
 %     board = maps['Board']
-% 
-%     mkeys = wg.get_masskey(board,'name')
-%     wg.set_node_attr(mkeys['Eliminate'],icon='eliminate-icon.png')
-%     wg.set_node_attr(mkeys['Flip'],icon='flip-icon.png')
 %     
-%     # Get main map
+%     # --- Get the mass keys ---
+%     mkeys = board.getMassKeys()
+%     mkeys['Eliminate']['icon'] = 'eliminiate-icon.png'
+%     mkeys['Flip']     ['icon'] = 'flip-icon.png'
+%     
+%     # --- Get the dead-pool map ---
 %     pool = maps['DeadMap']
-%     wg.set_node_attr(pool,icon='pool-icon.png')
+%     pool['icon'] = 'pool-icon.png'
+%     pool.getMassKeys()['Restore']['icon'] = 'restore-icon.png'
 % 
-%     mkeys = wg.get_masskey(pool,'name')
-%     wg.set_node_attr(mkeys['Restore'],icon='restore-icon.png')
-%     
-%     # Get the OOB map
-%     oob = wg.get_chartwindows(game)['OOBs']
-%     wg.set_node_attr(oob,icon='oob-icon.png')
+%     # --- Get the OOB map ---
+%     oob = game.getChartWindows()['OOBs']
+%     oob['icon'] = 'oob-icon.png'
+% 
+% 
+% #
+% # EOF
+% #
 % \end{verbatim}
 %
 % In the \texttt{patch.py} script we can use all the functionality
 % provided by \texttt{wgexport.py}.  Elements are XML elements that we
-% apply \texttt{xml.dom.minidom} operations on. 
+% apply \texttt{xml.dom.minidom} operations on.
+%
+% Note, if we need to use classes, etc.\ from \texttt{wgexport.py},
+% then we ought to import that module into our patch script, as shown
+% in the top comment above.  In the example patch script we do not
+% need that, so we leave it out.
+%
+% One can do quite complicated things in VASSAL, which can be set-up
+% in the patch script.  By default, the \texttt{wgexport} script sets
+% up the pieces and boards, and if one has defined regions with
+% specific piece names, then the script will likewise place the pieces
+% there (as with our \texttt{game turn} example above).
+%
+% One can, for example, setup the module so that the game turn marker
+% automatically flips or progresses when the turn track widget reach
+% specific points.  One can in principle also set it up so that when a
+% specific turn or phase is reach, then pieces are moved from the OOB
+% to the board.  The more one can do like that, the more the game is
+% automated. 
 % 
 % \iffalse
 % Local Variables:

Modified: trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.sty
===================================================================
--- trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.sty	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.sty	2023-03-30 20:06:53 UTC (rev 66713)
@@ -899,7 +899,7 @@
 % \begin{figure}
 %   \centering
 %   \begin{tikzpicture}
-%     \chit[game turn]
+%     \chit[game turn chit]
 %   \end{tikzpicture}
 %   \caption{The \textsf{wargame} game turn counter}
 %   \label{fig:plain-game-turn}
@@ -910,8 +910,8 @@
 %
 %    \begin{macrocode}
 \tikzset{
-  game turn/.append style={a},
-  game turn flipped/.style={game turn,b}
+  game turn chit/.append style={a},
+  game turn chit flipped/.append style={b}
 }
 %    \end{macrocode}
 %
@@ -920,7 +920,7 @@
 % \begin{figure}
 %   \centering
 %   \begin{tikzpicture}
-%     \doublechits{{game turn}}{1}{1.24}
+%     \doublechits{{game turn chit}}{1}{1.24}
 %   \end{tikzpicture}
 %   \caption{Modified game turn, front and back}
 %   \label{fig:game-turn}
@@ -1346,6 +1346,8 @@
     text=titlefg,
     font=\sffamily\bfseries\Huge,
     scale=1.5,
+    inner sep=0pt,
+    outer sep=0pt,
     transform shape,
   },
   sub title/.style={
@@ -1354,6 +1356,8 @@
     scale=1.5,
     transform shape,
     text width=5cm,
+    inner sep=0pt,
+    outer sep=0pt,
     align=right
   }    
 }
@@ -1859,7 +1863,7 @@
     turn,
     transform shape,
     anchor=east,
-    outer sep=5mm,
+    outer sep=2mm,
     scale=.7},
 }
 %    \end{macrocode}
@@ -1921,7 +1925,7 @@
 %
 % \begin{verbatim}
 % \begin{tikzpicture}[oob turn/.append style={anchor=west}]
-%   \oob{\alla}{4}{1.24}{0}
+%   \oob*{\alla}{4}{1.24}{0}
 % \end{tikzpicture}
 % \begin{tikzpicture}
 %   \oob{\allb}{4}{1.24}{0}
@@ -1950,31 +1954,31 @@
   at (0,2.5) {};%
   % Game title
   \node[%
-  below right=5mm and 5mm of oob frame.north west,
-  title,scale=.8]{A Game};
+    below right=5mm and 5mm of oob frame.north west,
+    title,scale=.8]{A Game};
   % Chart title
   \node[%
-  below left=5mm and 5mm of oob frame.north east,
-  sub title,scale=1.2,transform shape]{%
+    below left=5mm and 5mm of oob frame.north east,
+    sub title,scale=1.2,transform shape]{%
     Order of Battle};
   % Turn title
   \node[font=\sffamily\bfseries\Large,
-  white,transform shape]
-  at (0,1) {Turn};
+    anchor=south,
+    white,transform shape] at (0,1) {Turn};
   % 
   \begin{scope}[%
-    transform shape,
-    oob turn/.append style={anchor=west},%
-    shift={(-1.35,0)},
-    zone scope=A OOB]%
+      transform shape,
+      oob turn/.append style={anchor=west},%
+      shift={(-1.4,0)},
+      zone scope=A OOB]%
     \oobemptyturn%
     \oob*{\alla}{4}{1.4}{.2}%
   \end{scope}%
   % 
   \begin{scope}[%
-    transform shape,
-    shift={(1.35,0)},
-    zone scope=B OOB]%
+      transform shape,
+      shift={(1.4,0)},
+      zone scope=B OOB]%
     \oobrealturn%
     \oob{\allb}{4}{1.4}{.2}%
   \end{scope}%

Modified: trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.tex
===================================================================
--- trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.tex	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/doc/latex/wargame/tutorial/game.tex	2023-03-30 20:06:53 UTC (rev 66713)
@@ -3,6 +3,7 @@
 \usepackage{game}
 \usepackage[margin=1cm,bottom=2cm,top=2cm,a4paper]{geometry}
 \newcommand\Tikz{Ti{\itshape k}Z/PGF}
+\let\TikZ\Tikz
 \setlength{\parskip}{.5ex}
 \setlength{\parindent}{0em}
 \newcommand\tightlist{\setlength{\itemsep}{3pt}
@@ -113,8 +114,8 @@
 \end{verbatim}
 
 As a final bonus, we also make a board that includes our three charts:
-The OOB, CRT, and TE. We will not demonstrate the code here, but take
-a look in the sources if you are curious. 
+The OOB, CRT, and TEC. We will not demonstrate the code here, but take
+a look in the sources if you are curious.
 
 \newcommand\fullboard[1][]{%
   \begin{tikzpicture}[#1,rotate=90]%
@@ -126,13 +127,23 @@
       \inneroob
     \end{scope}
     %
-    \node[anchor=south west,minimum width=6.2cm,minimum height=10cm,
-    line  width=1pt,sub title,scale=.66,draw,align=center,
-    text depth=9.7cm,fill=neutral,transform shape]
-    at (oob frame.south west)
+    \node[
+    anchor=south west,
+    minimum width=6.2cm,
+    minimum height=10.5cm,
+    line width=1pt,
+    sub title,
+    scale=.66,
+    draw,
+    align=center,
+    text depth=9.7cm,
+    fill=neutral,
+    transform shape,
+    above right=1pt of oob frame.south west
+    ] (charts)
     {{\LARGE Charts}};
     %
-    \node[above right=5mm and 4mm of oob frame.south west,
+    \node[above right=5mm and 4mm of charts.south west,
     inner sep=0pt,
     transform shape,scale=.75,anchor=south west] (crt) {\crt};
     %
@@ -185,7 +196,7 @@
   \end{tikzpicture}
   
   \begin{tikzpicture}
-    \doublechits{{game turn}}{1}{1.24}
+    \doublechits{{game turn chit}}{1}{1.24}
   \end{tikzpicture}
 \end{center}
 

Modified: trunk/Master/texmf-dist/doc/latex/wargame/tutorial/patch.py
===================================================================
--- trunk/Master/texmf-dist/doc/latex/wargame/tutorial/patch.py	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/doc/latex/wargame/tutorial/patch.py	2023-03-30 20:06:53 UTC (rev 66713)
@@ -1,31 +1,33 @@
-def patch(build,data,vmod,verbose=False):
-    # We need to import the export module :-)
-    import wgexport as wg
+# --- We may need to import the export module ---
+#
+# from wgexport import *
 
-    # Get the game and last free GPID
-    game = wg.get_game(build)
-    gpid = int(game.getAttribute('nextPieceSlotId'))
+def patch(build,data,vmod,verbose=False,**kwargs):
+    # --- Get the game ---
+    game = build.getGame()
 
-    # Get all boards 
-    maps = wg.get_maps(build)
+    # --- Get the maps --- 
+    maps  = game.getMaps()
+
+    # --- Get the main board ---
     board = maps['Board']
-
-    mkeys = wg.get_masskey(board,'name')
-    wg.set_node_attr(mkeys['Eliminate'],icon='eliminate-icon.png')
-    wg.set_node_attr(mkeys['Flip'],icon='flip-icon.png')
     
-    # Get main map
+    # --- Get the mass keys ---
+    mkeys = board.getMassKeys()
+    mkeys['Eliminate']['icon'] = 'eliminate-icon.png'
+    mkeys['Flip']     ['icon'] = 'flip-icon.png'
+    
+    # --- Get the dead-pool map ---
     pool = maps['DeadMap']
-    wg.set_node_attr(pool,icon='pool-icon.png')
+    pool['icon'] = 'pool-icon.png'
+    pool.getMassKeys()['Restore']['icon'] = 'restore-icon.png'
 
-    mkeys = wg.get_masskey(pool,'name')
-    wg.set_node_attr(mkeys['Restore'],icon='restore-icon.png')
-    
-    # Get the OOB map
-    oob = wg.get_chartwindows(game)['OOBs']
-    wg.set_node_attr(oob,icon='oob-icon.png')
-    
-    
+    # --- Get the OOB map ---
+    oob = game.getChartWindows()['OOBs']
+    oob['icon'] = 'oob-icon.png'
 
-   
-    
+
+#
+# EOF
+#
+

Modified: trunk/Master/texmf-dist/doc/latex/wargame/wargame.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/source/latex/wargame/Makefile
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/Makefile	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/Makefile	2023-03-30 20:06:53 UTC (rev 66713)
@@ -2,7 +2,7 @@
 #
 #
 NAME		:= wargame
-VERSION		:= 0.3
+VERSION		:= 0.5
 LATEX_FLAGS	:= -interaction=nonstopmode 	\
 		   -file-line-error		\
 		   --synctex=15			\
@@ -16,8 +16,21 @@
 		   wargame.dtx				\
 		   package.dtx				\
 		   util/core.dtx			\
+		   util/misc.dtx			\
+		   util/compound.dtx			\
+		   util/bb.dtx				\
+		   util/tikz.dtx			\
+		   util/randomid.dtx			\
+		   util/icons.dtx			\
+		   util/export.dtx			\
 		   chit/shape.dtx			\
 		   chit/misc.dtx			\
+		   chit/modifiers.dtx			\
+		   chit/stack.dtx			\
+		   chit/oob.dtx				\
+		   chit/table.dtx			\
+		   chit/battle.dtx			\
+		   chit/dice.dtx			\
 		   chit/elements.dtx			\
 		   chit/core.dtx			\
 		   hex/shape.dtx			\
@@ -42,6 +55,7 @@
 		   hex/terrain/rough.dtx		\
 		   hex/board.dtx			\
 		   hex/coord.dtx			\
+		   hex/split.dtx			\
 		   natoapp6c/shape.dtx			\
 		   natoapp6c/symbols.dtx		\
 		   natoapp6c/list.dtx			\
@@ -105,15 +119,6 @@
 		   wgexport.cls					\
 		   $(TILES_PDF)				
 
-# 		   beach.png					\
-# 		   city.png					\
-# 		   mountains.png					\
-# 		   sea.png    					\
-# 		   woods.png					\
-# 		   light_woods.png  				\
-# 		   rough.png     				\
-# 		   swamp.png					\
-# 		   town.png
 DOC_FILES	:= wargame.pdf symbols.pdf compat.pdf
 
 ifdef VERBOSE	
@@ -130,6 +135,10 @@
 ifdef 		:= $(CI_JOB_ID)
 endif
 
+%.pdf:%.tex
+	@echo "LATEX $< -> $@"
+	$(MUTE)$(LATEX) $(LATEX_FLAGS) $<  $(REDIR)
+
 %.aux:%.tex
 	@echo "LATEX $< -> $@"
 	$(MUTE)$(LATEX) $(LATEX_FLAGS) $<  $(REDIR)
@@ -152,11 +161,7 @@
 	@echo "LATEX $*.tex -> $@ (via $<)"
 	$(MUTE)$(LATEX) $(LATEX_FLAGS) $*.tex  $(REDIR)
 
-%.pdf:%.tex
-	@echo "LATEX $< -> $@"
-	$(MUTE)$(LATEX) $(LATEX_FLAGS) $<  $(REDIR)
 
-
 %.ind:%.idx
 	@echo "INDEX $< -> $@ $<"
 	$(MUTE)$(MAKEINDEX) -s gind -o $@ $<  $(REDIR)
@@ -174,8 +179,9 @@
 everything:	all symbols.pdf compat.pdf tests/test.pdf
 
 tutorial: tutorial/game.pdf
-tutorial/game.pdf: all 
-	$(MUTE)$(MAKE) -C tutorial Game.vmod $(REDIR)
+tutorial/game.pdf: all
+	@echo "MAKE -C tutorial Game.vmod"
+	$(MUTE)$(MAKE) -C tutorial Game.vmod
 
 clean:
 	@echo "CLEAN"
@@ -198,6 +204,7 @@
 	$(MUTE)rm -rf __pycache__
 	$(MUTE)rm -rf $(ctandir) tmp
 	$(MUTE)rm -f  ctan.tex $(NAME).ctan.tar.gz
+	$(MUTE)$(MAKE) -C tutorial clean 
 
 
 
@@ -220,7 +227,9 @@
 wargame.pdf:	wargame.aux wargame.ind
 fast:		wargame.idx
 symbols.aux:	symbols.tex wargame.sty
+symbols.pdf:	symbols.aux
 compat.aux:	compat.tex $(TABLES:%=cmp_%.tex) wargame.sty
+compat.pdf:	compat.aux
 test.aux:	test.tex wargame.sty
 
 beach.pdf:	beach.tex
@@ -269,11 +278,11 @@
 
 
 logo.png:logo.pdf
-	pdftocairo -png $<
+	pdftocairo -transp -png $<
 	mv logo-1.png logo.png
 
 README.md.version:README.md
-	@echo "Versioned README.md"
+	@echo "Versioned README.md $(VERSION)"
 	$(MUTE)$(SED) 's/^# \(.*\)/# \1\n## Version $(VERSION)/' \
 		< $< | pandoc --to markdown > $@
 
@@ -289,6 +298,7 @@
 	$(MUTE)mkdir -p $(ctandir)/$(NAME)
 	$(MUTE)cp tmp/$(docdir)/README.md $(ctandir)/$(NAME)/
 	$(MUTE)(cd tmp && zip -q -r ../$(ctandir)/$(NAME).tds.zip *)
+	$(MUTE)rm tmp/$(docdir)/README.md
 	$(MUTE)(cd tmp && cp -a $(docdir) ../$(ctandir)/$(NAME)/doc)
 	$(MUTE)(cd tmp && cp -a $(srcdir) ../$(ctandir)/$(NAME)/source)
 	$(MUTE) rm -rf tmp

Added: trunk/Master/texmf-dist/source/latex/wargame/chit/battle.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/battle.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/battle.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,77 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Battle markers}
+%
+% Takes 1 arguments - the identifier.
+% 
+% Define \spec{every battle marker} to change the style. 
+% 
+%    \begin{macrocode}
+\tikzset{%
+  battle marker/.pic={
+    \node[shape=circle,
+    font=\sffamily\bfseries,
+    inner sep=0pt,
+    minimum size=5mm,
+    draw=black,
+    fill=yellow!85!black,
+    every battle marker/.try] at (-.3,.3) {%
+      \ifnum#1>0\relax #1\fi%
+    };
+  },
+  battle marker/.style={
+    chit={full={battle marker=#1},frame={draw=none}}},
+}
+%    \end{macrocode}
+%
+% Takes two arguments - the odds and the fill colour.  The latter is
+% useful to differentiate the severity of an attack.
+%
+% Define \spec{every odds marker} to change the style. 
+%
+%    \begin{macrocode}
+\tikzset{%
+  pics/odds marker/.style args={#1,#2}{
+    code={
+      \node[shape=circle,
+      font=\sffamily\bfseries\large,
+      inner sep=0pt,
+      minimum size=8mm,
+      draw=black,
+      fill=#2,
+      every odds marker/.try] at (.2,-.2) {#1};
+    }
+  },
+  odds marker/.style args={#1,#2}{
+    chit={full={odds marker={#1,#2}},frame={draw=none}}},
+}
+%    \end{macrocode}
+%
+% Takes two arguments - the result and the fill colour.  The latter is
+% useful to differentiate the severity of an attack.
+%
+% Define \spec{every result marker} to change the style. 
+%
+%    \begin{macrocode}
+\tikzset{
+  pics/result marker/.style args={#1,#2}{
+    code={
+      \message{^^JResults marker #1 (#2)}
+      \node[shape=circle,
+      font=\sffamily\bfseries\large,
+      inner sep=0pt,
+      minimum size=8mm,
+      draw=black,
+      fill=#2,
+      every result marker/.try] at (0,0) {#1};}},
+  result marker/.style args={#1,#2}{
+    chit={full={result marker={#1,#2}},frame={draw=none}}}
+}
+%    \end{macrocode}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/chit/battle.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/latex/wargame/chit/core.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/core.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/core.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -39,8 +39,14 @@
 %
 % \input{chit/shape.dtx}
 % \input{chit/elements.dtx}
+% \input{chit/modifiers.dtx}
+% \input{chit/stack.dtx}
+% \input{chit/oob.dtx}
+% \input{chit/table.dtx}
+% \input{chit/battle.dtx}
+% \input{chit/dice.dtx}
 % \input{chit/misc.dtx}
-% 
+%
 % \iffalse
 %</chit>
 % --------------------------------------------------------------------

Added: trunk/Master/texmf-dist/source/latex/wargame/chit/dice.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/dice.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/dice.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,286 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Dice}
+%
+% First, a regular 6-sided dice with configurable number of dots. Use
+% like 
+%
+%   \begin{Syntax}
+%     \cs{pic}\oarg{pic options}\{dice=\meta{eyes}\}
+%   \end{Syntax}
+% 
+% For example:
+%
+% \begin{enumerate}
+% \item \tikz{\pic[transform shape]{dice=1};}
+% \item \tikz{\pic[transform shape]{dice=2};}
+% \item \tikz{\pic[transform shape]{dice=3};}
+% \item \tikz{\pic[transform shape]{dice=4};}
+% \item \tikz{\pic[transform shape]{dice=5};}
+% \item \tikz{\pic[transform shape]{dice=6};}
+% \end{enumerate}
+%
+%    \begin{macrocode}
+\tikzset{
+  dice bg/.style={
+    % /utils/exec={
+    %   \pgfgettransformentries{%
+    %     \wg at jaca}{%
+    %     \wg at jacb}{%
+    %     \wg at jacc}{%
+    %     \wg at jacd}{%
+    %     \wg at tmp}{%
+    %     \wg at tmp}%
+    %   \pgfmathsetmacro{\wg at tmp}{%
+    %     sqrt(abs(\wg at jaca*\wg at jacd-\wg at jacb*\wg at jacc))}
+    %   \xdef\wg at tmp{\wg at tmp}},%
+    fill=black,
+    draw=none,
+    minimum width=1cm,
+    minimum height=1cm,
+    scale rounded corners,
+    rounded corners=.1cm,
+    inner sep=0pt,
+    transform shape},
+  dice fg/.style={
+    fill=white,
+    shape=circle,
+    inner sep=0pt,
+    minimum size=.2cm,
+    transform shape},
+  pics/dice/.style={
+    code={
+      \node[dice bg] (dice bg) {};
+      \ifodd#1\node[dice fg] at (dice bg) {};\fi
+      \ifnum#1>1%
+      \node[dice fg] at ($(dice bg)+(-45:.4)$){};%
+      \node[dice fg] at ($(dice bg)+(135:.4)$){};%
+      \fi%
+      \ifnum#1>3%
+      \node[dice fg] at ($(dice bg)+(  45:.4)$){};%
+      \node[dice fg] at ($(dice bg)+(-135:.4)$){};%
+      \fi%
+      \ifnum#1=6%  
+      \node[dice fg] at ($(dice bg)+(-.282,0)$){};
+      \node[dice fg] at ($(dice bg)+( .282,0)$){};
+      \fi
+    }
+  },
+  pics/dice/.default=3
+}
+\newcommand\dicemark[2][scale=.5]{%
+  \tikz[baseline={($(dice bg.south east)!.25!(dice bg.north east)$)},#1]{
+    \pic[transform shape]{dice=3};}}
+%    \end{macrocode}
+% 
+% Now some shapes of different dice.  This was originally done by
+% \href{https://tex.stackexchange.com/questions/144193/how-do-i-put-a-die-roll-at-the-top-of-each-page#answer-144474}{David
+% Carlisle}.  Usage is for example
+%
+% \begin{Syntax}
+%   \cs{node}[shape=\meta{dice},\meta{node options}] \marg{value};
+% \end{Syntax}
+%
+% where \meta{dice} is one of \texttt{d4}, \texttt{d6}, \texttt{d8},
+% \texttt{d10}, \texttt{d12}, or \texttt{d20}. 
+%
+% \def\DiceExample#1{%
+%   \begin{tikzpicture}[
+%     die/.style={
+%       shape=#1,
+%       scale line widths,
+%       line width=1pt,
+%       transform shape,
+%       scale=.7
+%     },
+%     fg die/.style={
+%       die,
+%       fill=##1,
+%       draw=gray!25!white,
+%       text=white},
+%     die shadow/.style={
+%       die,
+%       fill=black,
+%       draw=none,
+%       opacity=.5}]
+%     \node[die shadow,rotate=10] at (.20,.07){};
+%     \node[fg die=red,rotate=10] at (.15,.10){};
+%     \node[die shadow,rotate=-10] at (-.08,-.11){};
+%     \node[fg die=blue,rotate=-10] at (-.15,-.1){};
+%     \draw(-.5,-.5)rectangle(.6,.5);
+%   \end{tikzpicture}
+% }
+%
+% \paragraph{Tetrahedron}
+% 
+% \DiceExample{d4}
+%
+%    \begin{macrocode}
+\pgfdeclareshape{d4}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.4cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{-.35cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{-.35cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.4cm}}
+    % \pgfusepath{draw}  %draw border
+    % \pgfusepath{draw}  %draw rectangle
+  }}
+%    \end{macrocode}
+%    
+% \paragraph{Cubic}
+% 
+% \DiceExample{d6}
+%
+%    \begin{macrocode}
+\pgfdeclareshape{d6}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathrectanglecorners{\pgfpoint{.4cm}{.4cm}}{\pgfpoint{-.4cm}{-.4cm}}
+    % \pgfusepath{draw}  %draw rectangle
+  }}
+%    \end{macrocode}
+%    
+% \paragraph{Octahedron}
+%
+% \DiceExample{d8}
+% 
+%    \begin{macrocode}
+\pgfdeclareshape{d8}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{.25cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{.25cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+%    \end{macrocode}
+%    
+% \paragraph{Decahedron}
+%
+% \DiceExample{d10}
+% 
+%    \begin{macrocode}
+\pgfdeclareshape{d10}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.294cm}{-.154cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.3cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{.1cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{-.1cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{.1cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathmoveto{\pgfpoint{.294cm}{-.154cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}}
+    \pgfpathmoveto{\pgfpoint{-.475cm}{-.1cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}}
+    \pgfpathmoveto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.3cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+%    \end{macrocode}
+%    
+% \paragraph{Dodecahedron}
+%
+% \DiceExample{d12}
+% 
+%    \begin{macrocode}
+\pgfdeclareshape{d12}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{   % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{0.294cm}{.405cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{-.173cm}}
+    \pgfpathlineto{\pgfpoint{.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{-.173cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{.405cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.349cm}}
+    \pgfpathlineto{\pgfpoint{.332cm}{.108cm}}
+    \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}}
+    \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}}
+    \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.349cm}}
+    \pgfpathmoveto{\pgfpoint{.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{.332cm}{.108cm}}
+    \pgfpathmoveto{\pgfpoint{.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}}
+    \pgfpathmoveto{\pgfpoint{-.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}}
+    \pgfpathmoveto{\pgfpoint{-.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+%    \end{macrocode}
+%    
+% \paragraph{Icosohedron}
+%
+% \DiceExample{d20}
+% 
+%    \begin{macrocode}
+\pgfdeclareshape{d20}{
+  \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+  \anchor{text}{    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+    \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+    \pgfpathlineto{\pgfpoint{.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+    \pgfpathmoveto{\pgfpoint{.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+    \pgfpathmoveto{\pgfpoint{-.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+%    \end{macrocode}
+% 
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/chit/dice.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/latex/wargame/chit/elements.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/elements.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/elements.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -5,6 +5,12 @@
 % 
 % \subsubsection{Predefined \texttt{chit} element pictures}
 %
+%    \begin{macrocode}
+\DeclareRobustCommand\chit at sep[2][/]{%
+  \foreach[count=\is] \s in {#2}{%
+    \ifnum\is>1\relax#1\fi%
+    \s}}
+%    \end{macrocode}
 % \begin{TikzKey}{/tikz/pics/chit/1 factor,
 %   /tikz/pics/chit/2 factors,
 %   /tikz/pics/chit/2 factors artillery,
@@ -28,24 +34,32 @@
   pics/chit/2 factors artillery/.style args={#1,#2,#3}{%
     code={
       \chit at dbg{4}{ Chit 2 factors w/artillery: `#1' `#2' `#3'}% 
-      \node[chit/factor,chit/2 factors,pic actions]{%
-        #1$\overset{\text{\scriptsize #3}}{\text{--}}$#2};}},
+      \node[chit/factor,chit/2 factors]{%
+        {#1}$\overset{\text{\scriptsize #3}}{\text{--}}${#2}};}},
   pics/chit/3 factors/.style args={#1,#2,#3}{%
     code={
       \chit at dbg{4}{ Chit 3 factors: `#1' `#2' `#3'}% 
-      \node[chit/factor,chit/3 factors,pic actions]{#1-#2-#3};}},
+      \node[chit/factor,chit/3 factors]{#1-#2-#3};}},
   pics/chit/4 factors/.style args={#1,#2,#3,#4}{%
     code={
       \chit at dbg{4}{ Chit 3 factors: `#1' `#2' `#3' `#4'}% 
-      \node[chit/factor,chit/4 factors,pic actions]{#1-#2-#3-#4};}},
+      \node[chit/factor,chit/4 factors]{#1-#2-#3-#4};}},
   chit/identifier/.pic={
     \chit at dbg{4}{  Chit identifier: `#1'}% 
     \node[chit/identifier,pic actions]{#1};
   },
+  chit/identifiers/.pic={
+    \chit at dbg{4}{  Chit identifiers: `#1'}% 
+    \node[chit/identifier,pic actions]{\chit at sep{#1}};
+  },
   chit/small identifier/.pic={
     \chit at dbg{4}{  Chit small identifier: `#1'}% 
     \node[chit/small identifier,pic actions]{#1};
   },
+  chit/small identifiers/.pic={
+    \chit at dbg{4}{  Chit small identifiers: `#1'}% 
+    \node[chit/small identifier,pic actions]{\chit at sep{#1}};
+  },
   chit/identifier macro/.pic={%
     \chit at dbg{4}{ Chit identifier macro: \meaning#1}
     \edef\chit at i@tmp{#1}
@@ -69,32 +83,35 @@
 \tikzset{%
   chit/factor/.style={
     shape=rectangle,
-    font=\sffamily\bfseries\large,
+    font=\sffamily\bfseries\fontsize{12}{14}\selectfont,
     anchor=base,
     inner sep=0,
     %text=pgfstrokecolor,
     draw=none,
     fill=none,
+    transform shape,
   },
   chit/1 factor/.style={},
   chit/2 factors/.style={},
   chit/3 factors/.style={},
-  chit/4 factors/.style={text/.append style=\small},
+  chit/4 factors/.style={text/.append style=\fontsize{10}{12}\selectfont},
   chit/identifier/.style={
     shape=rectangle,
-    font=\sffamily\bfseries\scriptsize,
+    font=\sffamily\bfseries\fontsize{8}{9}\selectfont,
     inner sep=0,
     % text=pgfstrokecolor,
     draw=none,
     fill=none,
+    transform shape,
   },
   chit/small identifier/.style={
     shape=rectangle,
-    font=\sffamily\bfseries\tiny,
+    font=\sffamily\bfseries\fontsize{6}{7}\selectfont,
     inner sep=0,
     % text=pgfstrokecolor,
     draw=none,
     fill=none,
+    transform shape,
   },
 }
 %    \end{macrocode}

Modified: trunk/Master/texmf-dist/source/latex/wargame/chit/misc.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/misc.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/misc.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -1,393 +1,7 @@
 % \iffalse
+% <*chit>
 % --------------------------------------------------------------------
-%<*chit>
 % \fi
-% \subsubsection{Other pictures}
-% Pictures for frame, factors, left, right, and below. 
-%    \begin{macrocode}
-\tikzset{
-  pics/chit/shade/.style={
-    code={%
-      \path[fill=white,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
-  pics/chit/eliminate/.style={
-    code={%
-      \path[fill=red,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
-  pics/chit/shade/.default=0.5,
-  pics/chit/eliminate/.default=0.25,
-}
-\def\shadechit(#1){%
-  \pic at (#1) {chit/shade};}
-\def\eliminatechit(#1){%
-  \pic at (#1) {chit/eliminate};}
-%    \end{macrocode}
-% 
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% \subsubsection{Stacking of chits}
-%
-% Stacking of chits.  The key \spec{chit/stack direction} sets the
-% default direction to make the stack in.
-% 
-%    \begin{macrocode}
-% offset, location, direction, list
-\tikzset{%
-  chit/stack direction/.store in=\chit at stack@dir,
-  chit/stack direction/.initial={(.3,.3)},
-}
-%    \end{macrocode}
-%
-% Now the code 
-%
-%    \begin{macrocode}
-\def\chit at stack@dir{(.3,.3)}
-\def\stackchits(#1){%
-  \@ifnextchar({\st at ckchits{#1}}{\st at ckchits{#1}(.3,.3)}%)
-}
-\def\st at ckchits#1(#2)#3{
-  \chit at dbg{2}{Stacking chits `#1', `#2', `#3'}
-  \edef\xy{#1}
-  \chit at dbg{4}{Stack start at \xy}
-  \foreach[count=\i from 0] \c/\o in {#3} {%
-    \ifx\c\empty\else%
-      \edef\ccc{\c}
-      \chit at dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'}
-      \expandafter\ccc(\xy)
-      %%
-      \ifx\c\o\else
-        %\chit at dbg{0}{Option: \o}
-        \edef\ccc{\o}
-        \expandafter\ccc(\xy)
-      \fi
-      \expandafter\ccc(\xy)
-      \tikzmath{%
-        coordinate \cc;
-        \cc = (\xy) + (#2);}
-      \xdef\xy{\cc}
-    \fi
-  }
-}
-%    \end{macrocode}
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% \subsubsection{Making order of battle charts}
-% 
-%
-% Macros for making OOBs
-%
-% Style for turns
-%
-%    \begin{macrocode}
-\tikzset{
-  chit/oob turn/.pic={\node[pic actions]{#1};}}
-%    \end{macrocode}
-% 
-% 
-% current c, current r, n-columns, cell size, y
-% 
-%    \begin{macrocode}
-\def\chit at oob@cellupdate(#1,#2)#3#4#5{%
-  \edef\f{\ifwg at oob@inv-1\else1\fi}%
-  \chit at dbg{1}{ \space Cell update `c=|#1|' vs `#4'*(`#3'-1)}
-  \pgfmathparse{int(ifthenelse(abs(#1)>=#4*(#3-1),#5-1,#5))}%
-  \xdef#5{\pgfmathresult}
-  \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),#2-#4,#2)}%
-  \xdef#2{\pgfmathresult}%
-  \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),0,#1+\f*#4)}%
-  \xdef#1{\pgfmathresult}%
-  \chit at dbg{1}{ \space\space-> `\string#5'=#5 `\string#2'=#2 `\string#1'=#1}
-}
-%    \end{macrocode}
-%
-% current c, current r, cell size, extra vertical spacing
-%
-%    \begin{macrocode}
-\def\chit at oob@rowupdate(#1,#2)#3#4{%
-  \chit at dbg{2}{ Row update  c=`#1',r=`#2',s=`#3',e=`#4'}
-  %\pgfmathparse{ifthenelse(#1>0,#2-#3,#2)}%
-  \pgfmathparse{#2-#3)}%
-  \xdef#2{\pgfmathresult}%
-  %\xdef#1{0}
-  \chit at dbg{2}{ \space\space-> update `\string#2'=#2}
-}
-%    \end{macrocode}
-% current c, current r,  cell size, extra spacing
-%
-%    \begin{macrocode}
-\def\chit at oob@turnupdate(#1,#2)#3#4{%
-  \chit at dbg{2}{ Turn update c=`#1',r=`#2',s=`#3',e=`#4'}
-  % \pgfmathparse{#2-ifthenelse(#1>0,#3,0)-#4}%
-  \pgfmathparse{#2-#4-ifthenelse(abs(#1)>0.0001,#3,0)} 
-  \xdef#2{\pgfmathresult}%
-  \xdef#1{0}%
-  \chit at dbg{2}{ \space\space-> update `\string#1'=#1,`\string#2'=#2}
-}
-%    \end{macrocode}
-% chit list, n-colls, cell size, extra vertical spacing
-%
-% This expects a list of lists of chits, one list per turn; the
-% maximum number of columns; the size of cells, extra spacing between
-% turns.
-%
-% Note, the list of lists leaf elements should be styles for the
-% chits. 
-%
-% This depends on the Tikz pic \texttt{chit/oob~turn} which takes the
-% number as argument.
-%
-% 
-%    \begin{macrocode}
-\newif\ifwg at oob@inv\wg at oob@invfalse
-\def\wg at star@oob{\wg at oob@invtrue\wg at oob}
-\def\wg at nostar@oob{\wg at oob@invfalse\wg at oob}
-\def\oob{%
-  \@ifstar{\wg at star@oob%
-  }{\wg at nostar@oob%
-  }%
-}
-\def\wg at oob#1#2#3#4{
-  \def\r{0}
-  \chit at dbg{1}{OOB: `#1'}
-  \foreach[count=\ti from 0] \t/\y in #1{
-    \xdef\o{\r}
-    \def\c{0}
-    \ifx\t\y\def\y{0}\fi
-    \chit at dbg{1}{Turn \ti\space(\r,\t,y=\y):'}
-    \ifwg at oob@inv%
-      \pic at ( .5,\r) {chit/oob turn=\ti};%
-    \else
-      \pic at (-.5,\r) {chit/oob turn=\ti};%
-    \fi%
-    \ifx\t\empty\else%
-      \foreach \u/\m in \t{
-        %% \chit at dbg{2}{ `\u'=`\m'}
-        \ifx\u\empty\else
-          \ifx\m\@empty\def\m{1}\fi
-          \ifx\u\m\def\m{1}\fi
-          \foreach \n in {1,...,\m}{%
-            \ifx\u\chit at blank\else
-              \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r);
-            \fi
-            \chit at oob@cellupdate(\c,\r){#2}{#3}{\y}
-          }
-        \fi
-      }
-    \fi
-    \chit at dbg{1}{ End of chits in turn
-      \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
-    % IF no units where given, then we force \c to be non-zero so that
-    % \chit at oob@turnupdate increments the row 
-    \ifx\t\@empty
-      \def\c{#3}
-      \chit at dbg{2}{ Turn is empty, set c=`\c'}
-    \fi
-    %\ifnum\y<0% No explicit number of rows given
-    %  \def\c{#3}
-    %  \chit at dbg{2}{ No explicit number of rows given, set c=`\c'}
-    %\fi
-    % In case the user gave and explicit number of rows, add the rows
-    % that are missing.  \y is initially set to the number of
-    % requested rows, and then decremented every time we go down one
-    % row.  So if the number of rows we did so far is N, and the
-    % requested number of rows is M, then the loop below adds M-N
-    % rows.
-    \ifnum\y>0%
-      \chit at dbg{2}{ Looping rows from 2 to \y, break when row > \y}%
-      \foreach \rr  in {2,...,\y}{
-        %\ifnum\rr>\y% A little funny, but \y can be negative!
-        %  \chit at dbg{2}{ \space Breaking loop \rr\space > \y}%
-        %  \breakforeach%
-        %\else%
-          \chit at oob@rowupdate(\c,\r){#3}{#4}
-        %\fi
-      }
-    \fi
-    % This will zero \c.  However, if on entry |\c|>0, then we also
-    % increment the row 
-    \chit at oob@turnupdate(\c,\r){#3}{#4}
-    \chit at dbg{1}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')} 
-  }
-  \chit at dbg{2}{End of OOB (c=`\c',r=`\r',y=`\y')}
-}
-%    \end{macrocode}
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% \subsubsection{Table of chits}
-%
-%    \begin{macrocode}
-\tikzset{
-  chit/cell background/.style={fill=black},
-  blank chit/.style={/chit/frame={draw=none,fill=none}},
-}
-%    \end{macrocode}
-% 
-% These macros are used when we set tables of chits.  This allows us
-% to define blank spaces in the table by giving the element
-% \texttt{blank~chit}.
-% 
-%    \begin{macrocode}
-\def\chit at blank{blank chit}
-\def\chit at cellbg(#1,#2)#3{%
-  \draw[chit/cell background](#1-#3/2,#2-#3/2) rectangle++(#3,#3);
-}
-%    \end{macrocode}
-% 
-% \begin{Macro}{\ifchits at reset}
-%
-%   This `if' controls whether to reset the coordinates to the origin
-%   when \cs{chits} is called.  If true, then reset for a new table.
-%   
-%    \begin{macrocode}
-\newif\ifchits at reset\chits at resettrue
-%    \end{macrocode}
-% \end{Macro}
-% 
-% \begin{Macro}{\chits,\@chits,\chit at sng@cellupdate}
-%    \begin{macrocode}
-\def\chit at sng@cellupdate(#1,#2)#3#4{%
-  \chit at dbg{2}{Current `#1' vs `#4'*(`#3'+1)}
-  \pgfmathparse{ifthenelse(#1>=#4*(#3-1),#2-#4,#2)}%
-  \xdef#2{\pgfmathresult}%
-  \pgfmathparse{ifthenelse(#1>=#4*(#3-1),0,#1+#4)}%
-  \xdef#1{\pgfmathresult}%
-}
-%    \end{macrocode}
-%
-% The stared version (\cs{chits*}) of this macro continues the
-% previously set chit table.
-% 
-%    \begin{macrocode}
-\def\chits{%
-  \@ifstar{\chits at resetfalse\@chits}{\chits at resettrue\@chits}}
-%    \end{macrocode}
-% 
-%    \begin{macrocode}
-\def\@chits#1#2#3{
-  \ifchits at reset
-    \def\r{0}%
-    \def\c{0}%
-  \fi
-  \chit at dbg{1}{Chits to make: #1}%
-  \foreach[count=\ti from 0] \t/\x in #1{%
-    \chit at dbg{2}{Turn `\t' with option `\x'}
-    \ifx\t\empty\else%
-      \foreach \u/\m in \t{%
-        \ifx\u\empty\else%
-          \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
-          \ifx\m\@empty\def\m{1}\fi%
-          \ifx\u\m\def\m{1}\fi%
-          \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
-          \foreach \n in {1,...,\m}{%
-            \ifx\u\chit at blank%
-              \chit at dbg{3}{Ignoring blank chit:\u}%
-            \else%
-              \chit at cellbg(\c,\r){#3}%
-              \chit[\u=\ti](\c,\r)%
-              \chit at sng@cellupdate(\c,\r){#2}{#3}%
-            \fi%
-          }%
-        \fi%
-      }%
-    \fi%
-  }%
-}
-%    \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\doublechits,
-%   \@doublechits,
-%   \chit at dbl@cellupdate,
-%   \chit at dbl@flip}
-% \begin{enumerate}
-% \item coordinates
-% \item coordinates
-% \item cell-size
-% \end{enumerate}
-% 
-%    \begin{macrocode}
-\def\chit at dbl@flip(#1,#2)#3{%
-  \pgfmathparse{-#1}%
-  \xdef\mc{\pgfmathresult}%
-}
-%    \end{macrocode}
-% 
-%   \begin{enumerate}
-%   \item coordinates
-%   \item coordinates
-%   \item Number of columns
-%   \item cell-size
-%   \end{enumerate}
-%
-%    \begin{macrocode}
-\def\chit at dbl@cellupdate(#1,#2)#3#4{%
-  \pgfmathparse{ifthenelse(#1<-#4/2,#2,#4+#2)}%
-  \xdef#2{\pgfmathresult}%
-  \pgfmathparse{ifthenelse(#1<-#4/2,#4+#1,-(#3-.5)*#4)}%
-  \xdef#1{\pgfmathresult}%
-}
-%    \end{macrocode}
-%
-%   \begin{enumerate}
-%   \item List of list of keys
-%   \item Number of columns
-%   \item size of each cell
-%   \end{enumerate}
-% 
-%   The stared version (\cs{doublechits*}) of this macro continues the
-%   previously set chit table.
-% 
-%    \begin{macrocode}
-\def\doublechits{%
-  \@ifstar{\chits at resetfalse\@doublechits}{\chits at resettrue\@doublechits}}
-%    \end{macrocode}
-%
-%    \begin{macrocode}
-\def\@doublechits#1#2#3{%
-  \chit at dbg{1}{Setting double-sided chits: #1}
-  \ifchits at reset
-    \pgfmathparse{-(#2-.5)*#3}
-    \xdef\c{\pgfmathresult}
-    \def\r{0}
-  \fi
-  
-  \foreach[count=\ti from 0] \t/\x in #1{
-    \ifx\t\empty\else%
-      \foreach \u/\m in \t{
-        \ifx\u\empty\else
-          \ifx\m\@empty\def\m{1}\else%
-            \ifx\u\m\def\m{1}\fi\fi
-          \chit at dbg{2}{`\u'=`\m' (\c,\r)}
-          \foreach \n in {1,...,\m}{%
-            \ifx\u\chit at blank
-              \chit at dbg{3}{Ignoring blank chit:\u}
-            \else
-              \chit at cellbg(\c,\r){#3}
-              \chit[\u=\ti](\c,\r)
-              \chit at dbl@flip(\c,\r){#3}
-              \chit at cellbg(\mc,\r){#3}
-              \chit[\u\space flipped=\ti,zone turn=\t,zone mult=\n](\mc,\r)
-              \chit at dbl@cellupdate(\c,\r){#2}{#3}
-            \fi
-          }
-        \fi
-      }
-    \fi
-  }
-  \draw[dashed](0,-3*#3/4)--(0,\r-#3/4);
-  \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[anchor=west]{Back};
-  \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[anchor=east]{Front};
-}  
-%    \end{macrocode}
-% \end{Macro}
-%    
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
 % \subsubsection{Some utilities}
 %
 %
@@ -395,17 +9,32 @@
 %
 %    \begin{macrocode}
 \tikzset{
-  number chit/.pic={
-    \node[shape=rectangle,font=\sffamily\bfseries\LARGE]{%
-      \begin{tabular}{c} #1\end{tabular}};},
-  game turn/.pic={
-    \node[shape=rectangle,font=\sffamily\bfseries]{%
-      \begin{tabular}{c} Game\\Turn\end{tabular}};},
-  game turn/.style={
-    /chit/full={game turn},
+  chit/text base/.style={
+    shape=rectangle,
+    inner sep=0pt,
+    align=center,
+    text width=1.1cm},
+  chit/number/.style={
+    chit/text base,
+    font=\sffamily\bfseries\fontsize{12}{14}\selectfont},
+  chit/game turn/.style={
+    chit/text base,
+    font=\sffamily\bfseries},
+  chit/text/.style={
+    chit/text base,
+    font=\sffamily\bfseries},
+  chit/small text/.style={
+    chit/text base,
+    font=\sffamily\bfseries\fontsize{9}{10}\selectfont},
+  chit/number/.pic={\node[chit/number]{#1};},
+  chit/game turn/.pic={\node[chit/game turn]{Game\\Turn};},
+  chit/text/.pic={\node[chit/text]{#1};},
+  chit/small text/.pic={\node[chit/small text]{#1};},
+  game turn chit/.style={
+    /chit/full={chit/game turn},
     color=black,
     fill=white},
-  game turn flipped/.style={game turn},
+  game turn chit flipped/.style={game turn chit},
   dummy chit/.style={fill=white},
 }
 %    \end{macrocode}
@@ -435,7 +64,7 @@
 %
 %    \begin{macrocode}
 \DeclareRobustCommand\zocmark[1][]{%
-  \tikz[baseline=(current bounding box.center),scale=.1,#1]{%
+  \tikz[baseline=($(current bounding box.center)!.5!(current bounding box.south)$),scale=.1,#1]{%
     \begin{scope}[hex/first row and column are=0,
       hex/row direction is=normal,
       hex/column direction is=normal,
@@ -450,6 +79,10 @@
     \end{scope}}}
 %    \end{macrocode}
 %
+% Dummy implementations of zones hooks when exporting.  Here, these do
+% nothing, but in the \textsf{wgexport} class these are
+% re-implemented.
+% 
 %    \begin{macrocode}
 \tikzset{
   zone point/.code n args={3}{},
@@ -457,5 +90,5 @@
 %    \end{macrocode}
 %
 % \iffalse
-%</chit>
+% </chit>
 % \fi

Added: trunk/Master/texmf-dist/source/latex/wargame/chit/modifiers.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/modifiers.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/modifiers.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,42 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*chit>
+% \fi
+% \subsubsection{Modifications to chits}
+%
+% These defines overlays one can add on top of chits, for example to
+% shade a chit, put a semi-transparent red cover to indicate
+% elimination, and similar. 
+%
+%    \begin{macrocode}
+\tikzset{
+  pics/chit/shade/.style={
+    code={%
+      \path[fill=white,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
+  pics/chit/eliminate/.style={
+    code={%
+      \path[fill=red,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
+  pics/chit/shade/.default=0.5,
+  pics/chit/eliminate/.default=0.25,
+  dummy chit/.style={draw=none,fill=none,chit={}},
+}
+\def\shadechit{%
+  \@ifnextchar[{\sh at dechit}{\sh at dechit[.5]}%]
+}
+\def\eliminatechit{%
+  \@ifnextchar[{\elimin at techit}{\elimin at techit[.25]}%]
+}
+\def\sh at dechit[#1](#2){%
+  % \message{^^JShading chit with opacity `#1'}%
+  \pic[transform shape] at (#2) {chit/shade=#1};%
+  \@ifnextchar;{\@gobble}{}}
+\def\elimin at techit[#1](#2){%
+  \pic[transform shape] at (#2) {chit/eliminate=#1};%
+  \@ifnextchar;{\@gobble}{}}
+%    \end{macrocode}
+% 
+%
+% \iffalse
+% --------------------------------------------------------------------
+% </chit>
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/chit/modifiers.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/wargame/chit/oob.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/oob.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/oob.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,172 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Making order of battle charts}
+% 
+%
+% Macros for making OOBs
+%
+% Style for turns
+%
+%    \begin{macrocode}
+\tikzset{
+  chit/oob turn/.pic={\node[pic actions]{#1};}}
+%    \end{macrocode}
+% 
+% 
+% current c, current r, n-columns, cell size, y
+% 
+%    \begin{macrocode}
+\def\chit at oob@cellupdate(#1,#2)#3#4#5{%
+  \edef\f{\ifwg at oob@inv-1\else1\fi}%
+  \chit at dbg{1}{ \space Cell update `c=|#1|' vs `#4'*(`#3'-1)}
+  \pgfmathparse{int(ifthenelse(abs(#1)>=#4*(#3-1),#5-1,#5))}%
+  \xdef#5{\pgfmathresult}
+  \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),#2-#4,#2)}%
+  \xdef#2{\pgfmathresult}%
+  \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),0,#1+\f*#4)}%
+  \xdef#1{\pgfmathresult}%
+  \chit at dbg{1}{ \space\space-> `\string#5'=#5 `\string#2'=#2 `\string#1'=#1}
+}
+%    \end{macrocode}
+%
+% current c, current r, cell size, extra vertical spacing
+%
+%    \begin{macrocode}
+\def\chit at oob@rowupdate(#1,#2)#3#4{%
+  \chit at dbg{2}{ Row update  c=`#1',r=`#2',s=`#3',e=`#4'}
+  %\pgfmathparse{ifthenelse(#1>0,#2-#3,#2)}%
+  \pgfmathparse{#2-#3)}%
+  \xdef#2{\pgfmathresult}%
+  %\xdef#1{0}
+  \chit at dbg{2}{ \space\space-> update `\string#2'=#2}
+}
+%    \end{macrocode}
+% current c, current r,  cell size, extra spacing
+%
+%    \begin{macrocode}
+\def\chit at oob@turnupdate(#1,#2)#3#4{%
+  \chit at dbg{2}{ Turn update c=`#1',r=`#2',s=`#3',e=`#4'}
+  % \pgfmathparse{#2-ifthenelse(#1>0,#3,0)-#4}%
+  \pgfmathparse{#2-#4-ifthenelse(abs(#1)>0.0001,#3,0)} 
+  \xdef#2{\pgfmathresult}%
+  \xdef#1{0}%
+  \chit at dbg{2}{ \space\space-> update `\string#1'=#1,`\string#2'=#2}
+}
+%    \end{macrocode}
+% chit list, n-colls, cell size, extra vertical spacing
+%
+% This expects a list of lists of chits, one list per turn; the
+% maximum number of columns; the size of cells, extra spacing between
+% turns.
+%
+% Note, the list of lists leaf elements should be styles for the
+% chits. 
+%
+% This depends on the Tikz pic \texttt{chit/oob~turn} which takes the
+% number as argument.
+%
+% 
+%    \begin{macrocode}
+\newif\ifwg at oob@inv\wg at oob@invfalse
+\def\chit at oob@spacer{hspace}
+\def\wg at star@oob{\wg at oob@invtrue\wg at oob}
+\def\wg at nostar@oob{\wg at oob@invfalse\wg at oob}
+\def\oob{%
+  \@ifstar{\wg at star@oob%
+  }{\wg at nostar@oob%
+  }%
+}
+%    \end{macrocode}
+% 
+% The inner macro of \cs{oob}.  The arguments are
+% \begin{enumerate}
+% \item The list of lists of chits styles
+% \item The maximum number of columns
+% \item The width of each cell
+% \item Additional row spacing between turns
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\wg at oob#1#2#3#4{
+  \def\r{0}
+  \chit at dbg{2}{OOB: `#1'}
+  \foreach[count=\ti from 0] \t/\y in #1{
+    \xdef\o{\r}
+    \def\c{0}
+    \ifx\t\y\def\y{0}\fi
+    \chit at dbg{2}{Turn \ti\space(\r,\t,y=\y):'}
+    \ifwg at oob@inv%
+      \pic[transform shape] at ( .5*#3,\r) {chit/oob turn=\ti};% was dx=0.5
+    \else
+      \pic[transform shape] at (-.5*#3,\r) {chit/oob turn=\ti};% was dx=-0.5
+    \fi%
+    \ifx\t\empty\else%
+      \foreach \u/\m in \t{
+        %% \chit at dbg{2}{ `\u'=`\m'}
+        \ifx\u\empty\else
+          \ifx\m\@empty\def\m{1}\fi
+          \ifx\u\m\def\m{1}\fi
+          \foreach \n in {1,...,\m}{%
+            \chit at dbg{2}{OOB Chit is `\u'}%
+            \ifx\u\chit at oob@spacer%
+              \chit at dbg{3}{Chit `\u' is spacer `\chit at oob@spacer'}
+              \pgfmathparse{\c+#4}%
+              \xdef\c{\pgfmathresult}%
+            \else
+              \ifnum\chitdbglvl>2%
+                \node[minimum width=#3cm,minimum height=#3cm,
+                      draw,transform shape] at (\c,\r) {};
+              \fi
+              \ifx\u\chit at blank\else%
+                \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r);%
+              \fi%
+              \chit at oob@cellupdate(\c,\r){#2}{#3}{\y}
+            \fi
+          }
+        \fi
+      }
+    \fi
+    \chit at dbg{1}{ End of chits in turn
+      \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
+    % IF no units where given, then we force \c to be non-zero so that
+    % \chit at oob@turnupdate increments the row 
+    \ifx\t\@empty
+      \def\c{#3}
+      \chit at dbg{2}{ Turn is empty, set c=`\c'}
+    \fi
+    %\ifnum\y<0% No explicit number of rows given
+    %  \def\c{#3}
+    %  \chit at dbg{2}{ No explicit number of rows given, set c=`\c'}
+    %\fi
+    % In case the user gave and explicit number of rows, add the rows
+    % that are missing.  \y is initially set to the number of
+    % requested rows, and then decremented every time we go down one
+    % row.  So if the number of rows we did so far is N, and the
+    % requested number of rows is M, then the loop below adds M-N
+    % rows.
+    \ifnum\y>0%
+      \chit at dbg{2}{ Looping rows from 2 to \y, break when row > \y}%
+      \foreach \rr  in {2,...,\y}{
+        %\ifnum\rr>\y% A little funny, but \y can be negative!
+        %  \chit at dbg{2}{ \space Breaking loop \rr\space > \y}%
+        %  \breakforeach%
+        %\else%
+          \chit at oob@rowupdate(\c,\r){#3}{#4}
+        %\fi
+      }
+    \fi
+    % This will zero \c.  However, if on entry |\c|>0, then we also
+    % increment the row 
+    \chit at oob@turnupdate(\c,\r){#3}{#4}
+    \chit at dbg{2}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')} 
+  }
+  \chit at dbg{3}{End of OOB (c=`\c',r=`\r',y=`\y')}
+  \@ifnextchar;{\@gobble}{}}
+%    \end{macrocode}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/chit/oob.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/latex/wargame/chit/shape.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/shape.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/shape.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -4,6 +4,19 @@
 % \fi
 % 
 % \subsubsection{The \texttt{chit} key namespace}
+%
+% Some stuff to consider wrt.~line widths.  Setting the line width in
+% the \texttt{chit} scope overrides frame settings.  The frame stroke
+% can be larger but not smaller.  Setting the stroke width in the
+% symbol scope sets it for the symbol only.  Thus, to get a thin
+% border, we need to
+% \begin{itemize}
+% \item Set a small line width in the top chit scope.
+% \item Possible set a larger line width in the frame sub-scope.
+% \item Set a larger line width in the symbol sub-scope. 
+% \end{itemize}
+% I do not know why this is.
+% 
 % \begin{TikzKey}{/chit/full,
 %   /chit/symbol,
 %   /chit/left,
@@ -13,12 +26,15 @@
 %   /chit/lower left,
 %   /chit/lower right,
 %   /chit/factors,
+%   /chit/extra,
 %   /chit/setup,
+%   /chit/bevel,
 %   /chit/id}
 %
 %   The parts of a chit 
 %   
 %    \begin{macrocode}
+\newif\ifchit at clip\chit at cliptrue
 \tikzset{%
   /chit/.search also={/tikz},
   /chit/.cd,
@@ -36,6 +52,21 @@
   setup/.store in=\chit at setup,             setup/.initial=,%
   id/.store in=\chit at id,                   id/.initial=,%
   frame/.store in=\chit at frame,             frame/.initial=,%
+  extra/.store in=\chit at extra,             extra/.initial=,%
+  bev/.store in=\chit at bevel,               bev/.initial=,
+  bevel fraction/.store in=\chit at bevel@frac,bevel fraction/.initial=10,
+  bevel/.is choice,
+  bevel/none/.style       = {/chit/bev=},
+  bevel/north west/.style = {/chit/bev=1},
+  bevel/north east/.style = {/chit/bev=2},
+  bevel/south west/.style = {/chit/bev=3},
+  bevel/south east/.style = {/chit/bev=4},
+  bevel/NW/.style         = {/chit/bev=1},
+  bevel/NE/.style         = {/chit/bev=2},
+  bevel/SW/.style         = {/chit/bev=3},
+  bevel/SE/.style         = {/chit/bev=4},
+  bevel/.default          = north west,
+  clip/.is if=chit at clip%
 }
 %    \end{macrocode}
 % \end{TikzKey}
@@ -69,7 +100,28 @@
   chit/lower right/.style={chit/parts,anchor=south east},
   chit/setup/.style={chit/parts},
   chit/full/.style={chit/parts},
+  chit/frame/.try={draw=pgfstrokecolor},
+  chit/bevel highlight/.style={fill=white,opacity=.25},
+  chit/bevel shadow/.style={fill=black,opacity=.25},
 }
+\def\chit at bevel@frac{10}
+\newif\ifchit at draw@frame\chit at draw@frametrue
+\tikzset{
+  chit/frame style/.search also={/tikz},
+  chit/frame style/.cd,
+  none/.code={\chit at draw@framefalse},
+  draw/.code={%
+    \chit at dbg{2}{Frame draw option `#1'}
+    \edef\tikz at temp{#1}%
+    \ifx\tikz at temp\tikz at nonetext%
+      \chit at draw@framefalse%
+    \else%
+      \chit at draw@frametrue%
+      \tikzset{/tikz/draw=#1}
+    \fi
+  }
+}
+    
 %    \end{macrocode}
 % \end{TikzKey}
 % 
@@ -116,7 +168,7 @@
 %    \begin{macrocode}
 \def\chit at n@to#1#2{%
   %% Without a following start square bracket '[' by-pass to final
-  \chit at dbg{4}{Chit NATO App6(c) first step `#1' `#2'}
+  \chit at dbg{1}{Chit NATO App6(c) first step `#1' `#2'}
   \@ifnextchar[{%
     %\message{^^JStart square bracket}%
     \@chit at n@to{#1}{#2}}{%
@@ -129,7 +181,7 @@
 % 
 %    \begin{macrocode}
 \def\@chit at n@to@#1#2#3\@end at chit@n at to{%
-  \chit at dbg{4}{Chit NATO App6(c) w/o offset:
+  \chit at dbg{1}{Chit NATO App6(c) w/o offset:
     ^^J  Options:  #3
     ^^J  ID:       #1
     ^^J  Position: #2}
@@ -143,7 +195,7 @@
 % 
 %    \begin{macrocode}
 \def\@chit at n@to#1#2[#3]{%
-  \chit at dbg{4}{Chit NATO App6(c) second step `#1' `#2' `#3'}
+  \chit at dbg{1}{Chit NATO App6(c) second step `#1' `#2' `#3'}
   \@ifnextchar({\@@chit at n@to{#1}{#2}{#3}}{\@@chit at n@to{#1}{#2}{#3}(0,0)}%)
 }
 %    \end{macrocode}
@@ -152,7 +204,7 @@
 % 
 %    \begin{macrocode}
 \def\@@chit at n@to#1#2#3(#4)\@end at chit@n at to{%
-  \chit at dbg{4}{Chit NATO App6(c) w/offset:
+  \chit at dbg{1}{Chit NATO App6(c) w/offset:
     ^^J  Options:  #3
     ^^J  ID:       #1
     ^^J  Position: #2
@@ -186,8 +238,39 @@
 % 
 % Now follows the actual \text{chit} shape.  This is rather long, so
 % we will break it up a bit
-%
-%
+%    \begin{macrocode}
+\def\chit at bevel@path#1{
+  \scope[#1]
+  \wg at tmpc=\wg at tmpa\multiply\wg at tmpc by \chit at bevel@frac
+  \wg at tmpd=\wg at tmpb\multiply\wg at tmpd by \chit at bevel@frac
+  \divide\wg at tmpc100
+  \divide\wg at tmpd100
+  \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move down along edge
+  \wg at tmpb=-\wg at tmpb
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move left along edge
+  \wg at tmpa=-\wg at tmpa
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move in and up 
+  \advance\wg at tmpa\wg at tmpc%
+  \advance\wg at tmpb\wg at tmpd%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move right, but in 
+  \advance\wg at tmpa-\wg at tmpc\wg at tmpa=-\wg at tmpa%
+  \advance\wg at tmpa-\wg at tmpc%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move up but down
+  \advance\wg at tmpb-\wg at tmpd\wg at tmpb=-\wg at tmpb%
+  \advance\wg at tmpb-\wg at tmpd%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  \pgfclosepath%
+  \pgfusepath{fill}
+  \endscope
+}
+  
+%    \end{macrocode}
+% 
 % The first thing is we declare some saved anchors.  These are
 % computed (and defined as internal macros) when the shape is
 % instantised.  The anchors give the centre and north east corner of
@@ -224,7 +307,7 @@
     \let\chitframeopt\pgfutil at empty%
     \@ifundefined{chit at frame}{}{%
       \edef\chitframeopt{\chit at frame}}
-    \n at to@pp at dbg{3}{Chit Frame options: \meaning\chitframeopt}%
+    \chit at dbg{3}{Chit Frame options: \meaning\chitframeopt}%
   }
 %    \end{macrocode}
 %
@@ -296,6 +379,7 @@
   \backgroundpath{%
     %% This is the outline of the chit only.  The rest of the chit is
     %% made on the foreground "path".
+    \chit at dbg{1}{Chit drawing background path}
     \northeast%
     \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
     \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
@@ -314,29 +398,38 @@
 %
 %    \begin{macrocode}
   \behindforegroundpath{%
-    \chit at dbg{4}{%
-      Chit foreground: \meaning\id
-      ^^J  ID (set):    \meaning\chit at id
-      ^^J  Symbol:      \meaning\chit at symbol
-      ^^J  Full:        \meaning\chit at full
-      ^^J  Factors:     \meaning\chit at factors
-      ^^J  Left:        \meaning\chit at left
-      ^^J  Right:       \meaning\chit at right
-      ^^J  Upper left:  \meaning\chit at upper@left
-      ^^J  Lower left:  \meaning\chit at lower@left
-      ^^J  Upper right: \meaning\chit at upper@right
-      ^^J  Lower right: \meaning\chit at lower@right}
+    \chit at dbg{1}{Chit drawing foreground path}
+    % \chit at dbg{4}{%
+    %   Chit foreground: \meaning\id
+    %   ^^J  ID (set):    \meaning\chit at id
+    %   ^^J  Symbol:      \meaning\chit at symbol
+    %   ^^J  Full:        \meaning\chit at full
+    %   ^^J  Factors:     \meaning\chit at factors
+    %   ^^J  Left:        \meaning\chit at left
+    %   ^^J  Right:       \meaning\chit at right
+    %   ^^J  Upper left:  \meaning\chit at upper@left
+    %   ^^J  Lower left:  \meaning\chit at lower@left
+    %   ^^J  Upper right: \meaning\chit at upper@right
+    %   ^^J  Lower right: \meaning\chit at lower@right
+    %   ^^J  Extra:       \meaning\chit at extra
+    %   ^^J  Bevel:       \meaning\chit at bevel
+    %   ^^J  Frame:       \meaning\chit at frame}
+    \chit at dbg{1}{Chit report}
     \chit at report{}
+    \chit at dbg{1}{Chit start scope}
     \pgfscope
     %
-    \northeast%
-    \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
-    \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \pgfclosepath
-    \pgfusepath{clip}    
+    \ifchit at clip%
+      \chit at dbg{1}{Chit clip path}
+      \northeast%
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \pgfclosepath%
+      \pgfusepath{clip}%
+    \fi%
 %    \end{macrocode}
 %
 % If we do not have the \texttt{symbol} key set, then we set the
@@ -346,9 +439,10 @@
     \@ifundefined{chit at symbol}{%
       %% Draw full stuff
       \@ifundefined{chit at full}{}{%
+        \chit at dbg{1}{Chit draw full image: `\meaning\chit at full'}
         \center\wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
         \wg at pic@all{\chit at full}{}{\the\wg at tmpa,\the\wg at tmpb}{chit/full}}%
-    }{%
+    }{% With NATO symbol
 %    \end{macrocode}
 %
 %  Otherwise, we put in a node with shape \texttt{natoapp6c} and pass
@@ -355,10 +449,12 @@
 %  the \texttt{symbol} key--value pairs as options. 
 %
 %    \begin{macrocode}
+      \chit at dbg{1}{Chit draw symbol image}
       \edef\symid{\id symbol}%
       \symbol%
       \edef\args{{\symid}{\the\pgf at x,\the\pgf at y}\chit at symbol}%
       \chit at dbg{6}{Arguments to chit NATO symbol: \meaning\args}%
+      \chit at dbg{1}{Chit draw nato image}
       \expandafter\chit at n@to\args\@end at chit@n at to%
       \chit at dbg{6}{After making NATO symbol in chit}%
 %    \end{macrocode}
@@ -378,6 +474,7 @@
 %    \begin{macrocode}
       % Put in left of symbol
       \@ifundefined{chit at left}{}{%
+        \chit at dbg{1}{Chit draw left: `\meaning\chit at left'}
         \begin{scope}[]
           \pgfpointanchor{\symid}{west}%
           \wg at tmpa=\pgf at x\advance\wg at tmpa-\margin%
@@ -386,6 +483,7 @@
         \end{scope}}%
       % Put in right of symbol
       \@ifundefined{chit at right}{}{%
+        \chit at dbg{1}{Chit draw left: `\meaning\chit at right'}
         \begin{scope}[]
           \pgfpointanchor{\symid}{east}%
           \wg at tmpa=\pgf at x\advance\wg at tmpa+\margin%
@@ -416,6 +514,7 @@
 %    \begin{macrocode}
       % Put in upper left corner
       \@ifundefined{chit at upper@left}{}{%
+        \chit at dbg{1}{Chit draw upper left: `\meaning\chit at upper@left'}
         \begin{scope}[]
           \wg at pic@all{\chit at upper@left}{}{-\the\wg at tmpa,\the\wg at tmpb}{%
             chit/upper left}%
@@ -422,6 +521,7 @@
         \end{scope}}
       % Put in upper right corner
       \@ifundefined{chit at upper@right}{}{%
+        \chit at dbg{1}{Chit draw upper right: `\meaning\chit at upper@right'}
         \begin{scope}[]
           \wg at pic@all{\chit at upper@right}{}{\the\wg at tmpa,\the\wg at tmpb}{%
             chit/upper right}%
@@ -428,6 +528,7 @@
         \end{scope}}
       % Put in lower left corner
       \@ifundefined{chit at lower@left}{}{%
+        \chit at dbg{1}{Chit draw lower left: `\meaning\chit at lower@left'}
         \begin{scope}[]
           \wg at pic@all{\chit at lower@left}{}{-\the\wg at tmpa,-\the\wg at tmpb}{%
             chit/lower left}%
@@ -434,6 +535,7 @@
         \end{scope}}
       % Put in lower right corner
       \@ifundefined{chit at lower@right}{}{%
+        \chit at dbg{1}{Chit draw lower right: `\meaning\chit at lower@right'}
         \begin{scope}[]
           \wg at pic@all{\chit at lower@right}{}{\the\wg at tmpa,-\the\wg at tmpb}{%
             chit/lower right}%
@@ -448,24 +550,62 @@
 %    \begin{macrocode}
       % Put in factors
       \@ifundefined{chit at factors}{}{%
+        \chit at dbg{1}{Chit draw factors: `\meaning\chit at factors'}
         \advance\wg at tmpb-\margin%
         \begin{scope}[]
           \wg at pic@all{\chit at factors}{}{0,-\the\wg at tmpb}{chit/factors}%
         \end{scope}}%
-    }%
+      % Put in extra
+      \@ifundefined{chit at extra}{}{%
+        \chit at dbg{1}{Chit draw extra: `\meaning\chit at extra'}
+        \begin{scope}[]
+          \wg at pic@all{\chit at extra}{}{0,0}{chit/factors}%
+        \end{scope}}%
+    }% End of full or symbol
     \endpgfscope%
+    % Make bevel?
+    \@ifundefined{chit at bevel}{\let\chit at bevel\empty}{}
+    \ifx\chit at bevel\empty\else%
+      \chit at dbg{1}{Chit draw bevel}
+      %% South east bevel  
+      \northeast%
+      \wg at tmpa=-\pgf at x\wg at tmpb=-\pgf at y%
+      \ifcase\chit at bevel\relax%
+      \or% 1
+      \or\wg at tmpa=-\wg at tmpa% 2
+      \or\wg at tmpb=-\wg at tmpb% 3
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpb=-\wg at tmpb%4
+      \fi
+      \chit at bevel@path{chit/bevel highlight}
+      %% North west bevel
+      \northeast%
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \ifcase\chit at bevel\relax%
+      \or% 1
+      \or\wg at tmpa=-\wg at tmpa% 2
+      \or\wg at tmpb=-\wg at tmpb% 3
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpb=-\wg at tmpb%4
+      \fi
+      \chit at bevel@path{chit/bevel shadow}
+    \fi
     % Draw frame?
-    \edef\tmp at opt{[\chitframeopt]}
+    \chit at dbg{1}{Chit draw frame: `\meaning\chitframeopt'}
+    \edef\tmp at opt{[chit/frame style/.cd,chit/frame/.try,\chitframeopt]}
+    \chit at dbg{1}{Chit draw frame: `\meaning\tmp at opt}
     \expandafter\scope\tmp at opt
-    \northeast%
-    \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
-    \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \pgfclosepath
-    \pgfusepath{stroke}
-    \endscope
+      \northeast%
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \pgfclosepath%
+      \chit at dbg{3}{Line width for frame: `\the\pgflinewidth'}
+      \ifchit at draw@frame\pgfusepath{stroke}\fi%
+      \chit at draw@frametrue%
+      %\iftikz at mode@fill\pgfusepath{fill}\fi%
+    \endscope%
+    \chit at dbg{1}{Chit end of shape}
   }
 }
 %    \end{macrocode}
@@ -527,7 +667,7 @@
     ^^J  Name:       `#3'}
   \let\name\pgfutil at empty%
   \chit at dbg{1}{=== Before chit node}%
-  \node[draw,chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {};
+  \node[chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {};
   \chit at dbg{2}{=== After chit node}%
   \ifx|#3|\relax%
   \else%

Added: trunk/Master/texmf-dist/source/latex/wargame/chit/stack.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/stack.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/stack.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,54 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Stacking of chits}
+%
+% Stacking of chits.  The key \spec{chit/stack direction} sets the
+% default direction to make the stack in.
+% 
+%    \begin{macrocode}
+% offset, location, direction, list
+\tikzset{%
+  chit/stack direction/.store in=\chit at stack@dir,
+  chit/stack direction/.initial={(.3,.3)},
+}
+%    \end{macrocode}
+%
+% Now the code 
+%
+%    \begin{macrocode}
+\def\chit at stack@dir{(.3,.3)}
+\def\stackchits(#1){%
+  \@ifnextchar({\st at ckchits{#1}}{\st at ckchits{#1}(.3,.3)}%)
+}
+\def\st at ckchits#1(#2)#3{%
+  \chit at dbg{2}{Stacking chits `#1', `#2', `#3'}%
+  \edef\xy{#1}%
+  \chit at dbg{4}{Stack start at \xy}%
+  \foreach[count=\i from 0] \c/\o in {#3} {%
+    \ifx\c\empty\else%
+      \edef\ccc{\c}%
+      \chit at dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'}%
+      \expandafter\ccc(\xy)%
+      %%
+      \ifx\c\o\else%
+        %\chit at dbg{0}{Option: \o}
+        \edef\ccc{\o}%
+        \expandafter\ccc(\xy)%
+      \fi
+      \expandafter\ccc(\xy)%
+      \tikzmath{%
+        coordinate \cc;%
+        \cc = (\xy) + (#2);}
+      \xdef\xy{\cc}%
+    \fi%
+  }%
+  \@ifnextchar;{\@gobble}{}%  
+}
+%    \end{macrocode}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/chit/stack.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/wargame/chit/table.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/chit/table.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/chit/table.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,176 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Table of chits}
+%
+%    \begin{macrocode}
+\tikzset{
+  chit/cell background/.style={fill=black},
+  blank chit/.style={/chit/frame={draw=none,fill=none}},
+}
+%    \end{macrocode}
+% 
+% These macros are used when we set tables of chits.  This allows us
+% to define blank spaces in the table by giving the element
+% \texttt{blank~chit}.
+% 
+%    \begin{macrocode}
+\def\chit at blank{blank chit}
+\def\chit at cellbg(#1,#2)#3{%
+  \draw[chit/cell background](#1-#3/2,#2-#3/2) rectangle++(#3,#3);
+}
+%    \end{macrocode}
+% 
+% \begin{Macro}{\ifchits at reset}
+%
+%   This `if' controls whether to reset the coordinates to the origin
+%   when \cs{chits} is called.  If true, then reset for a new table.
+%   
+%    \begin{macrocode}
+\newif\ifchits at reset\chits at resettrue
+%    \end{macrocode}
+% \end{Macro}
+% 
+% \begin{Macro}{\chits,\@chits,\chit at sng@cellupdate}
+%    \begin{macrocode}
+\def\chit at sng@cellupdate(#1,#2)#3#4{%
+  \chit at dbg{2}{Current `#1' vs `#4'*(`#3'+1)}
+  \pgfmathparse{ifthenelse(#1>=#4*(#3-1),#2-#4,#2)}%
+  \xdef#2{\pgfmathresult}%
+  \pgfmathparse{ifthenelse(#1>=#4*(#3-1),0,#1+#4)}%
+  \xdef#1{\pgfmathresult}%
+}
+%    \end{macrocode}
+%
+% The stared version (\cs{chits*}) of this macro continues the
+% previously set chit table.
+% 
+%    \begin{macrocode}
+\def\chits{%
+  \@ifstar{\chits at resetfalse\@chits}{\chits at resettrue\@chits}}
+%    \end{macrocode}
+% 
+%    \begin{macrocode}
+\def\@chits#1#2#3{
+  \ifchits at reset
+    \def\r{0}%
+    \def\c{0}%
+  \fi
+  \chit at dbg{1}{Chits to make: #1}%
+  \foreach[count=\ti from 0] \t/\x in #1{%
+    \chit at dbg{2}{Turn `\t' with option `\x'}
+    \ifx\t\empty\else%
+      \foreach \u/\m in \t{%
+        \ifx\u\empty\else%
+          \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+          \ifx\m\@empty\def\m{1}\fi%
+          \ifx\u\m\def\m{1}\fi%
+          \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
+          \foreach \n in {1,...,\m}{%
+            \ifx\u\chit at blank%
+              \chit at dbg{3}{Ignoring blank chit:\u}%
+            \else%
+              \chit at cellbg(\c,\r){#3}%
+              \chit[\u=\ti](\c,\r)%
+              \chit at sng@cellupdate(\c,\r){#2}{#3}%
+            \fi%
+          }%
+        \fi%
+      }%
+    \fi%
+  }%
+  \@ifnextchar;{\@gobble}{}}
+%    \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\doublechits,
+%   \@doublechits,
+%   \chit at dbl@cellupdate,
+%   \chit at dbl@flip}
+% \begin{enumerate}
+% \item coordinates
+% \item coordinates
+% \item cell-size
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\chit at dbl@flip(#1,#2)#3{%
+  \pgfmathparse{-#1}%
+  \xdef\mc{\pgfmathresult}%
+}
+%    \end{macrocode}
+% 
+%   \begin{enumerate}
+%   \item coordinates
+%   \item coordinates
+%   \item Number of columns
+%   \item cell-size
+%   \end{enumerate}
+%
+%    \begin{macrocode}
+\def\chit at dbl@cellupdate(#1,#2)#3#4{%
+  \pgfmathparse{ifthenelse(#1<-#4/2,#2,#4+#2)}%
+  \xdef#2{\pgfmathresult}%
+  \pgfmathparse{ifthenelse(#1<-#4/2,#4+#1,-(#3-.5)*#4)}%
+  \xdef#1{\pgfmathresult}%
+}
+%    \end{macrocode}
+%
+%   \begin{enumerate}
+%   \item List of list of keys
+%   \item Number of columns
+%   \item size of each cell
+%   \end{enumerate}
+% 
+%   The stared version (\cs{doublechits*}) of this macro continues the
+%   previously set chit table.
+% 
+%    \begin{macrocode}
+\def\doublechits{%
+  \@ifstar{\chits at resetfalse\@doublechits}{\chits at resettrue\@doublechits}}
+%    \end{macrocode}
+%
+%    \begin{macrocode}
+\def\@doublechits#1#2#3{%
+  \chit at dbg{1}{Setting double-sided chits: #1}
+  \ifchits at reset
+    \pgfmathparse{-(#2-.5)*#3}
+    \xdef\c{\pgfmathresult}
+    \def\r{0}
+  \fi
+  
+  \foreach[count=\ti from 0] \t/\x in #1{
+    \ifx\t\empty\else%
+      \foreach \u/\m in \t{
+        \ifx\u\empty\else
+          \ifx\m\@empty\def\m{1}\else%
+            \ifx\u\m\def\m{1}\fi\fi
+          \chit at dbg{2}{`\u'=`\m' (\c,\r)}
+          \foreach \n in {1,...,\m}{%
+            \ifx\u\chit at blank
+              \chit at dbg{3}{Ignoring blank chit:\u}
+            \else
+              \chit at cellbg(\c,\r){#3}
+              \chit[\u=\ti](\c,\r)
+              \chit at dbl@flip(\c,\r){#3}
+              \chit at cellbg(\mc,\r){#3}
+              \chit[\u\space flipped=\ti,zone turn=\t,zone mult=\n](\mc,\r)
+              \chit at dbl@cellupdate(\c,\r){#2}{#3}
+            \fi
+          }
+        \fi
+      }
+    \fi
+  }
+  \draw[dashed](0,-3*#3/4)--(0,\r-#3/4);%
+  \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[transform shape,anchor=west]{Back};%
+  \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[transform shape,anchor=east]{Front};%
+  \@ifnextchar;{\@gobble}{}}  
+%    \end{macrocode}
+% \end{Macro}
+%    
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/chit/table.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/board.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/board.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/board.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -18,82 +18,261 @@
 %   
 %
 %    \begin{macrocode}
-\tikzset{
-  hex/board frame/.style={draw}
-}
 \def\boardframe{%
-  \@ifnextchar[{\bo at rdframe}{\bo at rdframe[0]}%}
+  \@ifnextchar[{\bo at rdframe}{\bo at rdframe[0]}%]
 }
+%    \end{macrocode}
+%
+% Below is our new implementation of \cs{boardframe}.  This is split
+% into parts. 
+%
+%
+% First, a macro that will define the path around rectangular placed
+% hexes.  This takes 4 mandatory arguments: lower left column and row,
+% and upper right column and row, in that order.  It also accepts an
+% optional argument.  If this is not empty, then it is assumed to be a
+% style to apply, and hexes will be drawn using that style.  The style
+% will be passed the hex coordinates and can react accordingly.
+% 
+%    \begin{macrocode}
+\def\bo at rdfr@me{
+  \@ifnextchar[{\bo at rdfr@me@}{\bo at rdfr@me@[]}%]
+}
+\def\bo at rdfr@me at u(#1)#2#3#4#5{
+  \hex at coords@conv{#1}
+  % \hex at dbg{0}{#1 -> `\hex at x',`\hex at y'}
+  \pgfmathparse{min(#2,\hex at x)}\xdef#2{\pgfmathresult}
+  \pgfmathparse{min(#3,\hex at y)}\xdef#3{\pgfmathresult}
+  \pgfmathparse{max(#4,\hex at x)}\xdef#4{\pgfmathresult}
+  \pgfmathparse{max(#5,\hex at y)}\xdef#5{\pgfmathresult}
+  \hex at dbg{2}{#1 -> ll=`#2',`#3', ur=`#4',`#5'}
+}
+\def\bo at rdfr@me@[#1]#2#3#4#5{
+  % Define rtmp and a ctmp to by directions 
+  \pgfmathparse{int(\hex at coords@row at fac)}\edef\rtmp{\pgfmathresult}
+  \pgfmathparse{int(\hex at coords@col at fac)}\edef\ctmp{\pgfmathresult}
+  % Define vertices for path 
+  \def\ctfv{SW}
+  \def\ctsv{SE}
+  \def\cbfv{NE}
+  \def\cbsv{NW}
+  \def\rrfv{E}
+  \def\rrsv{NE}
+  \def\rlfv{W}
+  \def\rlsv{SW}
+  % Swap around some definitions based on the row direction
+  \ifnum\rtmp<0
+    \let\max at short\hex at bot@short at col
+    \let\min at short\hex at top@short at col
+    \let\swp\ctfv\let\ctfv\cbsv\let\cbsv\swp
+    \let\swp\ctsv\let\ctsv\cbfv\let\cbfv\swp
+    \def\rrsv{SE}
+    \def\rlsv{NW}
+  \else
+    \let\max at short\hex at top@short at col
+    \let\min at short\hex at bot@short at col
+  \fi
+  % Swap around some definitions based on the column direction
+  \ifnum\ctmp<0
+    \let\swp\ctfv\let\ctfv\ctsv\let\ctsv\swp
+    \let\swp\cbfv\let\cbfv\cbsv\let\cbsv\swp
+    \let\swp\rrfv\let\rrfv\rlsv\let\rlsv\swp
+    \let\swp\rrsv\let\rrsv\rlfv\let\rlfv\swp
+  \fi
+  % Define tmp = 0 if no shorts, 1 if top short, 2 if both  
+  \pgfmathparse{ifthenelse(\hex at got@top at short,
+    ifthenelse(\hex at got@bot at short,2,1),0)}\edef\tmp{\pgfmathresult}
+  % If top-short, set factors 
+  \ifnum\tmp=1
+    \def\mnf{-1}
+    \def\mxf{-1}
+    \def\mnn{}
+    \def\mxn{}
+  % If both short, set factors 
+  \else\ifnum\tmp=2
+    \def\mnf{\rtmp}
+    \def\mxf{(-\rtmp)}
+    % If inverse rows, set factors 
+    \ifnum\rtmp<0
+      \def\mnn{}
+      \def\mxn{not}
+    \else
+      \def\mnn{not}
+      \def\mxn{}
+    \fi
+  % If none is short
+  \else
+    \def\mnf{1}
+    \def\mxf{1}
+    \def\mnn{not}
+    \def\mxn{not}
+  \fi\fi
+  % Define row at mn to give least row of column  
+  \def\row at mn##1{%   
+    \pgfmathparse{int(#3+\mnf*
+      \hex at coords@row at fac*\min at short(##1)*
+      \mnn(\min at short(\hex at coords@col at off)))} 
+    \edef\lr{\pgfmathresult}}
+  % Define row at mx to give largest row of column
+  \def\row at mx##1{%
+    \pgfmathparse{int(#5+\mxf* 
+      \hex at coords@row at fac*\max at short(##1)*
+      \mxn(\max at short(\hex at coords@col at off)))}
+    \edef\ur{\pgfmathresult}}
+  % 
+  % 
+  % Below defines a path around the perimeter of the hexes.
+  %
+  \def\@llx{10000}
+  \def\@lly{10000}
+  \def\@urx{-10000}
+  \def\@ury{-10000}
+  % Start with an empty path 
+  \def\p{}
+  % Loop across least row (can be top if \rtmp<0)
+  \foreach \c in {#2,...,#4}{%
+    \row at mn{\c}
+    \row at mx{\c}
+    % \message{^^JColumn: `\c' -> `\lr',`\ur' (#3,#5)}
+  }
+  \foreach \c in {#2,...,#4}{%
+    \row at mn{\c}
+    \xdef\p{\p
+      (hex cs:c=\c,r=\lr,v=\ctfv)--
+      (hex cs:c=\c,r=\lr,v=\ctsv)--}
+    \bo at rdfr@me at u(c=\c,r=\lr,v=\ctfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=\c,r=\lr,v=\ctsv)\@llx\@lly\@urx\@ury
+  }
+  % Go up (down if \rtmp<0) right side
+  \row at mn{#4}
+  \row at mx{#4}
+  \foreach \r in {\lr,...,\ur}{%
+    \xdef\p{\p
+      (hex cs:c=#4,r=\r,v=\rrfv)--
+      (hex cs:c=#4,r=\r,v=\rrsv)--}
+    \bo at rdfr@me at u(c=#4,r=\r,v=\rrfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=#4,r=\r,v=\rrsv)\@llx\@lly\@urx\@ury
+  }
+  % Go across largest row (can be bottom if \rtmp<0)
+  \foreach \c in {#4,...,#2}{%
+    \row at mx{\c}
+    % \message{^^JColumn: `\c', max:`\ur'}
+    \xdef\p{\p
+      (hex cs:c=\c,r=\ur,v=\cbfv)--
+      (hex cs:c=\c,r=\ur,v=\cbsv)--}
+    \bo at rdfr@me at u(c=\c,r=\ur,v=\cbfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=\c,r=\ur,v=\cbsv)\@llx\@lly\@urx\@ury
+  }
+  % Go up (down if \rtmp<0) left side.
+  \row at mn{#2}
+  \row at mx{#2}
+  \foreach \r in {\ur,...,\lr}{%
+    \xdef\p{\p
+       (hex cs:c=#2,r=\r,v=\rlfv)--
+       (hex cs:c=#2,r=\r,v=\rlsv)--}
+    \bo at rdfr@me at u(c=#2,r=\r,v=\rlfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=#2,r=\r,v=\rlsv)\@llx\@lly\@urx\@ury
+  }
+  % End path with cycle  
+  \edef\p{\p cycle}
+  % Define global path 
+  \global\let\hex at board@path\p
+  \hex at dbg{3}{Hex board path: `\meaning\hex at board@path'}
+  % If an optional argument was given, then use that to actually make
+  % hexes. 
+  \ifx|#1|\else
+    \foreach[count=\nc] \c in {#2,...,#4}{%
+      \row at mn{\c}
+      \row at mx{\c}
+      \foreach \r in {\lr,...,\ur}{%
+        \hex[#1={\c,\r}](c=\c,r=\r)
+      }
+    }
+  \fi  
+}
+%    \end{macrocode}
+%
+%
+% This is a no operations style used as default for the macro
+% \cs{boardhexes} below.
+% 
+%    \begin{macrocode}
+\tikzset{%
+  /hex/board/no op/.style args={#1,#2}{}}
+%    \end{macrocode}
+%
+% This macro will make the actual hexes using the specified, optional,
+% style.  It builds on \cs{bo at rdfr@me} above.
+% 
+%    \begin{macrocode}
+\def\boardhexes{%
+  \@ifnextchar[{\bo at rdhexes}{\bo at rdhexes[board/no op]}%]
+}
+\def\bo at rdhexes[#1](#2)(#3){%
+  \hex at coords@conv{#2}
+  \edef\llc{\hex at col}
+  \edef\llr{\hex at row}
+  \hex at coords@conv{#3}
+  \edef\urc{\hex at col}
+  \edef\urr{\hex at row}
+  \bo at rdfr@me[#1]{\llc}{\llr}{\urc}{\urr}}
+%    \end{macrocode}
+%
+% Creates a board frame using \cs{bo at rdfr@me}. 
+% 
+%    \begin{macrocode}
+\tikzset{board frame bb/.code={
+    \pgfkeys{
+      %/tikz/local bounding box=tmp board frame,
+      /tikz/transform shape,
+      /tikz/execute at end scope={%
+        % \hex at dbg{1}{Getting board frame BB}
+        %\wg at get@bb{tmp board frame}
+        \global\let\llx\@llx
+        \global\let\lly\@lly
+        \global\let\urx\@urx
+        \global\let\ury\@ury
+        % \hex at dbg{0}{Board bounding box (\llx,\lly)x(\urx,\ury)}
+      }}}}
+      
 \def\bo at rdframe[#1](#2)(#3){%
   \hex at coords@conv{#2}
-  \edef\llx{\hex at x}
-  \edef\lly{\hex at y}
   \edef\llc{\hex at col}
   \edef\llr{\hex at row}
-  \edef\ellc{\hex at eff@col}
-  \edef\ellr{\hex at eff@row}
   %
   \hex at coords@conv{#3}
-  \edef\urx{\hex at x}
-  \edef\ury{\hex at y}
   \edef\urc{\hex at col}
   \edef\urr{\hex at row}
-  \edef\eurc{\hex at eff@col}
-  \edef\eurr{\hex at eff@row}
   %
   \def\margin{#1}
   %
-  \hex at dbg{2}{%
-    Board Hex range: (\llc,\llr)x(\urc,\urr)
-    ^^JEffective range: (\ellc,\ellr)x(\eurc,\eurr)
-    ^^JBB:              (\llx,\lly)x(\urx,\ury)}%
-  \ifnum\hexdbglvl>1
-    %\draw[red,very thick](hex cs:c=\llc,r=\llr) rectangle(hex cs:c=\urc,r=\urr);
-    \draw[red,ultra thick,dashed](\llx,\lly) rectangle(\urx,\ury);
-    \draw[->,very thick,blue] (0,0) -- (0,1) (0,0) -- (1,0);
-  \fi
-  % Calculate how many half hex hides to add to the "bottom"
+  % This will store the bounding box in tmp node `board frame'
+  \bo at rdfr@me{\llc}{\llr}{\urc}{\urr}%
+  \begin{scope}[board frame bb]
+    \expandafter\path\hex at board@path;
+  \end{scope}
+  \hex at dbg{1}{Board frame LL: -> `\llx',`\lly'}
+  \pgfmathparse{\llx+ifthenelse(\llx<0,-1,1)*\margin}\edef\llx{\pgfmathresult}
+  \pgfmathparse{\lly+ifthenelse(\lly<0,-1,1)*\margin}\edef\lly{\pgfmathresult}
   %
-  \def\oddeven{isodd}
-  \ifnum\hex at coords@row at fac<0\def\oddeven{iseven}\fi%
-  \pgfmathparse{
-    ifthenelse(\hex at got@bot at short(\ellc),
-     ifthenelse(\hex at bot@short at col(\llc)*not(\oddeven(\ellc)),2,
-      ifthenelse(\hex at bot@short at col(\llc),0,1)),
-     ifthenelse(\oddeven(\ellc),1,2))}
-  \edef\olly{\pgfmathresult}%
-  \hex at dbg{2}{Delta lly: \olly half heights}
-  % Calculate how many half hex heights to add to the "top"
-  \def\oddeven{iseven}
-  \ifnum\hex at coords@row at fac<0\def\oddeven{isodd}\fi%
+  \hex at dbg{1}{Board frame UR: -> `\urx',`\ury'}
+  \pgfmathparse{\urx+ifthenelse(\urx<0,-1,1)*\margin}\edef\urx{\pgfmathresult}
+  \pgfmathparse{\ury+ifthenelse(\ury<0,-1,1)*\margin}\edef\ury{\pgfmathresult}
   %
-  \pgfmathparse{
-    ifthenelse(\hex at got@top at short(\urc),
-     ifthenelse(\hex at top@short at col(\urc)*\oddeven(\eurc),0,
-      ifthenelse(\hex at top@short at col(\urc),2,1)),
-     ifthenelse(\oddeven(\eurc),1,2))}
-  \edef\oury{\pgfmathresult}%
-  \hex at dbg{2}{Delta ury: \oury half heights}
-  % Calculate new LLY and URY
-  \pgfmathparse{\lly-\hex at coords@row at fac*(\olly*\hex at yy+\margin)}
-  \edef\lly{\pgfmathresult}
-  \pgfmathparse{\ury+\hex at coords@row at fac*(\oury*\hex at yy+\margin)}
-  \edef\ury{\pgfmathresult}
-  % Calculate new LLX and URX 
-  \pgfmathparse{\llx-1-\margin}\edef\llx{\pgfmathresult}
-  \pgfmathparse{\urx+1+\margin}\edef\urx{\pgfmathresult}
-  % Calculate width and height 
   \pgfmathparse{\urx-\llx}\edef\w{\pgfmathresult}
   \pgfmathparse{\ury-\lly}\edef\h{\pgfmathresult}
+  %% Print to the log
   \hex at dbg{0}{Board Frame: (\llx,\lly)x(\urx,\ury) (\w x\h) (\llc,\llr)x(\urc,\urr)}
+  %% Possibly draw 
   \draw[hex/board frame/.try](\llx,\lly) rectangle(\urx,\ury);
+  %% Store macros 
   \xdef\boardXmin{\llx}%
   \xdef\boardYmin{\lly}%
   \xdef\boardXmax{\urx}%
   \xdef\boardYmax{\ury}%
-}
-  
+}  
 %    \end{macrocode}
+% 
 % \end{Macro}
 %    
 % \begin{Macro}{\boardclip}
@@ -104,108 +283,28 @@
 %   \cs{boardclip}\marg{nx}\marg{ny}\marg{preaction}  
 % \end{Syntax}
 %    \begin{macrocode}
-\def\boardpath(#1)(#2){%
+\def\boardpath(#1)(#2){
   \hex at coords@reset%
   \tikzset{/hex/coords/.cd, #1}
-  \edef\llx{\hex at col}
-  \edef\lly{\hex at row}
+  \edef\llc{\hex at col}
+  \edef\llr{\hex at row}
   %%
   \hex at coords@reset%
   \tikzset{/hex/coords/.cd, #2}
-  \edef\urx{\hex at col}
-  \edef\ury{\hex at row}
-  \let\board at odd\@undefined%
-  \hex at dbg{1}{Board BB in hex: (\llx,\lly)x(\urx,\ury)}
-  %%
-  \def\fv{south}
-  \def\sv{north}
-  \ifnum\hex at coords@row at fac<0
-    \def\fv{north}
-    \def\sv{south}
-  \fi
-
-  \edef\hex at board@path{(hex cs:c=\llx,r=\lly,v=\fv\space west)}
-  %% First the left side
-  \foreach \r in {\lly,...,\ury} {%
-    \edef\t{
-      --(hex cs:c=\llx,r=\r,v=west)
-      --(hex cs:c=\llx,r=\r,v=\sv\space west)}
-    \wg at addto@macro{\hex at board@path}{\t}}
-  %% Then for top of board
-  \foreach \c in {\llx,...,\urx} {%
-    % To be done
-    %\pgfmathparse{int(ifthenelse(\hex at bot@short at col(\c),1,0))}
-    %\edef\tmp{\pgfmathresult}
-    %\ifnum\tmp>0
-    %\edef\t{
-    %  --(hex cs:c=\c,r=\ury,v=\sv\space west)
-    %  --(hex cs:c=\c,r=\ury,v=\sv\space east)}
-    %\else
-    \edef\t{
-      --(hex cs:c=\c,r=\ury,v=\fv\space east)
-      --(hex cs:c=\c,r=\ury,v=\fv\space west)}
-    %\fi
-    \wg at addto@macro{\hex at board@path}{\t}}
-  %% Then for right of board
-  \foreach \r in {\ury,...,\lly} {%
-    \edef\t{
-      --(hex cs:c=\urx,r=\r,v=east)
-      --(hex cs:c=\urx,r=\r,v=\fv\space east)}
-    \wg at addto@macro{\hex at board@path}{\t}}
-      
-  %% Then for bottom of board
-  \edef\t{--(hex cs:r=\lly,c=\urx,v=\fv\space west)}
-  \wg at addto@macro{\hex at board@path}{\t}
-  \foreach \c in {\urx,...,\llx} {%
-    \pgfmathparse{int(ifthenelse(\hex at bot@short at col(\c),1,0))}
-    \edef\tmp{\pgfmathresult}
-    \ifnum\tmp>0
-    \edef\t{
-      --(hex cs:c=\c,r=\lly,v=\sv\space east)
-      --(hex cs:c=\c,r=\lly,v=\sv\space west)}
-    \else
-    \edef\t{
-      --(hex cs:c=\c,r=\lly,v=\fv\space east)
-      --(hex cs:c=\c,r=\lly,v=\fv\space west)}
-    \fi
-    \wg at addto@macro{\hex at board@path}{\t}}
-    
-  \def\t{--cycle}
-  \wg at addto@macro{\hex at board@path}{\t}
+  \edef\urc{\hex at col}
+  \edef\urr{\hex at row}
+  
+  % This will store the bounding box in tmp node `board frame'
+  \bo at rdfr@me{\llc}{\llr}{\urc}{\urr}%
+  %% Use the path to extract the bounding box 
+  %\begin{scope}[local bounding box=board frame]
+  %  \expandafter\path\hex at board@path;
+  %\end{scope}
   \global\let\hexboardpath\hex at board@path
 }
-%\def\boardclip#1#2#3{%
-%  \pgfmathparse{int(#1-1)}\xdef\board at range{\pgfmathresult,...,0}%
-%  %% \show\board at range
-%  \draw \ifx|#3|\else[preaction={#3}]\fi%
-%  [clip]
-%  % [decorate,decoration={show path construction,
-%  % moveto code={\fill[red](\tikzinputsegmentfirst) circle(2pt)
-%  % node [fill=none,below]{moveto};},
-%  % lineto code={\draw[thick,blue,->](\tikzinputsegmentfirst)--
-%  % (\tikzinputsegmentlast) node [above] {lineto};},
-%  % curveto code={\draw[thick,green,->](\tikzinputsegmentfirst)..
-%  % controls(\tikzinputsegmentsupporta) and
-%  % (\tikzinputsegmentsupportb)
-%  % ..(\tikzinputsegmentlast) node[above]{curveto};},
-%  % closepath code={\draw[thick,orange,->](\tikzinputsegmentfirst)--
-%  % (\tikzinputsegmentlast) node [above] {closepath};}
-%  % }]
-%  (hex cs:r=0,c=0,v=south west)
-%  %% First the left side
-%  \foreach \r in {0,1,...,#2} {%
-%    --(hex cs:c=0,r=\r,v=west)--(hex cs:c=0,r=\r,v=north west)}
-%  %% Then for top of board
-%  \foreach \c in {0,1,...,#1} {%
-%    --(hex cs:r=#2,c=\c,v=north west)--(hex cs:c=\c,r=#2,v=north east)}
-%  %% Then for right of board
-%  \foreach \r in {#2,...,0} {%
-%    --(hex cs:c=#1,r=\r,v=east)--(hex cs:c=#1,r=\r,v=south east)}
-%  %% Then for bottom of board
-%  --(hex cs:r=0,c=#1,v=south west) \foreach \c in \board at range {%
-%    --(hex cs:r=0,c=\c,v=south east) --(hex cs:c=\c,r=0,v=south west) }
-%  --cycle; }
-%% New definition - much simpler 
+
+%    \end{macrocode}
+%    \begin{macrocode}
 \def\boardclip(#1)(#2)#3{%
   \boardpath(#1)(#2)
   \draw \ifx|#3|\else[preaction={#3}]\fi%

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/coord.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/coord.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/coord.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -278,11 +278,10 @@
 %    \begin{macrocode}
   \pgfmathparse{int(\hex at coords@col at fac*(\hex at col+\hex at coords@col at off))}%
   \xdef\hex at eff@col{\pgfmathresult}%
-  \hex at dbg{2}{Effective column: \hex at coords@col at fac * (\hex at col -
+  \hex at dbg{2}{Effective column: \hex at coords@col at fac * (\hex at col +
     \hex at coords@col at off) -> \hex at eff@col}%
   \pgfmathparse{\hex at eff@col*1.5}%
   \xdef\hex at x{\pgfmathresult}%
-  \expandafter\pgf at x=\hex at x cm%
 %    \end{macrocode}
 %
 % And then for the $y$ coordinate and set the dimension \cs{pgf at y}.
@@ -304,8 +303,8 @@
   \hex at dbg{2}{Effective row: \hex at coords@row at fac * (\hex at row +
     \hex at coords@row at off) -> \hex at eff@row}%
   \pgfmathparse{(2*\hex at eff@row-mod(round((\hex at col+\hex at coords@col at off)),2))*\hex at yy}%
+  \pgfmathparse{(2*\hex at eff@row-mod(abs(round(\hex at col+\hex at coords@col at off)),2))*\hex at yy}%  
   \xdef\hex at y{\pgfmathresult}%
-  \expandafter\pgf at x=\hex at y cm%
 %    \end{macrocode}
 %
 % If we have a vertex specification add that location to the current
@@ -312,9 +311,13 @@
 % coordinates.  If not, set the point. 
 % 
 %    \begin{macrocode}
-  \ifx\hex at vtx\@empty\pgfpointxy{\hex at x}{\hex at y}\else%
-  \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
-    \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at vtx}{1}}}\fi%
+  \ifx\hex at vtx\@empty\else%
+    \pgfmathparse{\hex at x+\hex at off*cos(\hex at vtx)}\xdef\hex at x{\pgfmathresult}
+    \pgfmathparse{\hex at y+\hex at off*sin(\hex at vtx)}\xdef\hex at y{\pgfmathresult}
+  \fi%
+  % \ifx\hex at vtx\@empty\pgfpointxy{\hex at x}{\hex at y}\else%
+  % \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
+  %   \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at vtx}{1}}}\fi%
 %    \end{macrocode}
 %
 % If we have an edge specification add that location to the current
@@ -322,20 +325,28 @@
 % 
 %    \begin{macrocode}
   \ifx\hex at edg\@empty\else%
-  \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
-    \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at edg}{\hex at yy}}}\fi
+    \pgfmathparse{\hex at x+\hex at off*\hex at yy*cos(\hex at edg)}%
+    \xdef\hex at x{\pgfmathresult}%
+    \pgfmathparse{\hex at y+\hex at off*\hex at yy*sin(\hex at edg)}%
+    \xdef\hex at y{\pgfmathresult}%
+  \fi%
+  % \ifx\hex at edg\@empty\else%
+  % \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
+  %   \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at edg}{\hex at yy}}}\fi
 %    \end{macrocode}
 %
 % For debugging, we can print out stuff. 
 % 
 %    \begin{macrocode}
+  \pgfpointxy{\hex at x}{\hex at y}    
   \hex at dbg{2}{Hex coordinates: #1
-    ^^J c=\hex at col
-    ^^J r=\hex at row
-    ^^J v=\hex at vtx
-    ^^J e=\hex at edg
-    ^^J x=\hex at x
-    ^^J y=\hex at y}%
+    ^^J c=`\hex at col'
+    ^^J r=`\hex at row'
+    ^^J v=`\hex at vtx'
+    ^^J e=`\hex at edg'
+    ^^J o=`\hex at off'
+    ^^J x=`\hex at x'
+    ^^J y=`\hex at y'}%
   \global\let\hex at x\hex at x%
   \global\let\hex at y\hex at y%
   \global\let\hex at row\hex at row%

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/core.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/core.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/core.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -89,6 +89,7 @@
 % \input{hex/extra.dtx}
 % \input{hex/paths.dtx}
 % \input{hex/board.dtx}
+% \input{hex/split.dtx}
 %   \iffalse
 %</hex>   
 %   \fi

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/extra.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/extra.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/extra.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -18,7 +18,7 @@
 %    \begin{macrocode}
 \tikzset{%
   hex/fortress/.pic={
-    \path[draw,pic actions]
+    \path[draw,solid,pic actions]
     (0:  .9) --
     (0:  .7) -- 
     (60: .7) -- ( 60:.9) -- ( 60:.7) --

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/paths.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/paths.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/paths.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -45,7 +45,30 @@
     }
   }%
 }%
-
+%    \end{macrocode}
+% 
+% A decoration to make a fortification line 
+% 
+%    \begin{macrocode}
+\pgfdeclaredecoration{fortification}{initial}
+{
+  \state{initial}[width=4\pgflinewidth]
+  {
+    \pgfpathlineto{\pgfpoint{2\pgflinewidth}{0}}
+    \pgfpathlineto{\pgfpoint{2\pgflinewidth}{2\pgflinewidth}}
+    \pgfpathlineto{\pgfpoint{4\pgflinewidth}{2\pgflinewidth}}
+    \pgfpathlineto{\pgfpoint{4\pgflinewidth}{0}}
+  }
+  \state{final}
+  {
+    \pgfpathlineto{\pgfpointdecoratedpathlast}
+  }
+}
+%    \end{macrocode}
+% 
+% Roads, railroads, rivers, borders. 
+%
+%    \begin{macrocode}
 \tikzset{
   hex/road/.style={
     rounded corners=3\pgflinewidth,% .25cm,
@@ -52,12 +75,15 @@
     color=black,
     transform shape,
     scale line widths,
-    thick},
+    thick,
+    every hex road/.try,
+  },
   hex/railroad/.style={
     %scale line widths,
     rounded corners=.25cm,
     color=gray!50!black,
     transform shape,
+    every hex railroad/.try,
     postaction={draw,decorate},
     decoration={ticks,
       segment length=9\pgflinewidth,
@@ -67,29 +93,42 @@
   hex/river/.style={
     color=blue,
     scale line widths,
+    scale rounded corners,
     line width=3pt,
     transform shape,
+    every hex river/.try,
     decorate,
     decoration={random steps,
-      segment length=.3cm,
-       amplitude=.15cm,
+      segment length=3\pgflinewidth,
+       amplitude=1.5\pgflinewidth,
        pre=lineto,
        post=lineto,
-       pre length=.05cm,
-       post length=.05cm},
-     rounded corners=.08cm},
+       pre length=.5\pgflinewidth,
+       post length=.5\pgflinewidth},
+     rounded corners=.75\pgflinewidth},
    hex/border/.style={
      color=gray,
-     rounded corners=3pt,
      dashed,
      transform shape,
      scale line widths,
-     very thick
+     very thick,
+     rounded corners=3\pgflinewidth,
+     every hex border/.try
    },
-   every river/.style={},
-   every road/.style={},
-   every railroad/.style={},
-   every border/.style={},
+   % 
+   % Fortification line
+   % 
+   hex/fortified line/.style={
+     draw=brown!50!black,
+     scale line widths,
+     line width=2pt,
+     every hex fortification line/.try,
+     decoration={fortification,raise=-2\pgflinewidth},
+     decorate},
+   % every river/.style={},
+   % every road/.style={},
+   % every railroad/.style={},
+   % every border/.style={},
 }
 %    \end{macrocode}
 %
@@ -97,25 +136,30 @@
 %   
 %    \begin{macrocode}
 \def\road{%
-  \hex at dbg{3}{Road}
+  %\hex at dbg{3}{Road}
   \@ifnextchar[{\road@}{\road@[]}%]
 }
 \def\road@[#1]{\draw[hex/road,every hex road/.try,#1]}
 \def\railroad{%
-  \hex at dbg{3}{Rail road}
+  %\hex at dbg{3}{Rail road}
   \@ifnextchar[{\railroad@}{\railroad@[]}%]
 }
 \def\railroad@[#1]{\draw[hex/railroad,every hex railroad/.try,#1]}
 \def\river{%
-  \hex at dbg{3}{River}
+  %\hex at dbg{3}{River}
   \@ifnextchar[{\river@}{\river@[]}%]
 }
-\def\river@[#1]{\draw[hex/river,every hex river/.try,#1]}
+\def\river@[#1]{\draw[hex/river,#1]}
 \def\border{%
   \hex at dbg{3}{Border}
   \@ifnextchar[{\border@}{\border@[]}%]
 }
 \def\border@[#1]{\draw[hex/border,every hex border/.try,#1]}
+\def\fortifiedline{%
+  \@ifnextchar[{\fortifiedline@}{\fortifiedline@[]}%]
+}%
+\def\fortifiedline@[#1]{%
+  \draw[hex/fortified line,every hex fortified line/.try,#1]}
 %    \end{macrocode}
 % \end{Macro}
 %
@@ -209,10 +253,10 @@
       markings,
       mark=between positions 0 and 1 step 0.75*\hex at scale*\hex at dy with {
         \node [single arrow,
-               single arrow head extend=3pt,
+               single arrow head extend=.1*\hex at scale*\hex at dy,
                fill=#1,
-               inner sep=\hex at scale*.5mm,
-               minimum width=\hex at scale*2mm,
+               inner sep=0.05*\hex at scale*\hex at dy,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=\hex at scale*\hex at dy/2,
                transform shape]{};
       }
@@ -236,10 +280,10 @@
       markings,
       mark=between positions 0 and 1 step 0.5*\hex at scale*\hex at dy with {
         \node [single arrow,
-               single arrow head extend=3pt,
+               single arrow head extend=.1*\hex at scale*\hex at dy,
                fill=#1,
-               inner sep=\hex at scale*.5mm,
-               minimum width=\hex at scale*2mm,
+               inner sep=0.05*\hex at scale*\hex at dy,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=\hex at scale*\hex at dy/3,
                transform shape]{};
       }
@@ -270,7 +314,7 @@
                anchor=west,
                inner sep=\hex at scale*.25mm,
                outer sep=.3*\hex at scale*\hex at dy,
-               minimum width=\hex at scale*2mm,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=1.4*\hex at scale*\hex at dy,
                transform shape]{};
       }
@@ -300,10 +344,10 @@
         transform shape] {};},
       mark=between positions 0 and 1 step 0.75*\hex at scale*\hex at dy with {
         \node [single arrow,
-               single arrow head extend=\hex at scale*3pt,
+               single arrow head extend=.1*\hex at scale*\hex at dy,
                fill=#1,
-               inner sep=\hex at scale*1mm,
-               minimum width=\hex at scale*3mm,
+               inner sep=0.05*\hex at scale*\hex at dy,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=\hex at scale*\hex at dy/2,
                transform shape]{};
       }
@@ -331,7 +375,7 @@
     fill=#2,
     transform shape,
     text=#1,
-    font=\sffamily\bfseries\Large},
+    font=\sffamily\bfseries\fontsize{14.4}{17}\selectfont},
   hex/move cost/.default={black}{none},
   % Argument is fill colour 
 %    \end{macrocode}

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/ridges.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/ridges.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/ridges.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -9,23 +9,31 @@
 % A hex can be decorated with up to 6 ridges --- one for each edge of
 % the hexagon.  The first thing is to set up the graphics style to use
 % for the ridges.  We use the \spec{wave} decoration. 
+%
+% If rounded corners are set for ridges, (e.g., via \spec{every hex
+% ridges}), then it should be  \spec{0pt} or \spec{4pt} (roughly 2mm)
+% or larger.  Otherwise, one will get a ``dimension too large''
+% error. 
 % 
 %    \begin{macrocode}
 \tikzset{%
-  hex/ridges/.style={
+  hex/ridges pre/.style={
     line cap=round,
     draw=pgfstrokecolor,
-    rounded corners=.25cm,
-    scale line widths,
+    solid,
+    /hex/ridges/.cd,%
+    radius=0.85,%
+    n=4,
+    R=.25,
+  },
+  hex/ridges/.style={
+    get scale,
     decoration={
       path has corners=true,
-      pre=waves,
-      post=waves,
-      pre length=-.1cm,
-      post length=0cm,
       waves,
-      radius=.2cm,
-      segment length=.2cm},
+      radius=\wg at scale\hex at r@R,
+      segment length=\wg at scale\hex at r@s,
+    },
     decorate}}
 %    \end{macrocode}
 %
@@ -57,6 +65,7 @@
   south/.is if=hex at r@s,
   south east/.is if=hex at r@se,
   radius/.store in=\hex at r@r,
+  curve radius/.store in=\hex at r@w,
   NE/.is if=hex at r@ne,
   N/.is if=hex at r@n,
   NW/.is if=hex at r@nw,
@@ -64,6 +73,8 @@
   S/.is if=hex at r@s,
   SE/.is if=hex at r@se,
   r/.store in=\hex at r@r,
+  n/.store in=\hex at r@n,
+  R/.store in=\hex at r@w,
 }
 %    \end{macrocode}
 %
@@ -74,10 +85,11 @@
 %   the routine below is handcrafted since it is relatively simple.
 %   
 %    \begin{macrocode}
+\newdimen\hex at r@s
+\newdimen\hex at r@R
 \def\hex at do@ridges{%
   \edef\hex at r@tmp{[
-    /hex/ridges/.cd,%
-    radius=0.8,%
+    hex/ridges pre,
     /tikz/every hex ridges/.try,
     \hex at ridges]}
   \expandafter\scope\hex at r@tmp%
@@ -89,44 +101,70 @@
       ^^Jsouth     =\ifhex at r@s  yes\else no\fi
       ^^Jsouth east=\ifhex at r@se yes\else no\fi
       ^^Jradius    =\hex at r@r
+      ^^Jn         =\hex at r@n
     }
+    \pgfmathparse{\hex at r@r/\hex at r@n}\xdef\hex at r@t{\pgfmathresult}
+    \hex at r@s=\hex at r@t cm
+    \hex at r@R=\hex at r@w cm
     \def\hex at r@p{}
     % Hand written algorithm
     \ifhex at r@ne
-      \def\hex at r@p{(0:\hex at r@r)--(60:\hex at r@r)}
+      \ifhex at r@se
+        \xdef\hex at r@p{(0:\hex at r@r)--(60:\hex at r@r)}
+      \else
+        \xdef\hex at r@p{($(0:\hex at r@r)+(-60:\hex at r@t/2)$)--(60:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along north east edge: `\hex at r@p'}
     \fi
     \ifhex at r@n
-      \hex at dbg{4}{Ridge along north edge: `\hex at r@p'}
       \ifhex at r@ne\else
-        \xdef\hex at r@p{\hex at r@p ( 60:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($( 60:\hex at r@r)+(0:\hex at r@t/2)$)}
       \fi
       \xdef\hex at r@p{\hex at r@p --(120:\hex at r@r)}
+      \hex at dbg{4}{Ridge along north edge: `\hex at r@p'}
     \fi
     \ifhex at r@nw
       \ifhex at r@n\else
-        \xdef\hex at r@p{\hex at r@p (120:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(120:\hex at r@r)+(60:\hex at r@t/2)$)}
       \fi
       \xdef\hex at r@p{\hex at r@p --(180:\hex at r@r)}
+      \hex at dbg{4}{Ridge along north west: `\hex at r@p'}
     \fi
     \ifhex at r@sw
       \ifhex at r@nw\else
-        \xdef\hex at r@p{\hex at r@p (180:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(180:\hex at r@r)+(120:\hex at r@t/2)$)}
       \fi
-      \xdef\hex at r@p{\hex at r@p --(240:\hex at r@r)}
+      \ifhex at r@s  
+        \xdef\hex at r@p{\hex at r@p --(240:\hex at r@r)}
+      \else
+        \xdef\hex at r@p{\hex at r@p --(240:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along south west: `\hex at r@p'}
     \fi
     \ifhex at r@s
       \ifhex at r@sw\else
-        \xdef\hex at r@p{\hex at r@p (240:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(240:\hex at r@r)+(-\hex at r@t/2,0)$)}
       \fi
-      \xdef\hex at r@p{\hex at r@p --(300:\hex at r@r)}
+      \ifhex at r@se
+        \xdef\hex at r@p{\hex at r@p --(300:\hex at r@r)}
+      \else
+        \xdef\hex at r@p{\hex at r@p --(300.5:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along south: `\hex at r@p'}
     \fi
     \ifhex at r@se
       \ifhex at r@s\else
-        \xdef\hex at r@p{\hex at r@p (300:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(300:\hex at r@r)+(-120:\hex at r@t/2)$)}
       \fi
-      \xdef\hex at r@p{\hex at r@p --(360:\hex at r@r)}
+      \ifhex at r@ne
+        \xdef\hex at r@p{\hex at r@p --cycle}
+      \else
+        \xdef\hex at r@p{\hex at r@p --(.5:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along se: `\hex at r@p'}
     \fi
     \hex at dbg{3}{ Ridges path: \hex at r@p}
+    % \draw[red] \hex at r@p;
     \draw[hex/ridges] \hex at r@p;
   \endscope% End of ridges scope 
 }

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/shape.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/shape.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/shape.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -14,6 +14,77 @@
 % such as terrain and so on. 
 % 
 %    \begin{macrocode}
+\tikzset{%
+  /hex/.cd,
+  bev/.store in=\hex at bevel,               bev/.initial=,
+  bevel fraction/.store in=\hex at bevel@frac,bevel fraction/.initial=10,
+  bevel/.is choice,
+  bevel/none/.style       = {/hex/bev=},
+  bevel/north west/.style = {/hex/bev=1},
+  bevel/north east/.style = {/hex/bev=2},
+  bevel/south west/.style = {/hex/bev=3},
+  bevel/south east/.style = {/hex/bev=4},
+  bevel/NW/.style         = {/hex/bev=1},
+  bevel/NE/.style         = {/hex/bev=2},
+  bevel/SW/.style         = {/hex/bev=3},
+  bevel/SE/.style         = {/hex/bev=4},
+  bevel/.default          = {north west},
+}
+\def\hex at bevel@frac{10}
+\tikzset{
+  hex/bevel highlight/.style={fill=white,opacity=.25},
+  hex/bevel shadow/.style={fill=black,opacity=.25},
+}
+%    \end{macrocode}
+% 
+%
+%
+%    \begin{macrocode}
+\newdimen\wg at tmpe
+\newdimen\wg at tmpf
+\newdimen\wg at tmpg
+\def\hex at bevel@path#1{%
+  \scope[#1]
+  \wg at tmpe=\wg at tmpa\multiply\wg at tmpe by \hex at bevel@frac
+  \wg at tmpf=\wg at tmpb\multiply\wg at tmpf by \hex at bevel@frac
+  \wg at tmpg=\wg at tmpc\multiply\wg at tmpg by \hex at bevel@frac
+  \divide\wg at tmpe100
+  \divide\wg at tmpf100
+  \divide\wg at tmpg100
+  % Start
+  \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Left
+  \pgfpathlineto{\pgfqpoint{-\wg at tmpa}{\wg at tmpb}}%
+  % Left-down 
+  \pgfpathlineto{\pgfqpoint{\wg at tmpc}{\wg at tmpd}}%
+  % Right down
+  \wg at tmpa=-\wg at tmpa%
+  \wg at tmpb=-\wg at tmpb%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Up, in
+  \advance\wg at tmpa\wg at tmpe%
+  \advance\wg at tmpb\wg at tmpf%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Left-down, in
+  \advance\wg at tmpc-\wg at tmpg
+  \pgfpathlineto{\pgfqpoint{\wg at tmpc}{\wg at tmpd}}%
+  % Left, down in
+  \advance\wg at tmpb-\wg at tmpf\wg at tmpb-\wg at tmpb%
+  \advance\wg at tmpb-\wg at tmpf
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Start, down in
+  \advance\wg at tmpa-\wg at tmpe\wg at tmpa-\wg at tmpa%
+  \advance\wg at tmpa-\wg at tmpe
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % %
+  \pgfclosepath%
+  \pgfusepath{fill}
+  \endscope}%
+%    \end{macrocode}
+% 
+%
+%
+%    \begin{macrocode}
 \hex at dbg{5}{Base vertex: \hex at xx,\hex at yy}
 \hex at dbg{5}{Base edges: \hex at e@xx,\hex at e@yy}
 \pgfdeclareshape{hex/hex}{%
@@ -187,6 +258,40 @@
 %    \begin{macrocode}
       \@ifundefined{hex at label}{\let\hex at label\empty}{}
       \ifx\hex at label\empty\else\hex at do@label\fi%
+%    \end{macrocode}
+% 
+%    \begin{macrocode}
+    \@ifundefined{hex at bevel}{\let\hex at bevel\empty}{}
+    \ifx\hex at bevel\empty\else%
+      \northeast
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \west
+      \wg at tmpc=\pgf at x\wg at tmpd=\pgf at y%
+      \ifcase\hex at bevel\relax
+      \or%1
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%2
+      \or\wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%3
+      \or% 4
+        \wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%
+        \wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%
+      \fi                                
+      \hex at bevel@path{chit/bevel highlight}
+      \northeast
+      \wg at tmpa=-\pgf at x\wg at tmpb=-\pgf at y%
+      \west
+      \wg at tmpc=-\pgf at x\wg at tmpd=-\pgf at y%
+      \ifcase\hex at bevel\relax
+      \or%1
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%2
+      \or\wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%3
+      \or% 4
+        \wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%
+        \wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%
+      \fi                                
+      \hex at bevel@path{chit/bevel shadow}
+    \fi      
+%    \end{macrocode}
+%    \begin{macrocode}
     \endscope%
 %    \end{macrocode}
 %

Added: trunk/Master/texmf-dist/source/latex/wargame/hex/split.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/split.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/split.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,517 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*hex>
+% \fi
+% 
+% \subsubsection{Board splitting}
+% \label{sec:impl:hex:split}
+%
+% \begin{Macro}{\splitboard}
+% 
+%   Calculates how to split a board into sheets of paper. 
+%   
+%   \begin{Syntax}
+%     \cs{splitboard}\oarg{options}
+%   \end{Syntax}
+%
+%  where options are
+%
+%  \begin{itemize}
+%  \item \spec{paper}=\meta{format}: Specifies the paper format.  One
+%    of \spec{a4}, \spec{a3}, \spec{letter}, \spec{tabloid}. Default
+%    is \spec{a4}.
+%  \item \spec{landscape}: Sets the paper format to be in landscape
+%    mode (default is portrait).
+%  \item \spec{margin}=\meta{size in centimetres}: Size of margins on
+%    each sheet in centimetres \emph{without} unit.  That is put
+%    \spec{0.6} for 6mm, \emph{not} \spec{6mm}. Default is \spec{0.6}.
+%    This should be \emph{slightly} larger (by roughly 5\%) than the
+%    \emph{least} margin required by the printer used. \emph{Must} be
+%    given \emph{before} \spec{paper} to have any effect.
+%  \item \spec{ncol}=\meta{number of columns}: Sets the number of
+%    columns of sheets.  
+%  \item \spec{nrow}=\meta{number of rows}: Set the number of rows of
+%    sheets.
+%  \item \spec{overlap}=\meta{size in centimetres}: Sets the size of
+%    the overlap between sheets in centimetres \emph{without} unit.
+%    That is put \spec{2} for 2cm, \emph{not} \spec{2cm}. Default is
+%    \spec{2}. 
+%  \item \spec{image}=\meta{file name}: File name of the board image
+%    (a PDF). Default is \spec{board}
+%  \item \spec{output}=\meta{file name}: File name (without
+%    \spec{.tex} ending) to write calculated split to.
+%  \item \spec{standalone}: Boolean flag.  If true, then output file
+%    will be a standalone document (i.e., has a \cs{documentclass}).
+%  \item \spec{scale}=\meta{scale}: Set scale of board. 
+%  \end{itemize}
+%
+% The macro will produce a file named \cs{jobname}\spec{\_out.tex}
+% which can be included in another document to generate the split
+% board PDF.
+%
+% To use, make, for example, the file \spec{calcsplit.tex} with the
+% content
+%
+% \begin{verbatim}
+%   \documentclass[11pt]{standalone}
+%   \usepackage{wargame}
+%   \usepackage{mystyle}
+%   \begin{document}
+%   \splitboard{paper=letter,margin=.7,ncol=2,nrow=2,overlap=1}
+%   \end{document}
+% \end{verbatim}
+%
+% to calculate the split of \spec{board.pdf} over $2\times2$ letter
+% paper sheets, with a non-printable margin of 7mm, and an overlap
+% between the segments of 1cm.
+%
+% The final split document can then be
+%
+% \begin{verbatim}
+%   \documentclass[11pt]{article}
+%   \usepackage[letterpaper,margin=7mm]{geometry}
+%   \begin{document}
+%   \input{calcsplit_out}
+%   \end{document}
+% \end{verbatim}
+%
+%
+% If you need to scale down the board, define the style \spec{board
+% scale}.  E.g.,
+%
+% \begin{verbatim}
+%   \tikzset{board scale/.style={scale=.9}}
+% \end{verbatim}
+% 
+% \end{Macro}
+%
+%
+% Styles used for drawing things.
+% 
+%    \begin{macrocode}
+\tikzset{%
+  % Margin must be <1cm
+  split/paper outline/.style={
+    shape=rectangle,
+    draw=red!50!black,
+    line width=.5mm},
+  split/effective outline/.style={
+    shape=rectangle,
+    draw=green!50!black,
+    dashed,
+    line width=.5mm},
+  split/board outline/.style={%
+    draw=magenta,
+    line width=.5mm,
+    dotted},
+}     
+%    \end{macrocode}
+%
+% A scratch dimension used
+% 
+%    \begin{macrocode}
+\newdimen\split at tmp   
+%    \end{macrocode}
+%
+% Get upper right and lower left corners of node. Argument is node name.
+% 
+%    \begin{macrocode}
+\def\split at getem#1{%
+  \draw (#1.north east);%  
+  \pgfgetlastxy{\split at ulx}{\split at uly}%
+  \xdef\split at ulx{\split at ulx}%
+  \xdef\split at ulx{\split at ulx}%
+  \draw (#1.south west);%  
+  \pgfgetlastxy{\split at lrx}{\split at lry}%
+  \xdef\split at lrx{\split at lrx}%
+  \xdef\split at lry{\split at lry}%
+}   
+%    \end{macrocode}
+%
+% Get board dimensions. Argument is node name. 
+% 
+%    \begin{macrocode}
+\def\split at getboard#1{%
+  \split at getem{#1}%
+  \xdef\split at bulx{\split at ulx}%
+  \xdef\split at buly{\split at uly}%
+  \xdef\split at blrx{\split at lrx}%
+  \xdef\split at blry{\split at lry}%
+  \split at w{\@percentchar\space Board:
+    (\split at bulx,\split at buly)(\split at blrx,\split at blry)}}   
+%    \end{macrocode}
+%
+% Adjust placement of markers and cut lines.
+% \begin{enumerate}
+% \item Dimension to adjust
+% \item Overlap dimension (with units)
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\split at adj#1#2{%
+  \split at tmp=#2%
+  \divide\split at tmp by 2%
+  \advance\split at tmp by #1%
+  \edef\t{\the\split at tmp}}   
+%    \end{macrocode}
+%
+% Get initial offset in a direction.
+%
+% \begin{enumerate}
+% \item Number of segments in direction
+% \item Overlap in centimetres (without unit)
+% \item Effective size, in centimetres (without unit), of sheets in
+%   direction
+% \item Full size, in centimetres (without unit), of board in
+%   direction. 
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\split at get@init#1#2#3#4{%
+  \pgfmathparse{((#1 * #3 - (#1 - 1) * #2) - #4)/2}%
+  \xdef\split at off{\pgfmathresult}%
+  \hex at dbg{2}{((#1 * #3 - (#1 - 1) * #2) - #4)/2 -> `\split at off'}}   
+%    \end{macrocode}
+%
+% Get initial offset of first segment.
+%
+% \begin{enumerate}
+% \item Number of rows 
+% \item Number of columns
+% \item Overlap in centimetres (without unit)
+% \item Effective height, in centimetres (without unit), of sheets
+% \item Effective width, in centimetres (without unit), of sheets
+% \item Full height, in centimetres (without unit), of board
+% \item Full width, in centimetres (without unit), of board
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\split at getinit#1#2#3#4#5#6#7{%
+  \split at get@init{#1}{#3}{#4}{#6}\xdef\dy{\split at off cm}
+  \split at get@init{#2}{#3}{#5}{#7}\xdef\dx{\split at off cm}}   
+%    \end{macrocode}
+%
+% Get coordinates of a segment
+%
+% \begin{enumerate}
+% \item Column number
+% \item Row number
+% \item Overlap, in centimetres (without unit)
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\split at getcoords#1#2#3{%
+  \hex at dbg{2}{Getting coords `c#1r#2'}%
+  \split at getem{c#1r#2}%
+  \edef\sulx{\split at ulx}%
+  \edef\suly{\split at uly}%
+  \edef\slrx{\split at lrx}%
+  \edef\slry{\split at lry}%
+  \edef\mlx{\split at blrx}%
+  \edef\mrx{\split at bulx}%
+  \edef\mty{\split at buly}%
+  \edef\mby{\split at blry}%
+  \pgfmathparse{int(#1-1)}\edef\pc{\pgfmathresult}%
+  \pgfmathparse{int(#2-1)}\edef\pr{\pgfmathresult}%
+  \pgfmathparse{int(#1+1)}\edef\nc{\pgfmathresult}%
+  \pgfmathparse{int(#2+1)}\edef\nr{\pgfmathresult}%
+  \pgfutil at ifundefined{pgf at sh@ns at c\pc r#2}{}{% Left
+    \hex at dbg{3}{\space Getting left `c\pc r#2'}%
+    \split at getem{c\pc r#2}\split at adj{\split at ulx}{-#3}\edef\mlx{\t}}%
+  \pgfutil at ifundefined{pgf at sh@ns at c\nc r#2}{}{% Right
+    \hex at dbg{3}{\space Getting right `c\nc r#2'}%
+    \split at getem{c\nc r#2}\split at adj{\split at lrx}{#3}\edef\mrx{\t}}%
+  \pgfutil at ifundefined{pgf at sh@ns at c#1r\pr}{}{% Above
+    \hex at dbg{3}{\space Getting above `c#1 r\pr'}%
+    \split at getem{c#1r\pr}\split at adj{\split at lry}{#3} \edef\mty{\t}}%
+  \pgfutil at ifundefined{pgf at sh@ns at c#1r\nr}{}{% Below
+    \hex at dbg{3}{\space Getting below `c#1 r\nr'}%
+    \split at getem{c#1r\nr}\split at adj{\split at uly}{-#3}\edef\mby{\t}}%
+  \draw[fill=red]  (\mlx,\mty) circle(.2);%
+  \draw[fill=green](\mrx,\mty) circle(.4);%
+  \draw[fill=blue] (\mlx,\mby) circle(.6);%
+  \draw[fill=cyan] (\mrx,\mby) circle(.8);%
+  \split at w{%
+    \@percentchar^^J%
+    \string\segment(\sulx,\suly)(\slrx,\slry){\mlx}{\mrx}{\mby}{\mty}
+    \@percentchar\space c#1r#2}
+}   
+%    \end{macrocode}
+%
+% Stream to write to
+% 
+%    \begin{macrocode}
+\newwrite\split at calcout   
+%    \end{macrocode}
+%
+% Short-hand for write outs.
+% 
+%    \begin{macrocode}
+\def\split at w{\immediate\write\split at calcout}   
+%    \end{macrocode}
+%
+% Open stream and set-up
+% 
+%    \begin{macrocode}
+\def\split at header#1{%
+  \immediate\openout\split at calcout=#1.tex
+  \ifsplit at standalone
+  \pgfmathparse{\split at margin*.95}\edef\tmp{\pgfmathresult}
+  \split at w{\@percentchar\@percentchar\space These are made with
+    `calcsplit' with `-jobname \jobname'}
+  \split at w{
+    ^^J\string\documentclass[twoside]{article}
+    ^^J\string\usepackage{geometry}
+    ^^J\string\geometry{papersize={\the\paperwidth,\the\paperheight},margin=\tmp cm}
+    ^^J\string\usepackage{wargame}
+    ^^J\string\setlength{\string\parindent}{0pt}
+    ^^J\string\setlength{\string\parskip}{0pt}
+    ^^J\string\begin{document}
+    ^^J\string\ignorespaces\@percentchar}
+  \fi
+  \split at w{\string\def\string\boardfile{\split at img}\@percentchar}
+  \split at w{\string\def\string\boardscale{\split at scale}\@percentchar}
+}   
+%    \end{macrocode}
+%
+% Write final stuff and close stream
+% 
+%    \begin{macrocode}
+\def\split at footer{%
+  \ifsplit at standalone
+  \split at w{^^J\string\end{document}}
+  \fi
+  \split at w{^^J\@percentchar\@percentchar End of `\jobname'^^J}
+  \immediate\closeout\split at calcout
+}   
+%    \end{macrocode}
+%
+% Initial calculations.  This draws the board and then extracts the
+% dimensions of the board.  It also defines some styles for drawing
+% the board segments. 
+% 
+%    \begin{macrocode}
+\def\split at init#1{%
+  \node[scale=\split at scale,
+  inner sep=0pt,
+  outer sep=0pt,
+  anchor=north west,
+  transform shape](b){\includegraphics{#1}};
+  \split at getboard{b}
+  %x
+  \split at tmp=\split at blrx cm\advance\split at tmp by -\split at bulx%
+  \wg at pt@to at cm{\split at tmp}\edef\split at bw{\pgfmathresult}%
+  \pgfmathparse{abs(\split at bw)}\edef\split at bw{\pgfmathresult}%
+  %
+  \split at tmp=\split at buly cm\advance\split at tmp by -\split at blry%
+  \wg at pt@to at cm{\split at tmp}\edef\split at bh{\pgfmathresult}%
+  \pgfmathparse{abs(\split at bh)}\edef\split at bh{\pgfmathresult}%
+  %
+  \wg at pt@to at cm{\paperwidth}\edef\split at pw{\pgfmathresult}%
+  \wg at pt@to at cm{\paperheight}\edef\split at ph{\pgfmathresult}%
+  %
+  \wg at pt@to at cm{\textwidth}\edef\split at ew{\pgfmathresult}%
+  \wg at pt@to at cm{\textheight}\edef\split at eh{\pgfmathresult}%
+  %
+  \hex at dbg{1}{Board:
+    (\split at bulx,\split at buly)(\split at blrx,\split at blry) \split at bw x\split at bh
+    ^^JPaper: \split at pw x\split at ph
+    ^^JEffective: \split at ew x\split at eh
+  }
+  \tikzset{
+    split/paper size/.style={
+      shape=rectangle,
+      minimum width=\paperwidth,
+      minimum height=\paperheight,
+      split/paper outline,
+    },
+    split/effective size/.style={
+      shape=rectangle,
+      minimum width=\textwidth,
+      minimum height=\textheight,
+      split/effective outline},
+    split/board size/.style={
+      shape=rectangle,
+      minimum width=\split at bw cm,
+      minimum height=\split at bh cm,
+      split/board outline}}
+  \node[board/.try,split/board size,anchor=north west] {};
+}   
+%    \end{macrocode}
+%
+% Calculate effective sheet sizes from sheet dimensions and the
+% defined margin.
+% 
+%    \begin{macrocode}
+\def\split at text@dim#1{%
+  \textwidth=\paperwidth%
+  \textheight=\paperheight%
+  \advance\textwidth by -#1cm%
+  \advance\textwidth by -#1cm%
+  \advance\textheight by -#1cm%
+  \advance\textheight by -#1cm%
+  \global\textwidth=\textwidth%
+  \global\textheight=\textheight%
+}   
+%    \end{macrocode}
+%
+% Options for the \cs{splitboard} macro.
+% 
+%    \begin{macrocode}
+\newif\ifsplit at standalone\split at standalonetrue
+\tikzset{%
+  split/.search also={/tikz},%
+  split/.cd,%
+  margin/.store in=\split at margin,
+  paper/.is choice,%
+  paper/a4/.code={%
+    \hex at dbg{3}{A4 paper for split}%
+    \global\paperwidth=21cm%
+    \global\paperheight=29.7cm%
+    \split at text@dim{\split at margin}},
+  paper/a3/.code={%
+    \hex at dbg{3}{A3 paper for split}%
+    \global\paperheight=42cm%
+    \global\paperwidth=29.7cm%
+    \split at text@dim{\split at margin}},
+  paper/letter/.code={%
+    \hex at dbg{3}{Letter paper for split}
+    \paperheight=27.9cm,%
+    \paperwidth=21.6cm,%
+    \split at text@dim{\split at margin}},%
+  paper/tabloid/.code={%
+    \hex at dbg{3}{Tabloid paper for split}%
+    \paperheight=43.2cm,%
+    \paperwidth=27.9cm,%
+    \split at text@dim{\split at margin}},
+  landscape/.code={%
+    \hex at dbg{3}{Landscape option for split}
+    \split at tmp=\paperheight
+    \global\paperheight=\paperwidth
+    \global\paperwidth=\split at tmp
+    \split at tmp=\textheight
+    \global\textheight=\textwidth
+    \global\textwidth=\split at tmp},
+  standalone/.is if=split at standalone,
+  scale/.store in=\split at scale,
+  output/.store in=\split at out,
+  ncol/.store in=\split at ncol,
+  nrow/.store in=\split at nrow,
+  overlap/.store in=\split at ov, % Centimeter, no unit 
+  image/.store in=\split at img,
+  paper/.default=a4,  paper/.initial=a4,
+  margin/.default=.6, margin/.initial=.6,
+  ncol/.default=0,    ncol/.initial=0,
+  nrow/.default=0,    nrow/.initial=0,
+  overlap/.default=2, overlap/.initial=2,
+  image/.default=board, image/.initial=board,
+  output/.default=\jobname_out,
+  standalone/.default=true,
+  scale/.default=1,
+}   
+%    \end{macrocode}
+%
+% The actual macro. The argument is key-value pairs of options. 
+% 
+%    \begin{macrocode}
+\def\splitboard#1{%
+  \pgfkeys{/tikz/split/.cd,%
+    standalone,%
+    output,%
+    margin,%
+    paper,%
+    image,%
+    overlap,%
+    scale,%
+    ncol,%
+    nrow,%
+    #1}
+  \hex at dbg{1}{%
+       Paper:     `\the\paperwidth'x`\the\paperheight'
+    ^^JEffective: `\the\textwidth'x`\the\textheight'
+    ^^JNcols:     `\split at ncol'
+    ^^JNrows:     `\split at nrow'
+    ^^JOverlap:   `\split at ov' cm}
+  \split at header{\split at out}
+  \begin{tikzpicture}
+    \split at init{\split at img}
+    \split at getinit{%
+      \split at nrow}{%
+      \split at ncol}{%
+      \split at ov}{\split at eh}{\split at ew}{\split at bh}{\split at bw}
+    \node[split/effective size,
+    above left=\dy and \dx of b.north west,
+    anchor=north west] (c1r1) {};
+    \node[split/paper size] at (c1r1) {};
+    % 
+    \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split at nrow}{%
+      \ifnum\r>1
+        \hex at dbg{3}{Placing first column of row `\r'}
+        \node[split/effective size,
+        below=-\split at ov cm of c1r\pr.south west,anchor=north west] (c1r\r){};
+        \node[split/paper size] at (c1r\r) {};
+      \fi
+      \foreach \c [remember=\c as \pc (initially 1)] in {2,...,\split at ncol}{%
+        \ifnum\c>\split at ncol\else%
+          \ifnum\c>\pc
+          \hex at dbg{3}{Placing column `\c' (`\pc') of row `\r'}
+            \node[split/effective size,
+            right=-\split at ov cm of c\pc r\r.north east,anchor=north west]
+            (c\c r\r) {};
+            \node[split/paper size] at (c\c r\r) {};
+          \fi
+        \fi
+      }
+    }
+    \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split at nrow}{%
+      \foreach \c [remember=\c as \pc (initially 0)] in {1,...,\split at ncol}{%
+        \split at getcoords{\c}{\r}{\split at ov cm}}}
+  \end{tikzpicture}
+  \split at footer
+}   
+%    \end{macrocode}
+%
+% Macro used by the written file.
+%
+%
+% \begin{enumerate}
+% \item first coordinate (e.g., \spec{(hex ak:c=C,r=17)})
+% \item second coordinate (e.g., \spec{(hex ak:c=M,r=33)})
+% \item Crop mark left
+% \item Crop mark right
+% \item Crop mark bottom
+% \item Crop mark top
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\segment(#1)(#2)#3#4#5#6{%
+  \begin{tikzpicture}%
+    \begin{scope}
+      \clip (#1) rectangle (#2);
+      \node[scale=\boardscale,
+      inner sep=0pt,
+      outer sep=0pt,
+      anchor=north west,
+      transform shape]{\includegraphics{\boardfile}};
+    \end{scope}
+    \pgfinterruptboundingbox
+    \draw(#3,#6)--++( 0.0, 0.3);
+    \draw(#3,#6)--++(-0.3, 0.0);
+    \draw(#3,#5)--++( 0.0,-0.3);
+    \draw(#3,#5)--++(-0.3, 0.0);
+    \draw(#4,#6)--++( 0.0, 0.3);
+    \draw(#4,#6)--++( 0.3, 0.0);
+    \draw(#4,#5)--++( 0.0,-0.3);
+    \draw(#4,#5)--++( 0.3, 0.0);
+    \endpgfinterruptboundingbox
+  \end{tikzpicture}%
+  \cleardoublepage}%   
+%    \end{macrocode}
+%
+% 
+%
+% 
+% \iffalse
+%</hex>
+% --------------------------------------------------------------------
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/hex/split.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/beach.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/beach.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/beach.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -21,10 +21,14 @@
 %    \end{macrocode}
 % \end{TikzKey}
 %
-% Now for the actual patterns.  We go in the same order as above ---
-% i.e, we start with the beach pattern.   This is rather long. 
+% \begin{TikzKey}{hex/terrain/beach}
+%   Now for the actual patterns.  We go in the same order as above ---
+%   i.e, we start with the beach pattern.  This is rather long.
 %
-% \begin{TikzKey}{hex/terrain/beach}
+%   \begin{center}
+%     \includegraphics{wargame.beach}
+%   \end{center}
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/city.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/city.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/city.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -5,6 +5,9 @@
 % \begin{TikzKey}{hex/terrain/city}
 %   And finally a city  
 % 
+%   \begin{center}
+%     \includegraphics{wargame.city}
+%   \end{center}
 %
 %    \begin{macrocode}
 \ifhex at terrain@pic

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/light_woods.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/light_woods.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/light_woods.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -19,6 +19,10 @@
 % \begin{TikzKey}{hex/terrain/light woods}
 %   Next, we have light woods. 
 %
+%   \begin{center}
+%     \includegraphics{wargame.light_woods}
+%   \end{center}
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/mountains.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/mountains.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/mountains.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -21,6 +21,10 @@
 %   And the mountains pattern.  This is the same as the beach pattern,
 %   only filled with a darker brown colour.
 %
+%   \begin{center}
+%     \includegraphics{wargame.mountains}
+%   \end{center}
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/rough.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/rough.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/rough.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -19,7 +19,11 @@
 % \begin{TikzKey}{hex/terrain/rough}
 %   Roughs. Again, a bit long. 
 % 
+%   \begin{center}
+%     \includegraphics{wargame.rough}
+%   \end{center}
 %
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/swamp.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/swamp.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/swamp.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -18,6 +18,10 @@
 % \begin{TikzKey}{hex/terrain/swamp}
 %   Swamps.  This is probably the shortest of the terrain patterns.
 %
+%   \begin{center}
+%     \includegraphics{wargame.swamp}
+%   \end{center}
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/town.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/town.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/town.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -5,7 +5,11 @@
 % \begin{TikzKey}{hex/terrain/town}
 %   A town.
 % 
+%   \begin{center}
+%     \includegraphics{wargame.town}
+%   \end{center}
 %
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/village.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/village.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/village.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -9,6 +9,10 @@
 %   houses, and separate styles for regular and small roads.  Note
 %   that we draw using the stroke colour for roads and houses.
 %
+%   \begin{center}
+%     \includegraphics{wargame.village}
+%   \end{center}
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/woods.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/woods.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain/woods.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -19,6 +19,10 @@
 % \begin{TikzKey}{hex/terrain/woods}
 %   Regular woods.
 %
+%   \begin{center}
+%     \includegraphics{wargame.woods}
+%   \end{center}
+%
 %    \begin{macrocode}
 \ifhex at terrain@pic
 \tikzset{

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/terrain.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/terrain.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/terrain.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -47,7 +47,7 @@
   pic/.default=,
   image/.default=,
   code/.default=,
-  clip/.default,
+  clip/.default=,
 }
 \iffalse
 \tikzset{
@@ -111,8 +111,9 @@
 %    \begin{macrocode}
     \@ifundefined{hex at t@clip}{\let\hex at t@clip\empty}{}
     \ifx\hex at t@clip\empty\else%
+      \edef\hex at t@cc{\hex at t@clip}%
       \def\hex at t@c{}
-      \foreach \c in \hex at t@clip{%
+      \foreach \c in \hex at t@cc{%
         \hex at dbg{5}{Clipping to `\c'}
         \expandafter\wg at pic\c\@endwg at pic {}{\wg at tmpa,\wg at tmpb}{%
           save path=\hex at t@tmp}%
@@ -132,8 +133,13 @@
     %% macros are undefined, define them to be empty
     \@ifundefined{hex at t@pic}{\let\hex at t@pic\empty}{}
     \@ifundefined{hex at t@image}{\let\hex at t@image\empty}{}
+    \@ifundefined{hex at t@code}{\let\hex at t@code\empty}{}
+    \@ifundefined{hex at t@code}{\let\hex at t@code\empty}{}
 %    \end{macrocode}
 %
+%    \begin{macrocode}
+    \ifx\hex at t@code\empty\else\hex at t@code\fi%
+%    \end{macrocode}
 % First we check if we have not got terrain images, but terrain
 % pictures.  If we have that, we process these in turn.  Note, the
 % user can give options to each terrain picture by preceding the
@@ -147,7 +153,8 @@
         % We have pictures
         \hex at dbg{5}{Terrain pictures}%
         \pgfpointorigin\wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
-        \wg at pic@all{\hex at t@pic}{}{\the\wg at tmpa,\the\wg at tmpb}{}%
+        \foreach \i in \hex at t@pic{%
+          \wg at pic@all{\i}{}{\the\wg at tmpa,\the\wg at tmpb}{}}%
       \fi% We have pictures.
 %    \end{macrocode}
 %    

Modified: trunk/Master/texmf-dist/source/latex/wargame/hex/towns.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/hex/towns.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/hex/towns.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -13,6 +13,7 @@
 \tikzset{%
   hex/town/.style={
     scale line widths,
+    solid,
     thin,
     fill=pgfstrokecolor,
     color=pgfstrokecolor},
@@ -21,7 +22,7 @@
     shape=rectangle,
     above right=.1,
     color=pgfstrokecolor,
-    font=\sffamily\normalsize}
+    font=\sffamily\fontsize{11}{13}\selectfont}
 }
 %    \end{macrocode}
 %
@@ -46,11 +47,11 @@
 % 
 %    \begin{macrocode}
 \tikzset{%
-  hex/town/village/.pic={\path[fill,pic actions] circle(.1);},
-  hex/town/town/.pic={\path[fill,pic actions] circle(.2);},
+  hex/town/village/.pic={\path[fill,solid,pic actions] circle(.1);},
+  hex/town/town/.pic={\path[fill,solid,pic actions] circle(.2);},
   hex/town/city/.pic={%
-    \path[fill,pic actions] circle(.25);
-    \path[draw,pic actions] circle(.35);}
+    \path[fill,solid,pic actions] circle(.25);
+    \path[draw,solid,pic actions] circle(.35);}
 }
 %    \end{macrocode}
 %    

Modified: trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/frames/friendly.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/frames/friendly.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/frames/friendly.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -419,6 +419,30 @@
   }
 }
 %    \end{macrocode}
+%
+%    \begin{macrocode}
+\pgfdeclareshape{natoapp6c friendly none}{%
+  \inheritsavedanchors[from=natoapp6c base]
+  \savedanchor\northeast{%
+    \pgf at x=1.5\n at to@pp at r%
+    \pgf at y=\n at to@pp at r}
+  \anchor{north east}{\northeast}
+  \anchor{north west}{\northeast\pgf at x=-\pgf at x}
+  \anchor{south east}{\northeast\pgf at y=-\pgf at y}
+  \anchor{south west}{\northeast\pgf at x=-\pgf at x\pgf at y=-\pgf at y}
+  \anchor{north}{\northeast\pgf at x=0cm}
+  \anchor{south}{\northeast\pgf at x=0cm\pgf at y=-\pgf at y}
+  \anchor{east}{\northeast\pgf at y=0cm}
+  \anchor{west}{\northeast\pgf at x=-\pgf at x\pgf at y=0cm}
+  \inheritanchor[from=natoapp6c base]{upper}
+  \inheritanchor[from=natoapp6c base]{lower}
+  \inheritanchor[from=natoapp6c base]{left}
+  \inheritanchor[from=natoapp6c base]{right}
+  \inheritanchor[from=natoapp6c base]{center}
+  \backgroundpath{}
+  \behindforegroundpath{}
+}
+%    \end{macrocode}
 % \end{NatoAppFrame}
 % 
 % \iffalse

Modified: trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/shape.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/shape.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/natoapp6c/shape.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -81,6 +81,7 @@
   command/space/.style={cmd=space},
   command/sub surface/.style={cmd=sub surface},
   command/sea mine/.style={cmd=sub surface},
+  command/none/.style={cmd=none},
 }
 %    \end{macrocode}
 % \end{NatoAppKey}
@@ -164,7 +165,11 @@
 % 
 %    \begin{macrocode}
 \tikzset{
-  natoapp6c/parts/.style={draw,shape=rectangle,transform shape},
+  natoapp6c/parts/.style={
+    scale line widths,
+    draw,
+    shape=rectangle,
+    transform shape},
   natoapp6c/main/.style={natoapp6c/parts},
   natoapp6c/modifiers/.style={natoapp6c/parts,scale=.6},
   natoapp6c/lower/.style={natoapp6c/parts},
@@ -607,13 +612,21 @@
 %   
 %    \begin{macrocode}
 \providecommand\natoappmark[2][]{%
-  \tikz[scale=.25,#1]{\natoapp[faction=friendly,command=land,main=#2]}}
+  \tikz[transform shape,
+  scale=.25,
+  baseline=(natoapp6c mark.south east),
+  natoapp6c mark/.try,
+  #1]{%1
+    \node[draw,transform
+    shape,natoapp6c={faction=friendly,command=land,
+      main=#2}] (natoapp6c mark){}}}
+% \natoapp[faction=friendly,command=land,main=#2](0,0)(natoapp6c mark)}}
 %    \end{macrocode}
 % \end{Macro}
 %
 % \begin{Macro}{\echelonmark}
 %    \begin{macrocode}
-\providecommand\echelonmark[2][]{\tikz[scale=.5,#1]{%
+\providecommand\echelonmark[2][]{\tikz[transform shape,scale=.5,#1]{%
     \pic[scale line widths,line width=1pt] {natoapp6c/s/echelon=#2};}}
 %    \end{macrocode}
 % \end{Macro}

Added: trunk/Master/texmf-dist/source/latex/wargame/util/bb.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/bb.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/bb.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,226 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*utils>
+% \fi
+% 
+% \subsubsection{Bounding boxes}
+% 
+%  Bounding box dimensions
+%
+%
+%    \begin{macrocode}
+\newdimen\wg at bb@minx
+\newdimen\wg at bb@miny
+\newdimen\wg at bb@maxx
+\newdimen\wg at bb@maxy
+%    \end{macrocode}
+%
+%
+%
+% Enable or disable bounding box tracking 
+%
+%    \begin{macrocode}
+\newif\ifwg at notrelevantforpathsize\wg at notrelevantforpathsizefalse
+%    \end{macrocode}
+% 
+%
+% \begin{Macro}{wg at resetbb}
+% Reset the bounding box tracking dimensions
+%
+% \begin{macrocode}
+\def\wg at resetbb{%
+  \global\wg at bb@minx=16000pt\relax%
+  \global\wg at bb@miny=16000pt\relax%
+  \global\wg at bb@maxx=-16000pt\relax%
+  \global\wg at bb@maxy=-16000pt\relax%
+}
+%    \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\old at pgf@protocolsize}
+%   Save PGF's bounding box algorithm
+%   
+%    \begin{macrocode}
+\let\old at pgf@protocolsize\pgf at protocolsizes
+%    \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\wg at protocolsizes}
+%   Our bounding box algorithm
+%
+%    \begin{macrocode}
+\def\wg at protocolsizes#1#2{%
+  \old at pgf@protocolsize{#1}{#2}
+  \ifwg at notrelevantforpathsize\else%
+  \ifdim#1<\wg at bb@minx\global\wg at bb@minx#1\fi%
+  \ifdim#1>\wg at bb@maxx\global\wg at bb@maxx#1\fi%
+  \ifdim#2<\wg at bb@miny\global\wg at bb@miny#2\fi%
+  \ifdim#2>\wg at bb@maxy\global\wg at bb@maxy#2\fi%
+  \fi
+}
+%    \end{macrocode}
+% \end{Macro}
+%
+% % \begin{environment}{getbbl}
+%   Environment that tracks the local bounding box
+%
+%    \begin{macrocode}
+\newenvironment{getbbl}{%
+  \wg at resetbb%
+  \wg at notrelevantforpathsizefalse%
+  \global\let\pgf at protocolsizes\wg at protocolsizes}{%
+  \gdef\pgf at sh@ns at L{rectangle}
+  \gdef\pgf at sh@np at L{%
+    \def\southwest{\pgfqpoint{\the\wg at bb@minx}{\the\wg at bb@miny}}%
+    \def\northeast{\pgfqpoint{\the\wg at bb@maxx}{\the\wg at bb@maxy}}%
+  }
+  \gdef\pgf at sh@nt at L{{1}{0}{0}{1}{0pt}{0pt}}
+  \gdef\pgf at sh@pi at L{\pgfpictureid}
+  \global\let\pgf at protocolsizes\old at pgf@protocolsize
+}
+%    \end{macrocode}
+% \end{environment}
+% 
+% \begin{environment}{getbb}
+%   Environment to track global bounding box
+%   
+%    \begin{macrocode}
+\newenvironment{getbb}{%
+  \wg at resetbb%
+  \wg at notrelevantforpathsizefalse%
+  \global\let\pgf at protocolsizes\wg at protocolsizes}{%
+  \gdef\pgf at sh@ns at M{rectangle}
+  \gdef\pgf at sh@np at M{%
+    \def\southwest{\pgfqpoint{\the\wg at bb@minx}{\the\wg at bb@miny}}%
+    \def\northeast{\pgfqpoint{\the\wg at bb@maxx}{\the\wg at bb@maxy}}%
+  }
+  \gdef\pgf at sh@nt at M{{1}{0}{0}{1}{0pt}{0pt}}
+  % \pgfgettransform\pgf at temp%
+  % \xdef\pgf at sh@nt at M{\pgf at temp}
+  % \pgfgettransformentries{\wg at tmp@a}{\wg at tmp@b}{\wg at tmp@c}{\wg at tmp@d}{\pgf at temp}{\pgf at temp}
+  % \message{^^JTransform of M: \meaning\pgf at temp}
+  % \xdef\pgf at sh@nt at M{{\wg at tmp@a}{\wg at tmp@b}{\wg at tmp@c}{\wg at tmp@d}{0pt}{0pt}}%
+  % \message{^^JTransform of M: \meaning\pgf at sh@nt at M}
+  \gdef\pgf at sh@pi at M{\pgfpictureid}
+  \global\let\pgf at protocolsizes\old at pgf@protocolsize
+}
+%    \end{macrocode}
+% \end{environment}
+% 
+% \iffalse
+% --------------------------------------------------------------------
+% \fi
+% 
+% \subsubsection{Some utilities to get bounding boxes and the like}
+%
+%
+% All coordinates, and such are recorded in centimetres.  It is worth
+% remembering that the Tikz coordinate system has the $y$ axis point
+% upward, while typical image software has the $y$ axis point down.
+% \texttt{pdftocairo} typically assumes a 150 PPI (pixels-per-inch)
+% resolution.
+%
+% That means that scaling factor becomes
+%
+% $$
+% \frac{150\mathrm{pixel}}{2.54\mathrm{cm}} =
+% 59.055\frac{\mathrm{pixel}}{\mathrm{cm}}
+% $$
+% 
+% \iffalse Using definition in terms of printers feet - the one to
+% use!
+% 
+% PNG: 1674 x 1101
+% PDF:   "lower left": [-0.02107,-0.02107],
+%        "upper right": [28.31705,18.60843]
+% Width: 28.31705+0.02107 = 28.33812
+% Height: 18.60843+0.02107 = 18.62950
+% Pixel / cm: 1674 / 28.33812 = 59.07237318495369488166
+%             1101 / 18.62950 = 59.09981480984460130438
+%             Average         = 59.08609399739914809302
+%          
+% Calculated = 150 / 2.54 = 59.05511811023622047244
+%
+% Using 1/72.27
+% 
+% "lower left": [-0.02109,-0.02109],
+%  "upper right": [28.321,18.61102]
+%  Width: 28.321+0.02109=28.34209
+%  Height: 18.61102+0.02109=18.63211
+% Pixel / cm: 1674 / 28.34209 = 59.06409866033168337267
+%             1101 / 18.63211 = 59.09153606328000425072
+%             Average         = 59.07781736180584381169
+% \fi
+% 
+% Since we want to write all dimensions in centimetres, we need to be
+% able to convert \texttt{pt} dimensions to centimetres.  We make two
+% macros to do that for us.
+%
+% The exact definition of 1pt is
+%
+% $$1\,\mathrm{pt} = \frac{249}{250}12"\frac{1}{864}=\frac{83}{6000}1"
+% = 0.03513\overline{6}$$ 
+% 
+%    \begin{macrocode}
+% 2.54 / 72.27 = .03514598035145980351
+% \def\wg at pt@to at cm#1{\pgfmathparse{#1 * 0.0351460}}
+\def\wg at pt@to at cm#1{\pgfmathparse{#1 * 0.0351367}}
+\def\ptpoint at to@cm#1#2{%
+  \wg at pt@to at cm{#1}\edef\x{\pgfmathresult}%
+  \wg at pt@to at cm{#2}\edef\y{\pgfmathresult}}
+%    \end{macrocode}
+%
+% The next macro gets an anchors coordinates and stores them (in units
+% of centimetres) in \cs{tmp at x} and \cs{tmp at y}
+%
+%    \begin{macrocode}
+\def\wg at get@nchor#1#2{%
+  \wg at dbg{2}{Get anchor coordinates #1.#2}
+  \pgfpointanchor{#1}{#2}%
+  \wg at dbg{2}{ `\the\pgf at x',`\the\pgf at y'}
+  \pgfgetlastxy\tmp at x\tmp at y%
+  \wg at dbg{2}{ `\tmp at x',`\tmp at y'}
+  \wg at pt@to at cm{\tmp at x}\edef\tmp at x{\pgfmathresult}
+  \wg at pt@to at cm{\tmp at y}\edef\tmp at y{\pgfmathresult}
+}
+%    \end{macrocode}
+%
+% This does the same as above, but transform to the global coordinate
+% system.
+% 
+%    \begin{macrocode}
+\def\wg at get@global at nchor#1#2{%
+  \pgfpointanchor{#1}{#2}%
+  \pgfgetlastxy\tmp at x\tmp at y%
+  \pgfpointtransformed{\pgfpoint{\tmp at x}{\tmp at y}}
+  \pgf at xa=\pgf at x
+  \pgf at ya=\pgf at y
+  %% \message{^^JAnchor #1.#2 @ (\the\pgf at xa,\the\pgf at ya)}
+  \wg at pt@to at cm{\the\pgf at xa}\edef\tmp at x{\pgfmathresult}
+  \wg at pt@to at cm{\the\pgf at ya}\edef\tmp at y{\pgfmathresult}
+}
+%    \end{macrocode}
+%
+% This records the bounding box given by a named node.  The result is
+% stored in the macros \cs{llx}, \cs{lly}, \cs{urx}, and \cs{ury}. 
+% 
+%    \begin{macrocode} 
+\def\wg at get@bb#1{%
+  \wg at get@nchor{#1}{south west}
+  \edef\llx{\tmp at x}
+  \edef\lly{\tmp at y}
+  \wg at get@nchor{#1}{north east}
+  \edef\urx{\tmp at x}
+  \edef\ury{\tmp at y}
+}
+\def\wglogbb#1{%
+  \wg at get@bb{#1}%
+  \message{^^J`#1' BB: (\llx,\lly) x (\urx,\ury)^^J}} 
+%    \end{macrocode}
+%
+%
+% 
+% \iffalse
+% --------------------------------------------------------------------
+%</utils>
+%\fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/util/bb.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/wargame/util/compound.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/compound.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/compound.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,242 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*utils>
+% \fi
+% 
+% \subsubsection{Pictures in compound nodes}
+%
+% \begin{Macro}{\wg at pic}
+%   The macro \cs{wg at pic} will render a \texttt{pic}. This is used by
+%   the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} node
+%   shapes extensively.
+%
+%   The arguments are
+%   \begin{enumerate}
+%   \item Prefix
+%   \item Position
+%   \item Fixed options
+%   \item User options
+%   \item Picture. 
+%   \end{enumerate}
+%
+%   That is, the macro expects calls like 
+%   \begin{Syntax}
+%     \cs{wg at pic}\oarg{options}\meta{picture}\cs{@endwg at pic}\marg{prefix}\marg{position}\marg{options}
+%   \end{Syntax}
+%
+%    Note the \cs{@endwg at pic} at the end of the call to swallow up
+%    \meta{picture}.   Typically this macro is used as
+%
+%    \begin{Syntax}
+%      \cs{edef}\cs{args}\{\meta{something}\}
+%      \cs{expandafter}\cs{wg at pic}\cs{args}\cs{@endwg at pic}\marg{prefix}\parg{position}\marg{options}
+%    \end{Syntax}
+%
+%    where \meta{something} typically expands to \oarg{user
+%    option}\meta{picture}
+%
+%    First, the top-level macro \cs{wg at pic} that looks for user
+%    options.
+%    
+%    \begin{macrocode}
+\def\wg at pic{%
+  \@ifnextchar[{\wg@@pic}{\wg@@pic[]}%]
+}
+%    \end{macrocode}
+%
+% This macro then forwards to \cs{wg@@pic} to gobble up
+% \meta{picture}.
+%
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\wg@@pic[#1]#2\@endwg at pic{%
+  \wg at dbg{2}{Options: `#1', picture: `#2'}%
+  \wg@@@pic{#1}{#2}%
+}
+%    \end{macrocode}
+% 
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \item Prefix
+% \item Coordinates
+% \item Fixed options
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\wg@@@pic#1#2#3#4#5{%
+  \ifx|#2|\wg at dbg{3}{No picture given}%
+  \else%
+    \wg at dbg{3}{^^JWG Pic:
+      ^^J  User options:  #1
+      ^^J  Picture:       #2
+      ^^J  Prefix:        #3
+      ^^J  Coordinates:   #4
+      ^^J  Fixed options: #5}%
+    % \wg at dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
+    \pic[#5,#1] at (#4) {#3#2};%
+    \ifwg at s@ve%
+      \pgf at relevantforpicturesizetrue%
+      \begin{getbbl}%
+        \pic[draw=none,fill=none,transform shape] at (#4) {#3#2};%
+      \end{getbbl}%
+      \wg at dbg{5}{Clipping to local bounding box}%
+      \clip (L.south west) rectangle (L.north east);%
+      \pgf at relevantforpicturesizefalse \global\wg at s@vefalse%
+    \fi
+  \fi%
+  \wg at dbg{3}{End of WG Pic}
+}
+%    \end{macrocode}
+% \end{Macro}
+% 
+%
+% \begin{Macro}{\wg at pic@all}
+%
+%   This macro sets all pictures in a list.
+%   
+%   \begin{enumerate}
+%   \item List
+%   \item Prefix
+%   \item Position
+%   \item Styles
+%   \end{enumerate}
+%
+%
+%    \begin{macrocode}
+\def\wg at pic@all#1#2#3#4{%
+  \wg at dbg{2}{WG picture loop
+    ^^J  List:    \meaning#1
+    ^^J  Prefix:  `#2'
+    ^^J  Position: `#3'
+    ^^J  Styles:   `#4'}
+  \foreach \p in #1{%
+    \wg at dbg{2}{WG picture element: \meaning\p}%
+    \expandafter\wg at pic\p\@endwg at pic {#2}{#3}{#4}%
+  }%
+}
+%    \end{macrocode}
+% \end{Macro}
+% 
+% \iffalse
+% --------------------------------------------------------------------
+% \fi
+% 
+% \subsubsection{Nodes in compound nodes}
+%
+% \begin{Macro}{\wg at node}
+%   The macro \cs{wg at node} will render a \texttt{node}. This can be
+%   used by the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex}
+%   node shapes.
+%
+%   The arguments are
+%   \begin{enumerate}
+%   \item Prefix
+%   \item Position
+%   \item Fixed options
+%   \item User options
+%   \item Body. 
+%   \end{enumerate}
+%
+%   That is, the macro expects calls like 
+%   \begin{Syntax}
+%     \cs{wg at node}\oarg{options}\meta{body}\cs{@endwg at node}\marg{prefix}\marg{position}\marg{options}
+%   \end{Syntax}
+%
+%    Note the \cs{@endwg at node} at the end of the call to swallow up
+%    \meta{body}.   Typically this macro is used as
+%
+%    \begin{Syntax}
+%      \cs{edef}\cs{args}\{\meta{something}\}
+%      \cs{expandafter}\cs{wg at node}\cs{args}\cs{@endwg at node}\marg{prefix}\parg{position}\marg{options}
+%    \end{Syntax}
+%
+%    where \meta{something} typically expands to \oarg{user
+%    option}\meta{body}
+%
+%    First, the top-level macro \cs{wg at node} that looks for user
+%    options.
+%    
+%    \begin{macrocode}
+\def\wg at node{%
+  \@ifnextchar[{\wg@@node}{\wg@@node[]}%]
+}
+%    \end{macrocode}
+%
+% This macro then forwards to \cs{wg@@node} to gobble up
+% \meta{body}.
+%
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\wg@@node[#1]#2\@endwg at node{%
+  \wg at dbg{2}{Options: `#1', body: `#2'}%
+  \wg@@@node{#1}{#2}%
+}
+%    \end{macrocode}
+% 
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \item Prefix
+% \item Coordinates
+% \item Fixed options
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\wg@@@node#1#2#3#4#5{%
+  \ifx|#2|\wg at dbg{3}{No body given}%
+  \else%
+    \wg at dbg{3}{^^JWG Pic:
+      ^^J  User options:  #1
+      ^^J  Body:          #2
+      ^^J  Prefix:        #3
+      ^^J  Coordinates:   #4
+      ^^J  Fixed options: #5}%
+    % \wg at dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
+    \node[#5,#1] at (#4) {#3#2};%
+  \fi%
+  \wg at dbg{3}{End of WG Node}
+}
+%    \end{macrocode}
+% \end{Macro}
+% 
+%
+% \begin{Macro}{\wg at node@all}
+%
+%   This macro sets all pictures in a list.
+%   
+%   \begin{enumerate}
+%   \item List
+%   \item Prefix
+%   \item Position
+%   \item Styles
+%   \end{enumerate}
+%
+%
+%    \begin{macrocode}
+\def\wg at node@all#1#2#3#4{%
+  \wg at dbg{2}{WG picture loop
+    ^^J  List:    \meaning#1
+    ^^J  Prefix:  `#2'
+    ^^J  Position: `#3'
+    ^^J  Styles:   `#4'}
+  \foreach \p in #1{%
+    \wg at dbg{2}{WG picture element: \meaning\p}%
+    \expandafter\wg at node\p\@endwg at node {#2}{#3}{#4}%
+  }%
+}
+%    \end{macrocode}
+% \end{Macro}
+% 
+% \iffalse
+% --------------------------------------------------------------------
+%</utils>
+%\fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/util/compound.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/latex/wargame/util/core.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/core.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/core.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -12,1342 +12,20 @@
 %<*utils>
 %\fi
 %
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
+% \input{util/misc.dtx}
+% \input{util/compound.dtx}
+% \input{util/bb.dtx}
+% \input{util/tikz.dtx}
+% \input{util/randomid.dtx}
+% \input{util/icons.dtx}
 % 
-% \subsubsection{Miscellaneous macros}
-%
-% \begin{Macro}{\wg at dbg}
-% Debugging support.  The counter \cs{wargamedbglvl} sets the debug
-% level.  The package code then uses \cs{wg at dbg} to print out
-% debugging messages.   This macro takes two arguments --- the first
-% is the \emph{least} debug level at which the message is printed, and
-% the second is the message it self.  
-% 
-%    \begin{macrocode}
-\newcount\wargamedbglvl\wargamedbglvl=0
-\def\wg at dbg#1#2{%
-  \ifnum#1>\wargamedbglvl\relax\else\message{^^J#2}\fi}
-%    \end{macrocode}
-% \end{Macro}
-% 
-% \begin{Macro}{\wg at addto@macro}
-%
-%   The macro \cs{wg at addto@macro}\marg{macro}\marg{other} adds the
-%   definition of the macro \meta{other} to the macro \meta{macro}.
-%   This uses the \cs{toks} trick of storing the \emph{tokens} of the
-%   definition of a \meta{macro} and \meta{other} into \spec{@} and
-%   expanding that token into the definition of \meta{macro}.
-%   Effectively, this means that the top-level definition of
-%   \meta{macro} and \meta{other} are expanded (i.e., macros used in
-%   the definition of either macro is \emph{not} expanded) and then
-%   that becomes the new definition of \meta{macro}.
-%
-%   We will use this macro to do \emph{shallow} definitions of macros
-%   to contain keys and such.
-% 
-%    \begin{macrocode}
-\long\def\wg at addto@macro#1#2{%
-  \begingroup
-  \toks@\expandafter\expandafter\expandafter{\expandafter#1#2}%
-  \xdef#1{\the\toks@}%
-  \endgroup}
-%    \end{macrocode}
-% \end{Macro}
-%
-%
-% \begin{Macro}{\wg at sub@nchor}
-%   Get anchor from sub node.  We cannot use \cs{pgfpointanchor} since
-%   that returns the anchor coordinates in the global coordinate
-%   system.
-%
-%    \begin{macrocode}
-\def\wg at sub@nchor#1#2{%
-  \wg at dbg{3}{^^JGet `#2' in `#1'}%
-  \@ifundefined{pgf at sh@ns@#1}{%
-    \pgf at x=0cm\pgf at y=0cm}{%
-    \pgf at process{%
-      \csname pgf at sh@ma@#1\endcsname% MW
-      \csname pgf at sh@np@#1\endcsname%
-      \pgf at sh@reanchor{\csname pgf at sh@ns@#1\endcsname}{#2}}}%
-  \wg at dbg{10}{-> \the\pgf at x,\the\pgf at y}%
-}
-%    \end{macrocode}
-% \end{Macro}
-% 
-% 
-% Scratch dimensions
-%
-%    \begin{macrocode}
-\newdimen\wg at tmpa
-\newdimen\wg at tmpb
-\newdimen\wg at tmpc
-\newdimen\wg at tmpd
-%    \end{macrocode}
-%
-%
-% Macro to easy restore a saved path
-%
-%    \begin{macrocode}
-\def\settosave#1{
-  \pgfsyssoftpath at setcurrentpath{#1}}
-%    \end{macrocode}
-%    
 % \iffalse
-% --------------------------------------------------------------------
-% \fi
-% 
-% \subsubsection{Pictures in compound nodes}
-%
-% \begin{Macro}{\wg at pic}
-%   The macro \cs{wg at pic} will render a \texttt{pic}. This is used by
-%   the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} node
-%   shapes extensively.
-%
-%   The arguments are
-%   \begin{enumerate}
-%   \item Prefix
-%   \item Position
-%   \item Fixed options
-%   \item User options
-%   \item Picture. 
-%   \end{enumerate}
-%
-%   That is, the macro expects calls like 
-%   \begin{Syntax}
-%     \cs{wg at pic}\oarg{options}\meta{picture}\cs{@endwg at pic}\marg{prefix}\marg{position}\marg{options}
-%   \end{Syntax}
-%
-%    Note the \cs{@endwg at pic} at the end of the call to swallow up
-%    \meta{picture}.   Typically this macro is used as
-%
-%    \begin{Syntax}
-%      \cs{edef}\cs{args}\{\meta{something}\}
-%      \cs{expandafter}\cs{wg at pic}\cs{args}\cs{@endwg at pic}\marg{prefix}\parg{position}\marg{options}
-%    \end{Syntax}
-%
-%    where \meta{something} typically expands to \oarg{user
-%    option}\meta{picture}
-%
-%    First, the top-level macro \cs{wg at pic} that looks for user
-%    options.
-%    
-%    \begin{macrocode}
-\def\wg at pic{%
-  \@ifnextchar[{\wg@@pic}{\wg@@pic[]}%]
-}
-%    \end{macrocode}
-%
-% This macro then forwards to \cs{wg@@pic} to gobble up
-% \meta{picture}.
-%
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \end{enumerate}
-% 
-%    \begin{macrocode}
-\def\wg@@pic[#1]#2\@endwg at pic{%
-  \wg at dbg{2}{Options: `#1', picture: `#2'}%
-  \wg@@@pic{#1}{#2}%
-}
-%    \end{macrocode}
-% 
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \item Prefix
-% \item Coordinates
-% \item Fixed options
-% \end{enumerate}
-% 
-%    \begin{macrocode}
-\def\wg@@@pic#1#2#3#4#5{%
-  \ifx|#2|\wg at dbg{3}{No picture given}%
-  \else%
-    \wg at dbg{3}{^^JWG Pic:
-      ^^J  User options:  #1
-      ^^J  Picture:       #2
-      ^^J  Prefix:        #3
-      ^^J  Coordinates:   #4
-      ^^J  Fixed options: #5}%
-    % \wg at dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
-    \pic[#5,#1] at (#4) {#3#2};%
-    \ifwg at s@ve%
-      \pgf at relevantforpicturesizetrue%
-      \begin{getbbl}%
-        \pic[draw=none,fill=none,transform shape] at (#4) {#3#2};%
-      \end{getbbl}%
-      \wg at dbg{5}{Clipping to local bounding box}%
-      \clip (L.south west) rectangle (L.north east);%
-      \pgf at relevantforpicturesizefalse \global\wg at s@vefalse%
-    \fi
-  \fi%
-  \wg at dbg{3}{End of WG Pic}
-}
-%    \end{macrocode}
-% \end{Macro}
-% 
-%
-% \begin{Macro}{\wg at pic@all}
-%
-%   This macro sets all pictures in a list.
-%   
-%   \begin{enumerate}
-%   \item List
-%   \item Prefix
-%   \item Position
-%   \item Styles
-%   \end{enumerate}
-%
-%
-%    \begin{macrocode}
-\def\wg at pic@all#1#2#3#4{%
-  \wg at dbg{2}{WG picture loop
-    ^^J  List:    \meaning#1
-    ^^J  Prefix:  `#2'
-    ^^J  Position: `#3'
-    ^^J  Styles:   `#4'}
-  \foreach \p in #1{%
-    \wg at dbg{2}{WG picture element: \meaning\p}%
-    \expandafter\wg at pic\p\@endwg at pic {#2}{#3}{#4}%
-  }%
-}
-%    \end{macrocode}
-% \end{Macro}
-% 
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% 
-% \subsubsection{Nodes in compound nodes}
-%
-% \begin{Macro}{\wg at node}
-%   The macro \cs{wg at node} will render a \texttt{node}. This can be
-%   used by the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex}
-%   node shapes.
-%
-%   The arguments are
-%   \begin{enumerate}
-%   \item Prefix
-%   \item Position
-%   \item Fixed options
-%   \item User options
-%   \item Body. 
-%   \end{enumerate}
-%
-%   That is, the macro expects calls like 
-%   \begin{Syntax}
-%     \cs{wg at node}\oarg{options}\meta{body}\cs{@endwg at node}\marg{prefix}\marg{position}\marg{options}
-%   \end{Syntax}
-%
-%    Note the \cs{@endwg at node} at the end of the call to swallow up
-%    \meta{body}.   Typically this macro is used as
-%
-%    \begin{Syntax}
-%      \cs{edef}\cs{args}\{\meta{something}\}
-%      \cs{expandafter}\cs{wg at node}\cs{args}\cs{@endwg at node}\marg{prefix}\parg{position}\marg{options}
-%    \end{Syntax}
-%
-%    where \meta{something} typically expands to \oarg{user
-%    option}\meta{body}
-%
-%    First, the top-level macro \cs{wg at node} that looks for user
-%    options.
-%    
-%    \begin{macrocode}
-\def\wg at node{%
-  \@ifnextchar[{\wg@@node}{\wg@@node[]}%]
-}
-%    \end{macrocode}
-%
-% This macro then forwards to \cs{wg@@node} to gobble up
-% \meta{body}.
-%
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \end{enumerate}
-% 
-%    \begin{macrocode}
-\def\wg@@node[#1]#2\@endwg at node{%
-  \wg at dbg{2}{Options: `#1', body: `#2'}%
-  \wg@@@node{#1}{#2}%
-}
-%    \end{macrocode}
-% 
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \item Prefix
-% \item Coordinates
-% \item Fixed options
-% \end{enumerate}
-% 
-%    \begin{macrocode}
-\def\wg@@@node#1#2#3#4#5{%
-  \ifx|#2|\wg at dbg{3}{No body given}%
-  \else%
-    \wg at dbg{3}{^^JWG Pic:
-      ^^J  User options:  #1
-      ^^J  Body:          #2
-      ^^J  Prefix:        #3
-      ^^J  Coordinates:   #4
-      ^^J  Fixed options: #5}%
-    % \wg at dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
-    \node[#5,#1] at (#4) {#3#2};%
-  \fi%
-  \wg at dbg{3}{End of WG Node}
-}
-%    \end{macrocode}
-% \end{Macro}
-% 
-%
-% \begin{Macro}{\wg at node@all}
-%
-%   This macro sets all pictures in a list.
-%   
-%   \begin{enumerate}
-%   \item List
-%   \item Prefix
-%   \item Position
-%   \item Styles
-%   \end{enumerate}
-%
-%
-%    \begin{macrocode}
-\def\wg at node@all#1#2#3#4{%
-  \wg at dbg{2}{WG picture loop
-    ^^J  List:    \meaning#1
-    ^^J  Prefix:  `#2'
-    ^^J  Position: `#3'
-    ^^J  Styles:   `#4'}
-  \foreach \p in #1{%
-    \wg at dbg{2}{WG picture element: \meaning\p}%
-    \expandafter\wg at node\p\@endwg at node {#2}{#3}{#4}%
-  }%
-}
-%    \end{macrocode}
-% \end{Macro}
-% 
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% 
-% \subsubsection{Bounding boxes}
-% 
-%  Bounding box dimensions
-%
-%
-%    \begin{macrocode}
-\newdimen\wg at bb@minx
-\newdimen\wg at bb@miny
-\newdimen\wg at bb@maxx
-\newdimen\wg at bb@maxy
-%    \end{macrocode}
-%
-%
-%
-% Enable or disable bounding box tracking 
-%
-%    \begin{macrocode}
-\newif\ifwg at notrelevantforpathsize\wg at notrelevantforpathsizefalse
-%    \end{macrocode}
-% 
-%
-% \begin{Macro}{wg at resetbb}
-% Reset the bounding box tracking dimensions
-%
-% \begin{macrocode}
-\def\wg at resetbb{%
-  \global\wg at bb@minx=16000pt\relax%
-  \global\wg at bb@miny=16000pt\relax%
-  \global\wg at bb@maxx=-16000pt\relax%
-  \global\wg at bb@maxy=-16000pt\relax%
-}
-%    \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\old at pgf@protocolsize}
-%   Save PGF's bounding box algorithm
-%   
-%    \begin{macrocode}
-\let\old at pgf@protocolsize\pgf at protocolsizes
-%    \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\wg at protocolsizes}
-%   Our bounding box algorithm
-%
-%    \begin{macrocode}
-\def\wg at protocolsizes#1#2{%
-  \old at pgf@protocolsize{#1}{#2}
-  \ifwg at notrelevantforpathsize\else%
-  \ifdim#1<\wg at bb@minx\global\wg at bb@minx#1\fi%
-  \ifdim#1>\wg at bb@maxx\global\wg at bb@maxx#1\fi%
-  \ifdim#2<\wg at bb@miny\global\wg at bb@miny#2\fi%
-  \ifdim#2>\wg at bb@maxy\global\wg at bb@maxy#2\fi%
-  \fi
-}
-%    \end{macrocode}
-% \end{Macro}
-%
-% % \begin{environment}{getbbl}
-%   Environment that tracks the local bounding box
-%
-%    \begin{macrocode}
-\newenvironment{getbbl}{%
-  \wg at resetbb%
-  \wg at notrelevantforpathsizefalse%
-  \global\let\pgf at protocolsizes\wg at protocolsizes}{%
-  \gdef\pgf at sh@ns at L{rectangle}
-  \gdef\pgf at sh@np at L{%
-    \def\southwest{\pgfqpoint{\the\wg at bb@minx}{\the\wg at bb@miny}}%
-    \def\northeast{\pgfqpoint{\the\wg at bb@maxx}{\the\wg at bb@maxy}}%
-  }
-  \gdef\pgf at sh@nt at L{{1}{0}{0}{1}{0pt}{0pt}}
-  \gdef\pgf at sh@pi at L{\pgfpictureid}
-  \global\let\pgf at protocolsizes\old at pgf@protocolsize
-}
-%    \end{macrocode}
-% \end{environment}
-% 
-% \begin{environment}{getbb}
-%   Environment to track global bounding box
-%   
-%    \begin{macrocode}
-\newenvironment{getbb}{%
-  \wg at resetbb%
-  \wg at notrelevantforpathsizefalse%
-  \global\let\pgf at protocolsizes\wg at protocolsizes}{%
-  \gdef\pgf at sh@ns at M{rectangle}
-  \gdef\pgf at sh@np at M{%
-    \def\southwest{\pgfqpoint{\the\wg at bb@minx}{\the\wg at bb@miny}}%
-    \def\northeast{\pgfqpoint{\the\wg at bb@maxx}{\the\wg at bb@maxy}}%
-  }
-  \gdef\pgf at sh@nt at M{{1}{0}{0}{1}{0pt}{0pt}}
-  % \pgfgettransform\pgf at temp%
-  % \xdef\pgf at sh@nt at M{\pgf at temp}
-  % \pgfgettransformentries{\wg at tmp@a}{\wg at tmp@b}{\wg at tmp@c}{\wg at tmp@d}{\pgf at temp}{\pgf at temp}
-  % \message{^^JTransform of M: \meaning\pgf at temp}
-  % \xdef\pgf at sh@nt at M{{\wg at tmp@a}{\wg at tmp@b}{\wg at tmp@c}{\wg at tmp@d}{0pt}{0pt}}%
-  % \message{^^JTransform of M: \meaning\pgf at sh@nt at M}
-  \gdef\pgf at sh@pi at M{\pgfpictureid}
-  \global\let\pgf at protocolsizes\old at pgf@protocolsize
-}
-%    \end{macrocode}
-% \end{environment}
-% 
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% 
-% \subsubsection{Other Tikz utilities}
-% 
-% \begin{TikzKey}{tikz/reverseclip}
-%   
-%   A reverse clipping path.  This is used to cut out stuff outside of
-%   path defined.
-%   
-%    \begin{macrocode}
-\tikzstyle{reverseclip}=[insert path={(current bounding box.north east) --
-  (current bounding box.south east) --
-  (current bounding box.south west) --
-  (current bounding box.north west) --
-  (current bounding box.north east)}]
-%    \end{macrocode}
-% \end{TikzKey}
-% 
-% \begin{TikzKey}{tikz/clip even odd rule}
-%   A reverse clipping path
-%
-%    \begin{macrocode}
-\tikzset{
-  clip even odd rule/.code={\pgfseteorule}, % Credit to Andrew Stacey
-}
-%    \end{macrocode}
-% \end{TikzKey}
-% 
-%
-% \begin{TikzKey}{tikz/invclip}
-%
-% Inverse clipping.  This should be an option \emph{after} the path to
-% do the inverse clipping by.  This works by adding a \emph{large}
-% (page) path to the current path, and then use that as clipping.
-% 
-%    \begin{macrocode}
-\tikzset{
-  invclip/.style={
-    clip,insert path=
-    [clip even odd rule]{
-      [reset cm](-\maxdimen,-\maxdimen)rectangle(\maxdimen,\maxdimen)
-    }
-  },
-}
-%    \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{save clip}
-%
-%   An option for use with sub-elements of NATO App 6(c) or chit
-%   nodes.  This will save the current path as a clipping path for the
-%   next paths to be drawn in the sub-element
-%
-%    \begin{macrocode}
-\newif\ifwg at s@ve\wg at s@vefalse
-\tikzset{
-  save clip/.is choice,
-  save clip/true/.code={\global\wg at s@vetrue},
-  save clip/false/.code={\global\wg at s@vefalse},
-  save clip/.default={true},
-  save clip/.initial={false},
-}
-%    \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{scale line widths}
-%
-%   Scales any line width specified in the node options.
-%
-%   Use like
-%
-% \begin{verbatim}
-%    \tikzset{
-%      some/.style={
-%        scale line widths,
-%        line width=1pt}
-%    }
-% \end{verbatim}
-%
-% Note that the order is important.
-%   
-%    \begin{macrocode}
-\tikzset{
-  scale line widths/.style={%
-    /utils/exec=\def\tikz at semiaddlinewidth##1{%
-      \pgfgettransformentries{%
-        \wg at jaca}{%
-        \wg at jacb}{%
-        \wg at jacc}{%
-        \wg at jacd}{%
-        \wg at tmp}{%
-        \wg at tmp}%
-      \pgfmathsetmacro{\wg at jac}{sqrt(abs(\wg at jaca*\wg at jacd-\wg at jacb*\wg at jacc))}%
-      \wg at dbg{4}{Scaling line width ##1 by \wg at jac}
-      \pgfmathsetmacro{\wg at lw}{\wg at jac*##1}%
-      \wg at dbg{4}{Scaled ##1 -> \wg at lw}
-      \tikz at addoption{\pgfsetlinewidth{\wg at lw pt}}%
-      \wg at dbg{4}{Added scaled option \wg at lw}
-      \pgfmathsetlength\pgflinewidth{\wg at lw pt}
-      \wg at dbg{4}{Did set line width \wg at lw pt}
-    }},
-  relative line width/.style={%
-    /utils/exec=\def\tikz at semiaddlinewidth##1{%
-      \wg at dbg{4}{Relative line width #1 times ##1}%
-      \pgfmathsetmacro{\wg at lv}{#1*##1}%
-      \tikz at addoption{\pgfsetlinewidth{\wg at lw pt}}%
-      \pgfmathsetlength\pgflinewidth{\wg at lw pt}}}
-}
-%    \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{sub pic actions}
-%
-%   This is key that propagates actions to sub pictures of pictures.
-%   The normal \texttt{pic actions} cannot be used as it causes an
-%   infinite loop.
-%
-%    \begin{macrocode}
-\tikzset{
-  sub pic actions/.code={%
-    \tikz at picmode%
-    \edef\opts{%
-      \iftikz at mode@draw draw,\else draw=none,\fi 
-      \iftikz at mode@fill fill\else fill=none\fi}
-    \wg at dbg{5}{^^JSub Mode: \meaning\tikz at picmode  \meaning\opts}
-    \pgfset{/tikz/.cd}
-    \pgfkeysalsofrom{\opts}
-  }}
-%    \end{macrocode}
-% \end{TikzKey}
-% 
-% \begin{TikzKey}{wg/debug show}
-%
-%   Show debugging information
-%   
-%    \begin{macrocode}
-\tikzset{
-  wg/debug show/.code={%
-    \extractcolorspec{pgfstrokecolor}{\wg at tmp@fg}
-    \def\wg at tmp@bg{none}
-    \@ifundefinedcolor{pgffillcolor}{}{
-      \extractcolorspec{pgffillcolor}{\wg at tmp@bg}}
-    \begingroup
-    \tikz at mode
-    \wargamedbglvl=#1
-    \wg at dbg{3}{Drawing with w/stroke `\wg at tmp@fg'
-      (\tikz at strokecolor,\iftikz at mode@draw\else not\space\fi drawing)
-      and fill `\wg at tmp@bg' (\tikz at fillcolor,\iftikz at mode@fill\else
-      not\space\fi  filling)}
-    \endgroup
-  }
-}
-%    \end{macrocode}
-% \end{TikzKey}
-%
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% 
-% \subsubsection{Random IDs}
-%
-%
-%    \begin{macrocode}
-\def\wg at r@ndom at id{%
-  \def\wg at uuid{}
-  \foreach \i in {1,...,8}{%
-    \pgfmathparse{Hex(random(0,15))}
-    \xdef\wg at uuid{\wg at uuid\pgfmathresult}}}
-%    \end{macrocode}
-%
-% \iffalse
 %</utils>
 %\fi
-% \iffalse
-% ====================================================================
-% \fi
-% 
-% \subsection{The \texttt{wgexport} class}
-% \label{sec:impl:util}
 %
-% This document class is used for exporting game component to be used
-% in a VASSAL module
-% libraries.
-% 
-% \iffalse
-%<*exportcls>
-%\fi
+% \input{util/export.dtx}
 %
-% Class identification and load \texttt{wargame} package
-% 
-%    \begin{macrocode}
-\ProvidesClass{wgexport}
-\PassOptionsToClass{multi=tikzpicture,varwidth=false}{standalone}
-\DeclareOption{noterrainpic}{%
-  \PassOptionsToPackage{\CurrentOption}{wargame}}
-\DeclareOption{terrainpic}{%
-  \PassOptionsToPackage{\CurrentOption}{wargame}}
-\DeclareOption*{%
-  \PassOptionsToClass{\CurrentOption}{standalone}}
-\ProcessOptions\relax
-\LoadClass{standalone}
-\RequirePackage{wargame}
-%    \end{macrocode}
-%
-% We need a few utilities before we get to the actual environment.
-% First, we need a tools to write out literal left and right curly
-% braces.  We do a bit of catcode hackery to accomplish that. 
-%
-%    \begin{macrocode}
-\begingroup
-\catcode`\^^I=12
-\def\@tabchar{^^I}
-\catcode`<=1 \catcode`>=2
-\catcode`{=12 \catcode`}=12
-\gdef\@lbchar<{>
-\gdef\@rbchar<}>
-\endgroup
-%    \end{macrocode}
-%
-% Above, we temporarily set the tab, and left and right curly brace
-% characters to be regular letters (12), and the catcodes of less than
-% and greater than to be those of left and right curly braces
-% respectively.  We then define the macros \cs{@tabchar},
-% \cs{@lbchar}, and \cs{@rbchar} to produce literal characters.
-% \LaTeX already has \cs{@percentchar}. 
-%
-% Everything we do should go inside this environment.  The single
-% optional argument is the file name stem of the output JSON file.
-% 
-%    \begin{macrocode}
-\newenvironment{imagelist}[1][\jobname]{%
-  \newwrite\mk at out%
-  \def\mk at i{}%
-  \def\mk at w{\immediate\write\mk at out}%
-  \immediate\openout\mk at out=#1.json
-  \mk at w{[}
-}{
-  \mk at w{\mk at i \@lbchar "name":"End of list", "category": "<<eol>>",
-      "subcategory": "" \@rbchar }
-  \mk at w{]}
-  \immediate\closeout\mk at out
-}
-%    \end{macrocode}
-% 
-%
-% Preceed all images (\textsf{tikzpicture}) with this command
-%
-% First argument is the name of the image.  This can be anything.
-% Note that for counters, if the name ends in \texttt{flipped} then it
-% is considered the backside of a counter.
-%
-% Second argument is the type of image. Recognised types are
-%
-% \begin{itemize}
-% \item \texttt{board}   for boards
-% \item \texttt{oob}     for OOBs
-% \item \texttt{chart}   for charts
-% \item \texttt{counter} for counters
-% \item \texttt{front}   for front page
-% \end{itemize}
-%
-% Other types can be used, and the images will be exported, but the
-% Python script pays no particular attention to those then.  Use for
-% example to prepare images for help or the like.
-%
-% The third argument is the sub type.  This is most relevant for the
-% counters.  Sub types can be anything, but since the counters will
-% receive different prototypes based on the sub type, it makes sense
-% to divide into sub types a la
-%
-% \begin{itemize}
-% \item factions
-% \item common markers
-% \end{itemize}
-% 
-% The faction sub types should just be the name of the faction.
-% E.g., Allies, Axis, Soviet, NATO, Warsaw Pact.  Spaces should not
-% matter.
-%
-% For common markers, there are a few names that are recognised
-% specifically by the Python script.  These are
-%
-% \begin{itemize}
-% \item \texttt{common}
-% \item \texttt{all}
-% \item \texttt{marker}
-% \item \texttt{markers}
-% \end{itemize}
-% 
-% Counters that has these sub-types will no be considered to belong
-% to any faction.
-%
-% Note that the Python script uses the faction names to guess the
-% players of the game, and uses them in several places.
-%
-% 
-%    \begin{macrocode}
-\def\info{%
-  \@ifstar{\@@info{,}}{\@@info{\@rbchar,}}}
-\def\@@info#1#2#3#4{%
-  \chit at dbg{2}{Making image `#2' of type `#3'/`#4' on page \thepage}%
-  \mk at w{ \@lbchar}%
-  \mk at w{ \space "name": "#2",}%
-  \mk at w{ \space "category": "#3",}%
-  \mk at w{ \space "subcategory": "#4", }%
-  \mk at w{ \space "number": \thepage #1}%
-  \let\oldmk at i\mk at i%
-  \ifx#1,\relax\edef\mk at i{\mk at i\space\space}\fi}
-\def\end at info{%
-  \let\mk at i\oldmk at i%
-  \mk at w{ \space \@rbchar,}}
-%    \end{macrocode}
-% 
-% Make separate images for each counter (single sided).
-% 
-%    \begin{macrocode}
-\newcommand\chitimages[2][]{%
-  \begingroup%
-  \let\chit at report\do at chit@report%
-  \let\natoapp at report\do at natoapp@report%
-  \chit at dbg{2}{chits to make images of `#2'}%
-  \foreach[count=\ti from 0] \t/\x in #2{%
-    \ifx\t\empty\else% Ignore empty rows
-      \chit at dbg{5}{^^JSubcategory: `\x' (default `#1')}
-      \ifx\t\x\def\x{#1}\fi% Take sub-category or default
-      \foreach \u/\m in \t{% 
-        \ifx\u\empty\else% Ignore empty cells 
-          \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
-          \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined 
-          \ifx\u\m\def\m{1}\fi% If the same as unit
-          \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
-          %% We only make one copy of the chit, since we can duplicate
-          %% it in VASSAL
-          \info*{\u}{counter}{\x}
-          \begin{tikzpicture}
-            \chit[\u=\ti]%
-          \end{tikzpicture}
-          \end at info%
-          %% \foreach \n in {1,...,\m}{% Make a number of copies 
-          %%   \ifx\u\chit at blank%
-          %%     \chit at dbg{3}{Ignoring blank chit:\u}%
-          %%   \else%
-          %%     \info{\u}{counter}{#2}
-          %%     \begin{tikzpicture}
-          %%       \chit[\u=\ti](\c,\r)%
-          %%     \end{tikzpicture}
-          %%   \fi%
-          %% }%
-        \fi%
-      }%
-      \chit at dbg{2}{End of inner loop}%  
-    \fi%
-  }%
-  \chit at dbg{2}{End of outer loop}%
-  \endgroup%
-}
-%    \end{macrocode}
-% 
-% Make separate images for each counter (double sided).  The back-side
-% counters must be defined by append `\texttt{ flipped}' the front
-% face name
-% 
-%    \begin{macrocode}
-\newcommand\doublechitimages[2][]{%
-  \begingroup%
-  \let\chit at report\do at chit@report%
-  \let\natoapp at report\do at natoapp@report%
-  \foreach[count=\ti from 0] \t/\x in #2{%
-    \ifx\t\empty\else% Ignore empty rows
-      \chit at dbg{5}{^^JSubcategory: `\x' (default `#1')}
-      \ifx\t\x\def\x{#1}\fi% Take sub-category or default
-      \foreach \u/\m in \t{% 
-        \ifx\u\empty\else% Ignore empty cells 
-          \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
-          \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined 
-          \ifx\u\m\def\m{1}\fi% If the same as unit
-          \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
-          %% Flipped chit
-          \edef\s{\u\space flipped}%
-          %% We only make one copy of the chit, since we can duplicate
-          %% it in VASSAL
-          \info*{\u}{counter}{\x}%
-          \begin{tikzpicture}%
-            \chit[\u=\ti]%
-          \end{tikzpicture}%
-          \end at info%
-          \info*{\s}{counter}{\x}%
-          \begin{tikzpicture}%
-            \chit[\s=\ti]%
-          \end{tikzpicture}%
-          \end at info%
-          %% \foreach \n in {1,...,\m}{% Make a number of copies 
-          %%   \ifx\u\chit at blank%
-          %%     \chit at dbg{3}{Ignoring blank chit:\u}%
-          %%   \else%
-          %%     \info{\u}{counter}{#2}
-          %%     \begin{tikzpicture}
-          %%       \chit[\u=\ti](\c,\r)%
-          %%     \end{tikzpicture}
-          %%   \fi%
-          %% }%
-        \fi%
-      }%
-    \fi%
-  }%
-  \endgroup%
-}
-%    \end{macrocode}
-%
-% Special for boards, we have the environment \textsf{boardimage}.
-% Like \cs{info} we must specify the name and sub-category of the
-% board, but the category is assumed to be \texttt{board} (though the
-% optional argument can specify a different category).
-%
-% Within this environment some specific styles are defined that allows
-% the user to specify VASSAL zones on the board.  For this to work
-% properly, the parent \textsf{tikzpicture} \emph{must} have the style
-% \texttt{zoned}.  This style will record the bounding box of the
-% picture which we will need to calculate VASSAL coordinates later
-% on.
-%
-% Other styles are \texttt{zone scope}, to be applied to
-% \texttt{scope}s in the picture, and \texttt{zone path} to be applied
-% to \texttt{path}s (or \cs{draw}, \cs{fill}, or the like) in the
-% picture.  These will record coordinates of these elements in side
-% the picture.  The Python script will then define VASSAL zones based
-% on these coordinates.
-%
-% For \texttt{zone scope} applied to a \texttt{scope}, what is
-% recorded are
-%
-% \begin{itemize}
-% \item The current coordinate transformation matrix
-% \item The current translation
-% \item The bounding box, within the current transformation and
-%   translation. 
-% \end{itemize}
-%
-% To define a zone in the board, simply enclose it in a
-%
-% \begin{verbatim}
-% \begin{scope}[zone scope=name]
-%   ...
-% \end{scope}
-% \end{verbatim}
-%
-% The \meta{name} will be the name of the scope.  If this contains the
-% sub-string \texttt{hex} (upper, lower, or mixed case), then the zone
-% will get a hex grid with numbering attached to it.
-%
-% If the \meta{name} contains the sub-string \texttt{turn} (any case),
-% then it is assumed to be a turn track and a rectangular grid will be
-% attached.  The column and row separator will be set to \texttt{T},
-% so that it won't collide with the main zone.  Similar if \meta{name}
-% contains \texttt{oob}, except the separator is set to \texttt{O}. 
-%
-% If \meta{name} contains the sub-string \texttt{pool}, then it is
-% assumed to be a pool of counters, and \emph{no} grid is attached. 
-%
-% For \texttt{zone path} applied to a \texttt{path}, what is recorded
-% is the path coordinates (as straight line segments) in the global
-% coordinate system.
-%
-% Both styles take one argument --- the name of the zone.  If that
-% name contains the sub-string \texttt{hex} anywhere in the name, then
-% the zone is assumed to contain a hex grid.  Otherwise, a rectangular
-% grid (of fixed size) will be applied to it.
-%
-% The environment \texttt{boardimage} also records the coordinate
-% options currently in use (keys \texttt{hex/first row is},
-% \texttt{hex/row direction is}, and so on), as well as the current
-% label option (as defined by \texttt{every hex} or \texttt{every hex
-% node}).
-%
-% All coordinates, and such are recorded in centimetres.  It is worth
-% remembering that the Tikz coordinate system has the $y$ axis point
-% upward, while typical image software has the $y$ axis point down.
-% \texttt{pdftocairo} typically assumes a 150 PPI (pixels-per-inch)
-% resolution.
-%
-% That means that scaling factor becomes
-%
-% $$\frac{150\mathrm{pixel}}{2.54\mathrm{cm}}=59.055\frac{\mathrm{pixel}}{\mathrm{cm}}$$
-% 
 % \iffalse
-% Using definition in terms of printers feet - the one to use!
-% 
-% PNG: 1674 x 1101
-% PDF:   "lower left": [-0.02107,-0.02107],
-%        "upper right": [28.31705,18.60843]
-% Width: 28.31705+0.02107 = 28.33812
-% Height: 18.60843+0.02107 = 18.62950
-% Pixel / cm: 1674 / 28.33812 = 59.07237318495369488166
-%             1101 / 18.62950 = 59.09981480984460130438
-%             Average         = 59.08609399739914809302
-%          
-% Calculated = 150 / 2.54 = 59.05511811023622047244
-%
-% Using 1/72.27
-% 
-% "lower left": [-0.02109,-0.02109],
-%  "upper right": [28.321,18.61102]
-%  Width: 28.321+0.02109=28.34209
-%  Height: 18.61102+0.02109=18.63211
-% Pixel / cm: 1674 / 28.34209 = 59.06409866033168337267
-%             1101 / 18.63211 = 59.09153606328000425072
-%             Average         = 59.07781736180584381169
+% EOF
 % \fi
-%
-% The information extracted is written to the
-% \cs{jobname}\texttt{.json} file as a sub-object (with name given by
-% the first optional argument) of the image object.  In that way, we
-% can later on easily get the information from our catalogue of
-% images. 
-%
-% Note, the styles \texttt{zoned}, \texttt{zone scope}, and
-% \texttt{zone path} are defined in \texttt{wargame} to be dummies so
-% that one can have them in the definition of the board without
-% impact. 
-%
-% Since we want to write all dimensions in centimetres, we need to be
-% able to convert \texttt{pt} dimensions to centimetres.  We make two
-% macros to do that for us.
-%
-% The exact definition of 1pt is
-%
-% $$1\,\mathrm{pt} = \frac{249}{250}12"\frac{1}{864}=\frac{83}{6000}1"
-% = 0.03513\overline{6}$$ 
-% 
-%    \begin{macrocode}
-% 2.54 / 72.27 = .03514598035145980351
-% \def\pt at to@cm#1{\pgfmathparse{#1 * 0.0351460}}
-\def\pt at to@cm#1{\pgfmathparse{#1 * 0.0351367}}
-\def\ptpoint at to@cm#1#2{%
-  \pt at to@cm{#1}\edef\x{\pgfmathresult}%
-  \pt at to@cm{#2}\edef\y{\pgfmathresult}}
-%    \end{macrocode}
-%    \begin{macrocode}
-\def\mk at get@anchor#1#2{%
-  \pgfpointanchor{#1}{#2}%
-  \pgfgetlastxy\tmp at x\tmp at y%
-  \pt at to@cm{\tmp at x}\edef\tmp at x{\pgfmathresult}
-  \pt at to@cm{\tmp at y}\edef\tmp at y{\pgfmathresult}
-}
-\def\mk at get@global at anchor#1#2{%
-  \pgfpointanchor{#1}{#2}%
-  \pgfgetlastxy\tmp at x\tmp at y%
-  \pgfpointtransformed{\pgfpoint{\tmp at x}{\tmp at y}}
-  \pgf at xa=\pgf at x
-  \pgf at ya=\pgf at y
-  \pt at to@cm{\the\pgf at xa}\edef\tmp at x{\pgfmathresult}
-  \pt at to@cm{\the\pgf at ya}\edef\tmp at y{\pgfmathresult}
-}
-\def\get at bb#1{%
-  % \pgfpointanchor{#1}{south west}%
-  % \pgfgetlastxy\tmp at llx\tmp at lly%
-  % \pgfpointanchor{#1}{north east}%
-  % \pgfgetlastxy\tmp at urx\tmp at ury%
-  % \pt at to@cm{\tmp at llx}\edef\llx{\pgfmathresult}
-  % \pt at to@cm{\tmp at lly}\edef\lly{\pgfmathresult}
-  % \pt at to@cm{\tmp at urx}\edef\urx{\pgfmathresult}
-  % \pt at to@cm{\tmp at ury}\edef\ury{\pgfmathresult}
-  \mk at get@anchor{#1}{south west}
-  \edef\llx{\tmp at x}
-  \edef\lly{\tmp at y}
-  \mk at get@anchor{#1}{north east}
-  \edef\urx{\tmp at x}
-  \edef\ury{\tmp at y}
-}
-%    \end{macrocode}
-%    \begin{macrocode}
-\def\mk at transform{%
-  \pgfgettransformentries{\mxx}{\mxy}{\myx}{\myy}{\ptdx}{\ptdy}
-  \pt at to@cm{\ptdx}\edef\dx{\pgfmathresult}
-  \pt at to@cm{\ptdy}\edef\dy{\pgfmathresult}
-  \mk at w{ \mk at i "xx": \mxx,}
-  \mk at w{ \mk at i "xy": \mxy,}
-  \mk at w{ \mk at i "yx": \myx,}
-  \mk at w{ \mk at i "yy": \myy,}
-  \mk at w{ \mk at i "dx": \dx,}
-  \mk at w{ \mk at i "dy": \dy,}
-}
-%    \end{macrocode}
-%    \begin{macrocode}
-\def\mk at bb#1{%
-  \get at bb{#1}
-  \mk at w{ \mk at i "lower left":  [\llx,\lly],}
-  \mk at w{ \mk at i "upper right": [\urx,\ury],}
-  \begingroup  
-   % \pgftransforminvert
-   % \pgfpointanchor{#1}{south west}%
-   % \pgfgetlastxy\tmp at llx\tmp at lly%
-   % \pgfpointtransformed{\pgfpoint{\tmp at llx}{\tmp at lly}}
-   % \pgf at xa=\pgf at x
-   % \pgf at ya=\pgf at y
-   % %
-   % \pgfpointanchor{#1}{north east}%
-   % \pgfgetlastxy\tmp at urx\tmp at ury%
-   % \pgfgetlastxy\tmp at llx\tmp at lly%
-   % \pgfpointtransformed{\pgfpoint{\tmp at urx}{\tmp at ury}}
-   % \pgf at xb=\pgf at x
-   % \pgf at yb=\pgf at y
-   % \pt at to@cm{\the\pgf at xa}\edef\llx{\pgfmathresult}
-   % \pt at to@cm{\the\pgf at ya}\edef\lly{\pgfmathresult}
-   % \pt at to@cm{\the\pgf at xb}\edef\urx{\pgfmathresult}
-   % \pt at to@cm{\the\pgf at yb}\edef\ury{\pgfmathresult}x
-   \mk at get@global at anchor{#1}{south west}
-   \mk at w{ \mk at i "global lower left":  [\tmp at x,\tmp at y],}
-   \mk at get@global at anchor{#1}{north east}
-   \mk at w{ \mk at i "global upper right": [\tmp at x,\tmp at y]}
- \endgroup
-}
-\def\mk at pos#1(#2){%
-  \hex at dbg{10}{^^JMarking `#2' with `#1' - start}
-  \coordinate[transform shape] (tmp) at (#2) {};
-  \mk at get@anchor{tmp}{center}
-  \hex at dbg{3}{^^JMarking `#2' with `#1' - `\tmp at x',\tmp at y'}
-  \tikzset{zone point={#1}{\tmp at x}{\tmp at y}}
-}
-%    \end{macrocode}
-%
-% For the key \texttt{zone path} to work, we need to be able to record
-% the path as it moves along.  To that end, we make a custom
-% decoration that will do that for us, and, once the path is finished,
-% write the path to our JSON file. 
-%
-%    \begin{macrocode}
-\pgfdeclaredecoration{record path construction}{initial}{%
-  \state{initial}[width=0pt,next state=more]{
-    \begingroup
-      \pgf at decorate@inputsegment at first
-      \ptpoint at to@cm{\the\pgf at x}{\the\pgf at y}
-      \xdef\wg at path{[\x,\y]}
-    \endgroup
-  }%
-  \state{more}[width=\pgfdecoratedinputsegmentremainingdistance]{%
-    \begingroup
-      \pgf at decorate@inputsegment at last
-      \ptpoint at to@cm{\the\pgf at x}{\the\pgf at y}
-      \xdef\wg at path{\wg at path,[\x,\y]}
-    \endgroup
-  }
-  \state{final}{%
-    \begingroup
-      \pgf at decorate@inputsegment at last
-      \ptpoint at to@cm{\the\pgf at x}{\the\pgf at y}
-      \xdef\wg at path{\wg at path,[\x,\y]}
-    \endgroup
-    \mk at w{ \mk at i "zone path \wg at record@path at name": \@lbchar}
-    \mk at w{ \mk at i\space "path": [\wg at path] \@rbchar,}
-  }
-}%
-%    \end{macrocode}
-%    
-% Now we can make our environment
-%
-% The first thing we do is to use the \cs{info} macro to mark the
-% image. Then we open our JSON file. We make a short-hand macro for
-% writing to that file.  The macro \cs{bd at i} records the current
-% indention (which is important in JSON)
-% 
-%    \begin{macrocode}
-\newenvironment{boardimage}[3][board]{%
-  \def\bd at n{#2}
-  \newcount\mk at point
-  \mk at point=0
-  \let\oomk at i\mk at i%
-  \let\markpos\mk at pos%
-%    \end{macrocode}
-%
-%
-% Then, to extract the label option, we make a dummy \texttt{node}
-% with the styles \texttt{every hex} and \texttt{every hex node}, so
-% we can extract that option. 
-%
-%    \begin{macrocode}
-  \info{dummy}{<<dummy>>}{}%
-  %\tikz{}%
-  \tikz{\scoped[every hex/.try,every hex node/.try]{%
-      \node[inner sep=0,outer sep=0]{%
-        \global\let\mk at label\hex at label}}}%
-%    \end{macrocode}
-%
-% The next thing we do is to make an object.  The first things we put
-% in are the units used (``cm''), and the grid options.
-%
-%    \begin{macrocode}
-  \info*{#2}{#1}{#3}%
-  \mk at w{ \mk at i "zones": \@lbchar}%
-  \edef\mk at i{\mk at i\space}
-  %% Everything is made into centimeters
-  \mk at w{ \mk at i "units": "cm",}
-  \@ifundefined{mk at label}{}{\mk at w{ \mk at i "labels": "\mk at label",}}
-  %% Write out coordinate options as "coords" object
-  \mk at w{ \mk at i"coords": \@lbchar}%
-  \mk at w{ \mk at i "row": \@lbchar}%
-  \mk at w{ \mk at i\space "offset": \hex at coords@row at off,}%
-  \mk at w{ \mk at i\space "factor": \hex at coords@row at fac \@rbchar,}%
-  \mk at w{ \mk at i "column": \@lbchar}%
-  \mk at w{ \mk at i\space "offset": \hex at coords@col at off,}%
-  \mk at w{ \mk at i\space "factor": \hex at coords@col at fac,}%
-  \mk at w{ \mk at i\space "top short": "\hex at top@short at col",}%
-  \mk at w{ \mk at i\space "bottom short": "\hex at bot@short at col" \@rbchar}%
-  \mk at w{ \mk at i\@rbchar,}%
-%    \end{macrocode}
-%
-% We then monkey-patch \cs{boardframe} to also output coordinates to
-% our JSON file.  Note that this will probably be embedded in a
-% different object. 
-%
-%    \begin{macrocode}
-  %%
-  \let\oldbo at rdframe\bo at rdframe%
-  \def\bo at rdframe[##1](##2)(##3){%
-    \oldbo at rdframe[##1](##2)(##3)%
-    \mk at w{ \mk at i"board frame": \@lbchar}
-    \mk at w{ \mk at i\space "lower left": [\llx,\lly],}
-    \mk at w{ \mk at i\space "upper right": [\urx,\ury],}
-    \mk at w{ \mk at i\space "margin": \margin,}
-    \mk at w{ \mk at i\space "width": \w,}
-    \mk at w{ \mk at i\space "height": \h \@rbchar,}}%
-%    \end{macrocode}
-%
-% Next, we make the style \texttt{zoned} to be applied to the
-% \texttt{tikzpicture} environment.  This records the bounding box of
-% the full picture. 
-%
-%    \begin{macrocode}
-  \tikzset{
-    zoned/.code={% Apply to whole picture
-      \pgfkeys{%
-        % This needs to be done in the picture!
-        /tikz/execute at end picture={%
-          \mk at w{ \mk at i "zoned": \@lbchar}
-          \mk at transform%
-          \mk at bb{current bounding box}
-          \mk at w{ \mk at i \@rbchar,}
-        }
-      }
-    },
-%    \end{macrocode}
-%
-% The next style is the \texttt{zone scope}.  At the start of the
-% scope we record the current transformation matrix.  Then we install
-% a handler to extract the bounding box at the end of the scope.  Note
-% that we increase indention here. 
-%
-%    \begin{macrocode}
-    zone scope/.code={%
-      \mk at w{ \mk at i"zone scope ##1": \@lbchar}
-      \let\omk at i\mk at i
-      \edef\mk at i{\mk at i\space}
-      \mk at transform%
-      %\bd at w{ \@rbchar,}
-      \gdef\wg at export@box{##1}%
-      \pgfkeys{%
-        /tikz/local bounding box=wg export box,
-        /tikz/execute at end scope={
-          \mk at bb{wg export box}
-          \let\mk at i\omk at i
-          \mk at w{ \mk at i\@rbchar,}},
-      } % pgfkeys
-    }, % zone scope
-%    \end{macrocode}
-% The next style gets the global coordinates of the current (0,0)
-% point - f.ex. in a node - and outputs that 
-%    \begin{macrocode}
-    zone point/.code n args={3}{
-      \pgf at xa=##2 cm
-      \pgf at ya=##3 cm
-      \pgfpointtransformed{\pgfpoint{\pgf at xa}{\pgf at ya}}
-      % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
-      \pgf at xa=\pgf at x
-      \pgf at ya=\pgf at y
-      \pt at to@cm{\the\pgf at xa}\edef\px{\pgfmathresult}
-      \pt at to@cm{\the\pgf at ya}\edef\py{\pgfmathresult}
-      \advance\mk at point1
-      \global\mk at point=\mk at point
-      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1", "type": "point", "coords":  [\px,\py]
-        \@rbchar, }
-      %\message{^^JZone point \the\mk at point\space ##1: ##2,##3 -> \px,\py}
-    },
-    zone oob point/.code n args={3}{
-      \pgf at xa=##2 cm
-      \pgf at ya=##3 cm
-      \advance\pgf at xa.1cm
-      \advance\pgf at ya.1cm
-      \pgfpointtransformed{\pgfpoint{\pgf at xa}{\pgf at ya}}
-      % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
-      \pgf at xa=\pgf at x
-      \pgf at ya=\pgf at y
-      \pt at to@cm{\the\pgf at xa}\edef\px{\pgfmathresult}
-      \pt at to@cm{\the\pgf at ya}\edef\py{\pgfmathresult}
-      \advance\mk at point1
-      \global\mk at point=\mk at point
-      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1", "type": "point", "coords":  [\px,\py]
-        \@rbchar, }
-      %\message{^^JZone point \the\mk at point\space ##1: ##2,##3 -> \px,\py}
-    },
-    zone global point/.code n args={3}{
-      \advance\mk at point1
-      \global\mk at point=\mk at point
-      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1", "type": "point", "coords":  [\px,\py]
-        \@rbchar, }
-    },      
-%    \end{macrocode}
-%
-% The \texttt{zone path} style is a bit more simple, but only because
-% the bulk of the work is done in a decoration.  We need to be able to
-% pass a name to that decoration, s we make a key for that.  The user
-% need not think about that though. 
-%
-%    \begin{macrocode}
-    /pgf/decoration/record path name/.store in=\wg at record@path at name,
-    zone path/.style={%
-      postaction={decorate,decoration={
-          record path construction,
-          record path name=##1}}
-    } % zone path
-  }% tikzset
-}
-%    \end{macrocode}
-%
-% That finishes the first part of the environment.  At the end of the
-% environment, we simple write the name of the picture, and close our
-% JSON output. 
-%
-%    \begin{macrocode}
-{%
-  \mk at w{ \mk at i "name": "\bd at n" }%
-  \let\mk at i\oomk at i%
-  \mk at w{ \mk at i\@rbchar}%
-  \end at info%
-}
-%    \end{macrocode}
-%
-%
-% TO BE DONE: We could add hooks to both the \texttt{hex} and
-% \texttt{chit} shapes that would allow us to write out the settings
-% for each of these.  This would allow us to make data files that
-% contain the information available in the \LaTeX{} code.  For
-% example, we could write a counters 
-%
-% \begin{itemize}
-% \item Left and right identifiers
-% \item Upper left, upper right, lower left, and lower right
-%   identifiers. (some care must be taken if these contains graphics
-%   and not just text.)
-% \item Factors
-% \item NATO symbol
-%   \begin{itemize}
-%   \item Faction, command, echelon
-%   \item Mains
-%   \item Left, right, top, and bottom attributes and modifiers
-%   \item Below attribute 
-%   \end{itemize}
-% \end{itemize}
-%
-% If one then assumed that for example the upper left corner holds the
-% start-up hex, then one could use that information.
-% 
-% The code below exports the chit information to the JSON file.  Not
-% sure how to use it though. 
-%
-%    \begin{macrocode}
-\tikzset{
-  zone turn/.store in=\zone at turn,
-  zone mult/.store in=\zone at mult
-}
-\def\do at chit@report{%
-  \mk at w{ \mk at i "chit": \@lbchar}
-  \@ifundefined{id}{}         {\mk at w{ \mk at i\space "id":      "\id", }}%
-  \@ifundefined{chit at symbol}{}     {\mk at w{ \mk at i\space "symbol":  "true", }}%
-  \@ifundefined{chit at full}{}       {\mk at w{ \mk at i\space "full":    "\chit at full", }}
-  \@ifundefined{chit at factors}{}    {\mk at w{ \mk at i\space "factors": "\chit at factors", }}%
-  \@ifundefined{chit at left}{}       {\mk at w{ \mk at i\space "left":    "\chit at left", }}%
-  \@ifundefined{chit at right}{}      {\mk at w{ \mk at i\space "right":   "\chit at right", }}%
-  \@ifundefined{chit at upper@left}{} {\mk at w{ \mk at i\space "upper left":  "\chit at upper@left", }}%
-  \@ifundefined{chit at lower@left}{} {\mk at w{ \mk at i\space "lower left":  "\chit at lower@left", }}%
-  \@ifundefined{chit at upper@right}{}{\mk at w{ \mk at i\space "upper right": "\chit at upper@right", }}%
-  \@ifundefined{chit at lower@right}{}{\mk at w{ \mk at i\space "lower right": "\chit at lower@right}", }%
-  \mk at w{ \mk at i\space "end": 0}
-  \@ifundefined{chit at symbol}{
-    \mk at w{ \mk at i \@rbchar }
-  }{
-    \mk at w{ \mk at i \@rbchar, }% NATOAPP6c will follow
-  }%
-}
-\def\do at natoapp@report{%
-  \mk at w{ \mk at i "natoapp6c": \@lbchar}
-  \@ifundefined{id}{}{\mk at w{ \mk at i\space "id": "\id", }}
-  \@ifundefined{natoapp at fac}{}{\mk at w{ \mk at i\space "faction": "\natoapp at fac", }}
-  \@ifundefined{natoapp at cmd}{}{\mk at w{ \mk at i\space "command": "\natoapp at cmd", }}
-  \@ifundefined{natoapp at ech}{}{\mk at w{ \mk at i\space "echelon": "\natoapp at ech", }}
-  \@ifundefined{natoapp at main}{}{\mk at w{ \mk at i\space "main": "\natoapp at main", }}
-  \@ifundefined{natoapp at left}{}{\mk at w{ \mk at i\space "left": "\natoapp at left", }}
-  \@ifundefined{natoapp at right}{}{\mk at w{ \mk at i\space "right": "\natoapp at right", }}
-  \@ifundefined{natoapp at upper}{}{\mk at w{ \mk at i\space "upper": "\natoapp at upper", }}
-  \@ifundefined{natoapp at lower}{}{\mk at w{ \mk at i\space "lower": "\natoapp at lower", }}
-  \@ifundefined{natoapp at below}{}{\mk at w{ \mk at i\space "below": "\natoapp at below", }}
-  \mk at w{ \mk at i\space "end": 0}
-  \mk at w{ \mk at i \@rbchar}
-}
-%    \end{macrocode}
-% \iffalse
-%</exportcls>
-%\fi
-%
+

Added: trunk/Master/texmf-dist/source/latex/wargame/util/export.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/export.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/export.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,817 @@
+% \iffalse
+% ====================================================================
+% \fi
+% 
+% \subsection{The \texttt{wgexport} class}
+% \label{sec:impl:util}
+%
+% This document class is used for exporting game component to be used
+% in a VASSAL module
+% libraries.
+% 
+% \iffalse
+%<*exportcls>
+%\fi
+%
+% Class identification and load \texttt{wargame} package
+% 
+%    \begin{macrocode}
+\ProvidesClass{wgexport}
+\PassOptionsToClass{multi=tikzpicture,varwidth=false}{standalone}
+\DeclareOption{noterrainpic}{%
+  \PassOptionsToPackage{\CurrentOption}{wargame}}
+\DeclareOption{terrainpic}{%
+  \PassOptionsToPackage{\CurrentOption}{wargame}}
+\DeclareOption*{%
+  \PassOptionsToClass{\CurrentOption}{standalone}}
+\ProcessOptions\relax
+\LoadClass{standalone}
+\RequirePackage{wargame}
+%    \end{macrocode}
+%
+% We need a few utilities before we get to the actual environment.
+% First, we need a tools to write out literal left and right curly
+% braces.  We do a bit of catcode hackery to accomplish that. 
+%
+%    \begin{macrocode}
+\begingroup
+\catcode`\^^I=12
+\def\@tabchar{^^I}
+\catcode`<=1 \catcode`>=2
+\catcode`{=12 \catcode`}=12
+\gdef\@lbchar<{>
+\gdef\@rbchar<}>
+\endgroup
+%    \end{macrocode}
+%
+% Above, we temporarily set the tab, and left and right curly brace
+% characters to be regular letters (12), and the catcodes of less than
+% and greater than to be those of left and right curly braces
+% respectively.  We then define the macros \cs{@tabchar},
+% \cs{@lbchar}, and \cs{@rbchar} to produce literal characters.
+% \LaTeX already has \cs{@percentchar}. 
+%
+% Everything we do should go inside this environment.  The single
+% optional argument is the file name stem of the output JSON file.
+% 
+%    \begin{macrocode}
+\newenvironment{imagelist}[1][\jobname]{%
+  \newwrite\mk at out%
+  \def\mk at i{}%
+  \def\mk at w{\immediate\write\mk at out}%
+  \immediate\openout\mk at out=#1.json
+  \mk at w{[}
+}{
+  \mk at w{\mk at i \@lbchar "name":"End of list", "category": "<<eol>>",
+      "subcategory": "" \@rbchar }
+  \mk at w{]}
+  \immediate\closeout\mk at out
+}
+%    \end{macrocode}
+% 
+%
+% Preceed all images (\textsf{tikzpicture}) with this command
+%
+% First argument is the name of the image.  This can be anything.
+% Note that for counters, if the name ends in \texttt{flipped} then it
+% is considered the backside of a counter.
+%
+% Second argument is the type of image. Recognised types are
+%
+% \begin{itemize}
+% \item \texttt{board}   for boards
+% \item \texttt{oob}     for OOBs
+% \item \texttt{chart}   for charts
+% \item \texttt{counter} for counters
+% \item \texttt{front}   for front page
+% \end{itemize}
+%
+% Other types can be used, and the images will be exported, but the
+% Python script pays no particular attention to those then.  Use for
+% example to prepare images for help or the like.
+%
+% The third argument is the sub type.  This is most relevant for the
+% counters.  Sub types can be anything, but since the counters will
+% receive different prototypes based on the sub type, it makes sense
+% to divide into sub types a la
+%
+% \begin{itemize}
+% \item factions
+% \item common markers
+% \end{itemize}
+% 
+% The faction sub types should just be the name of the faction.
+% E.g., Allies, Axis, Soviet, NATO, Warsaw Pact.  Spaces should not
+% matter.
+%
+% For common markers, there are a few names that are recognised
+% specifically by the Python script.  These are
+%
+% \begin{itemize}
+% \item \texttt{common}
+% \item \texttt{all}
+% \item \texttt{marker}
+% \item \texttt{markers}
+% \end{itemize}
+% 
+% Counters that has these sub-types will no be considered to belong
+% to any faction.
+%
+% Note that the Python script uses the faction names to guess the
+% players of the game, and uses them in several places.
+%
+% 
+%    \begin{macrocode}
+\def\info{%
+  \@ifstar{\@@info{,}}{\@@info{\@rbchar,}}}
+\def\@@info#1#2#3#4{%
+  \chit at dbg{2}{Making image `#2' of type `#3'/`#4' on page \thepage}%
+  \mk at w{ \@lbchar}%
+  \mk at w{ \space "name": "#2",}%
+  \mk at w{ \space "category": "#3",}%
+  \mk at w{ \space "subcategory": "#4", }%
+  \mk at w{ \space "number": \thepage #1}%
+  \let\oldmk at i\mk at i%
+  \ifx#1,\relax\edef\mk at i{\mk at i\space\space}\fi}
+\def\end at info{%
+  \let\mk at i\oldmk at i%
+  \mk at w{ \space \@rbchar,}}
+%    \end{macrocode}
+% 
+% Make separate images for each counter (single sided).
+% 
+%    \begin{macrocode}
+\newcommand\chitimages[2][]{%
+  \begingroup%
+  \let\chit at report\do at chit@report%
+  \let\natoapp at report\do at natoapp@report%
+  \chit at dbg{2}{chits to make images of `#2'}%
+  \foreach[count=\ti from 0] \t/\x in #2{%
+    \chit at dbg{2}{^^JRow: `\t' (`\x')}
+    \ifx\t\empty\else% Ignore empty rows
+      \chit at dbg{5}{^^JSubcategory: `\x' (default `#1')}
+      % Take sub-category or default
+      \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi
+      \foreach \u/\m in \t{% 
+        \ifx\u\empty\else% Ignore empty cells 
+          \ifx\u\chit at blank\else%
+            \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+            \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined 
+            \ifx\u\m\def\m{1}\fi% If the same as unit
+            \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
+            %% We only make one copy of the chit, since we can duplicate
+            %% it in VASSAL
+            \info*{\u}{counter}{\x}
+            \begin{tikzpicture}
+              \chit[\u=\ti]%
+            \end{tikzpicture}
+            \end at info%
+            %% \foreach \n in {1,...,\m}{% Make a number of copies 
+            %%   \ifx\u\chit at blank%
+            %%     \chit at dbg{3}{Ignoring blank chit:\u}%
+            %%   \else%
+            %%     \info{\u}{counter}{#2}
+            %%     \begin{tikzpicture}
+            %%       \chit[\u=\ti](\c,\r)%
+            %%     \end{tikzpicture}
+            %%   \fi%
+            %% }%
+          \fi%
+        \fi%
+      }%
+      \chit at dbg{2}{End of inner loop}%  
+    \fi%
+  }%
+  \chit at dbg{2}{End of outer loop}%
+  \endgroup%
+}
+%    \end{macrocode}
+% 
+% Make separate images for each counter (double sided).  The back-side
+% counters must be defined by append `\texttt{ flipped}' the front
+% face name
+% 
+%    \begin{macrocode}
+\newcommand\doublechitimages[2][]{%
+  \begingroup%
+  \let\chit at report\do at chit@report%
+  \let\natoapp at report\do at natoapp@report%
+  \foreach[count=\ti from 0] \t/\x in #2{%
+    \ifx\t\empty\else% Ignore empty rows
+      \chit at dbg{5}{^^JSubcategory: `\x' (default `#1')}
+      % Take sub-category or default
+      \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi
+      \foreach \u/\m in \t{% 
+        \ifx\u\empty\else% Ignore empty cells 
+          \ifx\u\chit at blank\else%
+            \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+            \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined 
+            \ifx\u\m\def\m{1}\fi% If the same as unit
+            \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
+            %% Flipped chit
+            \edef\s{\u\space flipped}%
+            %% We only make one copy of the chit, since we can duplicate
+            %% it in VASSAL
+            \info*{\u}{counter}{\x}%
+            \begin{tikzpicture}%
+              \chit[\u=\ti]%
+            \end{tikzpicture}%
+            \end at info%
+            \info*{\s}{counter}{\x}%
+            \begin{tikzpicture}%
+              \chit[\s=\ti]%
+            \end{tikzpicture}%
+            \end at info%
+            %% \foreach \n in {1,...,\m}{% Make a number of copies 
+            %%   \ifx\u\chit at blank%
+            %%     \chit at dbg{3}{Ignoring blank chit:\u}%
+            %%   \else%
+            %%     \info{\u}{counter}{#2}
+            %%     \begin{tikzpicture}
+            %%       \chit[\u=\ti](\c,\r)%
+            %%     \end{tikzpicture}
+            %%   \fi%
+            %% }%
+          \fi%
+        \fi%
+      }%
+    \fi%
+  }%
+  \endgroup%
+}
+%    \end{macrocode}
+%
+% Special for boards, we have the environment \textsf{boardimage}.
+% Like \cs{info} we must specify the name and sub-category of the
+% board, but the category is assumed to be \texttt{board} (though the
+% optional argument can specify a different category).
+%
+% Within this environment some specific styles are defined that allows
+% the user to specify VASSAL zones on the board.  For this to work
+% properly, the parent \textsf{tikzpicture} \emph{must} have the style
+% \texttt{zoned}.  This style will record the bounding box of the
+% picture which we will need to calculate VASSAL coordinates later
+% on.
+%
+% Other styles are \texttt{zone scope}, to be applied to
+% \texttt{scope}s in the picture, and \texttt{zone path} to be applied
+% to \texttt{path}s (or \cs{draw}, \cs{fill}, or the like) in the
+% picture.  These will record coordinates of these elements in side
+% the picture.  The Python script will then define VASSAL zones based
+% on these coordinates.
+%
+% For \texttt{zone scope} applied to a \texttt{scope}, what is
+% recorded are
+%
+% \begin{itemize}
+% \item The current coordinate transformation matrix
+% \item The current translation
+% \item The bounding box, within the current transformation and
+%   translation. 
+% \end{itemize}
+%
+% To define a zone in the board, simply enclose it in a
+%
+% \begin{verbatim}
+% \begin{scope}[zone scope=name]
+%   ...
+% \end{scope}
+% \end{verbatim}
+%
+% The \meta{name} will be the name of the scope.  If this contains the
+% sub-string \texttt{hex} (upper, lower, or mixed case), then the zone
+% will get a hex grid with numbering attached to it.
+%
+% If the \meta{name} contains the sub-string \texttt{turn} (any case),
+% then it is assumed to be a turn track and a rectangular grid will be
+% attached.  The column and row separator will be set to \texttt{T},
+% so that it won't collide with the main zone.  Similar if \meta{name}
+% contains \texttt{oob}, except the separator is set to \texttt{O}. 
+%
+% If \meta{name} contains the sub-string \texttt{pool}, then it is
+% assumed to be a pool of counters, and \emph{no} grid is attached. 
+%
+% For \texttt{zone path} applied to a \texttt{path}, what is recorded
+% is the path coordinates (as straight line segments) in the global
+% coordinate system.
+%
+% Both styles take one argument --- the name of the zone.  If that
+% name contains the sub-string \texttt{hex} anywhere in the name, then
+% the zone is assumed to contain a hex grid.  Otherwise, a rectangular
+% grid (of fixed size) will be applied to it.
+%
+% The environment \texttt{boardimage} also records the coordinate
+% options currently in use (keys \texttt{hex/first row is},
+% \texttt{hex/row direction is}, and so on), as well as the current
+% label option (as defined by \texttt{every hex} or \texttt{every hex
+% node}).
+%
+%
+% The information extracted is written to the
+% \cs{jobname}\texttt{.json} file as a sub-object (with name given by
+% the first optional argument) of the image object.  In that way, we
+% can later on easily get the information from our catalogue of
+% images. 
+%
+% Note, the styles \texttt{zoned}, \texttt{zone scope}, and
+% \texttt{zone path} are defined in \texttt{wargame} to be dummies so
+% that one can have them in the definition of the board without
+% impact. 
+%    \begin{macrocode}
+\def\mk at transform{%
+  \pgfgettransformentries{\mxx}{\mxy}{\myx}{\myy}{\ptdx}{\ptdy}
+  \wg at pt@to at cm{\ptdx}\edef\dx{\pgfmathresult}
+  \wg at pt@to at cm{\ptdy}\edef\dy{\pgfmathresult}
+  \mk at w{ \mk at i "xx": \mxx,}
+  \mk at w{ \mk at i "xy": \mxy,}
+  \mk at w{ \mk at i "yx": \myx,}
+  \mk at w{ \mk at i "yy": \myy,}
+  \mk at w{ \mk at i "dx": \dx,}
+  \mk at w{ \mk at i "dy": \dy,}
+}
+%    \end{macrocode}
+%    \begin{macrocode}
+\def\mk at bb#1{%
+  \wg at get@bb{#1}
+  \mk at w{ \mk at i "lower left":  [\llx,\lly],}
+  \mk at w{ \mk at i "upper right": [\urx,\ury],}
+  \begingroup  
+   \wg at get@global at nchor{#1}{south west}
+   \mk at w{ \mk at i "global lower left":  [\tmp at x,\tmp at y],}
+   \wg at get@global at nchor{#1}{north east}
+   \mk at w{ \mk at i "global upper right": [\tmp at x,\tmp at y]}
+ \endgroup
+}
+\def\mk at pos#1(#2){%
+  \wg at dbg{10}{^^JMarking `#2' with `#1' - start}
+  \coordinate[transform shape] (tmp) at (#2) {};
+  \wg at get@nchor{tmp}{center}
+  \wg at dbg{3}{^^JMarking `#2' with `#1' - `\tmp at x',\tmp at y'}
+  \tikzset{zone point={#1}{\tmp at x}{\tmp at y}}
+}
+%    \end{macrocode}
+%
+% For the key \texttt{zone path} to work, we need to be able to record
+% the path as it moves along.  To that end, we make a custom
+% decoration that will do that for us, and, once the path is finished,
+% write the path to our JSON file. 
+%
+%    \begin{macrocode}
+\pgfdeclaredecoration{record path construction}{initial}{%
+  \state{initial}[width=0pt,next state=more]{
+    \begingroup
+      \pgf at decorate@inputsegment at first
+      \ptpoint at to@cm{\the\pgf at x}{\the\pgf at y}
+      \xdef\wg at path{[\x,\y]}
+    \endgroup
+  }%
+  \state{more}[width=\pgfdecoratedinputsegmentremainingdistance]{%
+    \begingroup
+      \pgf at decorate@inputsegment at last
+      \ptpoint at to@cm{\the\pgf at x}{\the\pgf at y}
+      \xdef\wg at path{\wg at path,[\x,\y]}
+    \endgroup
+  }
+  \state{final}{%
+    \begingroup
+      \pgf at decorate@inputsegment at last
+      \ptpoint at to@cm{\the\pgf at x}{\the\pgf at y}
+      \xdef\wg at path{\wg at path,[\x,\y]}
+    \endgroup
+    \mk at w{ \mk at i "zone path \wg at record@path at name": \@lbchar}
+    \mk at w{ \mk at i\space "path": [\wg at path] \@rbchar,}
+  }
+}%
+%    \end{macrocode}
+%    
+% Now we can make our environment
+%
+% The first thing we do is to use the \cs{info} macro to mark the
+% image. Then we open our JSON file. We make a short-hand macro for
+% writing to that file.  The macro \cs{bd at i} records the current
+% indention (which is important in JSON)
+% 
+%    \begin{macrocode}
+\newenvironment{boardimage}[3][board]{%
+  \def\bd at n{#2}
+  \newcount\mk at point
+  \mk at point=0
+  \let\oomk at i\mk at i%
+  \let\markpos\mk at pos%
+%    \end{macrocode}
+%
+%
+% Then, to extract the label option, we make a dummy \texttt{node}
+% with the styles \texttt{every hex} and \texttt{every hex node}, so
+% we can extract that option. 
+%
+%    \begin{macrocode}
+  \info{dummy}{<<dummy>>}{}%
+  %\tikz{}%
+  \tikz{\scoped[%
+    every hex/.try,every hex node/.try,
+    ]{%
+      \def\hex at col{0}%
+      \def\hex at row{0}%
+      \node[hex,inner sep=0,outer sep=0]{%
+        \message{^^JHex label: `\meaning\hex at label'}%
+        \global\let\mk at label\hex at label}}}%
+%    \end{macrocode}
+%
+% The next thing we do is to make an object.  The first things we put
+% in are the units used (``cm''), and the grid options.
+%
+%    \begin{macrocode}
+  \info*{#2}{#1}{#3}%
+  \mk at w{ \mk at i "zones": \@lbchar}%
+  \edef\mk at i{\mk at i\space}
+  %% Everything is made into centimeters
+  \mk at w{ \mk at i "units": "cm",}
+  \hex at dbg{0}{Label: `\meaning\mk at label'}
+  \@ifundefined{mk at label}{}{\mk at w{ \mk at i "labels": "\mk at label",}}
+  %% Write out coordinate options as "coords" object
+  \mk at w{ \mk at i"coords": \@lbchar}%
+  \mk at w{ \mk at i "row": \@lbchar}%
+  \mk at w{ \mk at i\space "offset": \hex at coords@row at off,}%
+  \mk at w{ \mk at i\space "factor": \hex at coords@row at fac \@rbchar,}%
+  \mk at w{ \mk at i "column": \@lbchar}%
+  \mk at w{ \mk at i\space "offset": \hex at coords@col at off,}%
+  \mk at w{ \mk at i\space "factor": \hex at coords@col at fac,}%
+  \mk at w{ \mk at i\space "top short": "\hex at top@short at col",}%
+  \mk at w{ \mk at i\space "bottom short": "\hex at bot@short at col" \@rbchar}%
+  \mk at w{ \mk at i\@rbchar,}%
+%    \end{macrocode}
+%
+% We then monkey-patch \cs{boardframe} to also output coordinates to
+% our JSON file.  Note that this will probably be embedded in a
+% different object. 
+%
+%    \begin{macrocode}
+  %%
+  \let\oldbo at rdframe\bo at rdframe%
+  \def\bo at rdframe[##1](##2)(##3){%
+    \oldbo at rdframe[##1](##2)(##3)%
+    \mk at w{ \mk at i"board frame": \@lbchar}
+    \mk at w{ \mk at i\space "lower left": [\llx,\lly],}
+    \mk at w{ \mk at i\space "upper right": [\urx,\ury],}
+    \mk at w{ \mk at i\space "margin": \margin,}
+    \mk at w{ \mk at i\space "width": \w,}
+    \mk at w{ \mk at i\space "height": \h \@rbchar,}}%
+%    \end{macrocode}
+%
+% Next, we make the style \texttt{zoned} to be applied to the
+% \texttt{tikzpicture} environment.  This records the bounding box of
+% the full picture. 
+%
+%    \begin{macrocode}
+  \tikzset{
+    zoned/.code={% Apply to whole picture
+      \pgfkeys{%
+        % This needs to be done in the picture!
+        /tikz/execute at end picture={%
+          \mk at w{ \mk at i "zoned": \@lbchar}
+          \mk at transform%
+          \mk at bb{current bounding box}
+          \mk at w{ \mk at i \@rbchar,}
+        }
+      }
+    },
+%    \end{macrocode}
+%
+% The next style is the \texttt{zone scope}.  At the start of the
+% scope we record the current transformation matrix.  Then we install
+% a handler to extract the bounding box at the end of the scope.  Note
+% that we increase indention here. 
+%
+%    \begin{macrocode}
+    zone scope/.code={%
+      \mk at w{ \mk at i"zone scope ##1": \@lbchar}
+      \let\omk at i\mk at i
+      \edef\mk at i{\mk at i\space}
+      \mk at transform%
+      %\bd at w{ \@rbchar,}
+      \gdef\wg at export@box{##1}%
+      \pgfkeys{%
+        /tikz/local bounding box=wg export box,
+        /tikz/execute at end scope={
+          \mk at bb{wg export box}
+          \let\mk at i\omk at i
+          \mk at w{ \mk at i\@rbchar,}},
+      } % pgfkeys
+    }, % zone scope
+%    \end{macrocode}
+% The next style gets the global coordinates of the current (0,0)
+% point - f.ex. in a node - and outputs that 
+%    \begin{macrocode}
+    zone point/.code n args={3}{
+      \pgf at xa=##2 cm
+      \pgf at ya=##3 cm
+      \pgfpointtransformed{\pgfpoint{\pgf at xa}{\pgf at ya}}
+      % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
+      \pgf at xa=\pgf at x
+      \pgf at ya=\pgf at y
+      \wg at pt@to at cm{\the\pgf at xa}\edef\px{\pgfmathresult}
+      \wg at pt@to at cm{\the\pgf at ya}\edef\py{\pgfmathresult}
+      \advance\mk at point1
+      \global\mk at point=\mk at point
+      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1", "type": "point", "coords":  [\px,\py]
+        \@rbchar, }
+      %\message{^^JZone point \the\mk at point\space ##1: ##2,##3 -> \px,\py}
+    },
+    zone oob point/.code n args={3}{
+      \pgf at xa=##2 cm
+      \pgf at ya=##3 cm
+      \advance\pgf at xa.1cm
+      \advance\pgf at ya.1cm
+      \pgfpointtransformed{\pgfpoint{\pgf at xa}{\pgf at ya}}
+      % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
+      \pgf at xa=\pgf at x
+      \pgf at ya=\pgf at y
+      \wg at pt@to at cm{\the\pgf at xa}\edef\px{\pgfmathresult}
+      \wg at pt@to at cm{\the\pgf at ya}\edef\py{\pgfmathresult}
+      \advance\mk at point1
+      \global\mk at point=\mk at point
+      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1",
+        "parent": "\wg at export@box", "type": "point", "coords":  [\px,\py]
+        \@rbchar, }
+      %\message{^^JZone point \the\mk at point\space ##1: ##2,##3 -> \px,\py}
+    },
+    zone global point/.code n args={3}{
+      \advance\mk at point1
+      \global\mk at point=\mk at point
+      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1", "type": "point", "coords":  [\px,\py]
+        \@rbchar, }
+    },      
+%    \end{macrocode}
+%
+% The \texttt{zone path} style is a bit more simple, but only because
+% the bulk of the work is done in a decoration.  We need to be able to
+% pass a name to that decoration, s we make a key for that.  The user
+% need not think about that though. 
+%
+%    \begin{macrocode}
+    /pgf/decoration/record path name/.store in=\wg at record@path at name,
+    zone path/.style={%
+      postaction={decorate,decoration={
+          record path construction,
+          record path name=##1}}
+    } % zone path
+  }% tikzset
+}
+%    \end{macrocode}
+%
+% That finishes the first part of the environment.  At the end of the
+% environment, we simple write the name of the picture, and close our
+% JSON output. 
+%
+%    \begin{macrocode}
+{%
+  \mk at w{ \mk at i "name": "\bd at n" }%
+  \let\mk at i\oomk at i%
+  \mk at w{ \mk at i\@rbchar}%
+  \end at info%
+}
+%    \end{macrocode}
+%
+% Make battle markers.  Mandatory argument is how many markers,
+% optional is the group to add the markers to.
+% 
+%    \begin{macrocode}
+\def\wg at gennumberm@rkers#1#2#3{
+  \message{^^JNumbered markers: Type=`#1' Max=`#2' Category=`#3'}
+  \def\markers{}
+  \def\keys{}
+  \foreach \i in {1,...,#2}{%
+    \xdef\keys{/tikz/#1 \i/.style={/tikz/#1=\i},\keys}
+    \xdef\markers{\markers,#1 \i}}
+  {%
+    \nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}%
+\tikzset{
+  wg hidden unit/.pic={},
+  wg hidden unit/.style={
+    chit={
+      frame={draw=none,fill=none},
+      full=wg hidden unit}}}
+\DeclareRobustCommand\battlemarkers[2][BattleMarkers]{%
+  \wg at gennumberm@rkers{battle marker}{#2}{#1}%
+  \message{^^JMake a hidden unit and add to Markers category}
+  {%
+    \nopagecolor%
+    \chitimages[Markers]{{wg hidden unit}}%
+    %
+    \info{battle-marker-icon}{icon}{}%
+    \tikz[scale=.7,transform shape]{\pic{battle marker=0};}%
+    \info{clear-battles-icon}{icon}{}
+    \tikz[scale=.4,transform shape]{%
+      \pic{eliminate icon};
+      \pic[scale=.7,transform shape] at (-.3,0) {battle marker=0};}%
+  }%
+}
+%    \end{macrocode}
+% 
+% Make odds markers.  Mandatory argument is a list of odds and fill colours.
+% Optional is the group to add the markers to.
+% 
+%    \begin{macrocode}
+\def\wg at gencolorm@rkers#1#2#3{%
+  \def\markers{}
+  \def\keys{}
+  \foreach \o/\f in {#2}{%
+    \ifx\o\f\def\f{white}\fi%
+    \message{^^JOdds marker `#1 \o' w/fill `\f'}%
+    \xdef\keys{/tikz/#1 \o/.style={/tikz/#1={\o,\f}},\keys}
+    \xdef\markers{\markers,#1 \o}}
+  {\nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}%
+\DeclareRobustCommand\oddsmarkers[2][OddsMarkers]{%
+  \wg at gencolorm@rkers{odds marker}{#2}{#1}%
+  \info{odds-battles-icon}{icon}{}
+  \tikz[scale=.5,transform shape]{\pic{odds marker={?:?,white}}}
+  \info{resolve-battles-icon}{icon}{}
+  \tikz[scale=.3,transform shape]{%
+    \pic{dice};
+    \pic[scale=1.2,transform shape] at (-.2,-.2) {battle marker=0};}%
+}
+%    \end{macrocode}
+% 
+% Make results markers.  Mandatory argument is a list of results and
+% fill colours.  Optional is the group to add the markers to.
+% 
+%    \begin{macrocode}
+\DeclareRobustCommand\resultmarkers[2][ResultMarkers]{%
+  \wg at gencolorm@rkers{result marker}{#2}{#1}}
+%    \end{macrocode}
+% 
+% Common icons used by many modules
+% 
+%    \begin{macrocode}
+\DeclareRobustCommand\commonicons[2]{%
+  \begingroup%
+  \nopagecolor%
+  \tikzset{icon/.style={scale=.4,transform shape}}%
+  %
+  \info{pool-icon}{icon}{}
+  \tikz[icon]{\pic{pool icon};}
+  % 
+  \info{oob-icon}{icon}{}%
+  \tikz[icon]{\pic{oob icon={#1}{#2}};}%
+  %
+  \info{flip-icon}{icon}{}%
+  \tikz[icon]{\pic{flip icon};}%
+  %
+  \info{eliminate-icon}{icon}{}%
+  \tikz[icon]{\pic{eliminate icon};}%
+  %
+  \info{restore-icon}{icon}{}%
+  \tikz[icon]{\pic{restore icon};}%
+  %
+  \info{dice-icon}{icon}{}%
+  \tikz[icon,scale=.9]{\pic{dice};}%  
+  %
+  \info{unit-icon}{icon}{}%
+  \tikz[icon,scale=.7]{%
+    \chit[fill=#1,
+      symbol={[
+        scale line widths,
+        line width=1pt,
+        faction=friend,
+        command=land,
+        main=infantry,
+        scale=1.3](0,-.15)}]}%
+    \endgroup%
+}
+%    \end{macrocode}
+%    
+% \subsubsection{Making dice}
+% \begin{Syntax}
+%   \cs{dice}\oarg{tikz-options}\oarg{node-options}\marg{name}\marg{name}\marg{list}
+% \end{Syntax}
+% \begin{enumerate}
+% \item \meta{tikz-options}
+% \item \meta{node-options}
+% \item \meta{name} - an identifier - e.g., the same as \meta{shape}.
+% \item \meta{shape} - one of \texttt{d4}, \texttt{d6}, \texttt{d8},
+%   \texttt{d10}, \texttt{d12}, or \texttt{d20}.
+% \item \meta{list} - list of pairs
+%   \meta{value}\texttt{/}\meta{printed}, where \meta{value} is the
+%   value, and \meta{printed} is the shown value. If \meta{printed} is
+%   left out, then \meta{value} is used.
+% \end{enumerate}
+% 
+%    \begin{macrocode}
+\def\dice{%
+  \@ifnextchar[{\wg at dice}{\wg at dice[]}%]
+}
+\def\wg at dice[#1]{%
+  \@ifnextchar[{\wg@@dice{#1}}{\wg@@dice{#1}[]} %]
+}
+\def\wg@@dice#1[#2]#3#4#5{%
+  \foreach \v/\p in {#5}{%
+    \info{#3-\v}{die-roll}{#3}
+    \tikz[#1]{
+      \node[shape=#4,transform shape,draw=none,fill=black,opacity=.5]
+      at (.05,-.03){};
+      \node[shape=#4,#2,transform shape]{\p};}}}
+%    \end{macrocode}
+% 
+% \subsubsection{Hooks into chits, etc.}
+% 
+% TO BE DONE: We could add hook the \texttt{hex} shape that would
+% allow us to write out the settings for each of these.  This would
+% allow us to make data files that contain the information available
+% in the \LaTeX{} code.  
+%
+% If one then assumed that for example the upper left corner holds the
+% start-up hex, then one could use that information.
+% 
+% The code below exports the chit information to the JSON file.
+% Together with the battle, odds, and result markers stuff above, this
+% allows the exporter to almost automatically set up battle odds and
+% result calculations.   The fields exported are 
+%
+% \begin{itemize}
+% \item Left and right identifiers
+% \item Upper left, upper right, lower left, and lower right
+%   identifiers. (some care must be taken if these contains graphics
+%   and not just text.)
+% \item Factors
+% \item NATO symbol
+%   \begin{itemize}
+%   \item Faction, command, echelon
+%   \item Mains
+%   \item Left, right, top, and bottom attributes and modifiers
+%   \item Below attribute 
+%   \end{itemize}
+% \end{itemize}
+%
+% The exporter can set up prototypes for NATO types, echelons,
+% etc. The exporter can also set factors as marks on the units. 
+%
+%    \begin{macrocode}
+\tikzset{
+  zone turn/.store in=\zone at turn,
+  zone mult/.store in=\zone at mult
+}
+\def\@chit at rep@line#1#2{%
+  \@ifundefined{#2}{}{
+    \edef\wg at chit@tmp{\csname #2\endcsname}
+    {\escapechar=`/
+      \xdef\tmp{\detokenize\expandafter{\wg at chit@tmp} \@empty}}
+    % \message{^^J\meaning\@tmp -> \meaning\tmp}
+    \mk at w{ \mk at i\space "#1": "\tmp",}}}
+  
+\def\do at chit@report{%
+  \chit at dbg{3}{Start of Chit Report}
+  \mk at w{ \mk at i "chit": \@lbchar}
+  \chit at dbg{3}{Report - ID}
+  \@ifundefined{id}{}         {\mk at w{ \mk at i\space "id":      "\id", }}%
+  \chit at dbg{3}{Report - Symbol: `\meaning\chit at symbol'}
+  \@ifundefined{chit at symbol}{}{\mk at w{ \mk at i\space "symbol":  "true", }}%
+  \chit at dbg{3}{Report - Full: `\meaning\chit at full'}
+  \@chit at rep@line{full}{chit at full}
+  \chit at dbg{3}{Report - Factors: `\meaning\chit at factors'}
+  \@chit at rep@line{factors}{chit at factors}%
+  \chit at dbg{3}{Report - Left: `\meaning\chit at left'}
+  \@chit at rep@line{left}{chit at left}%
+  \chit at dbg{3}{Report - Right: : `\meaning\chit at right'}
+  \@chit at rep@line{right}{chit at right}%
+  \chit at dbg{3}{Report - Upper left: `\meaning\chit at upper@left'}
+  \@chit at rep@line{upper left}{chit at upper@left}%
+  \chit at dbg{3}{Report - Lower left: `\meaning\chit at lower@left'}
+  \@chit at rep@line{lower left}{chit at lower@left}%
+  \chit at dbg{3}{Report - Upper right: `\meaning\chit at upper@right}
+  \@chit at rep@line{upper right}{chit at upper@right}%
+  \chit at dbg{3}{Report - Lower right: `\meaning\chit at lower@right'}
+  \@chit at rep@line{lower right}{chit at lower@right}%
+  \chit at dbg{3}{Report - End comma}
+  \mk at w{ \mk at i\space "end": 0}
+  \@ifundefined{chit at symbol}{
+    \mk at w{ \mk at i \@rbchar }
+  }{
+    \mk at w{ \mk at i \@rbchar, }% NATOAPP6c will follow
+  }%
+  \chit at dbg{3}{End of Chit Report}
+}
+%    \end{macrocode}
+% Report out NATO App6 symbol settings
+%    \begin{macrocode}
+\def\do at natoapp@report{%
+  \mk at w{ \mk at i "natoapp6c": \@lbchar}
+  \@chit at rep@line{id}{\id}
+  \@chit at rep@line{faction}{natoapp at fac}
+  \@chit at rep@line{command}{natoapp at cmd}
+  \@chit at rep@line{echelon}{natoapp at ech}
+  \@chit at rep@line{main}{natoapp at main}
+  \@chit at rep@line{left}{natoapp at left}
+  \@chit at rep@line{right}{natoapp at right}
+  \@chit at rep@line{upper}{natoapp at upper}
+  \@chit at rep@line{lower}{natoapp at lower}
+  \@chit at rep@line{below}{natoapp at below}
+  \mk at w{ \mk at i\space "end": 0}
+  \mk at w{ \mk at i \@rbchar}
+}
+%    \end{macrocode}
+%
+%
+% \iffalse
+%</exportcls>
+%\fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/util/export.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/wargame/util/icons.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/icons.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/icons.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,118 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*utils>
+% \fi
+% 
+% \subsubsection{VASSAL icons}
+%
+%
+%  Some icons that may be useful in VASSAL.  We put them here so they
+%  may be used in manuals and the like too. 
+%
+%
+% First, the line style 
+%    \begin{macrocode}
+\tikzset{
+  trash can line/.style={scale line widths,scale rounded corners,
+    line width=.5mm,->},
+}
+%    \end{macrocode}
+%
+% Then, the body and lid of a trash can.
+% 
+%    \begin{macrocode}
+\tikzset{
+  trash can body/.pic={%
+    \path[fill=black,scale line widths,scale rounded corners,
+    rounded corners=.05cm]
+    (-.3,.2) --++(.6,0) --++(-.1,-.7) --++(-.4,0) --cycle;
+    \path[fill=white]
+    (-.025,-.4) arc(180:360:.025) --++(   0,.5) arc(0:180:.025)   --cycle;
+    \path[fill=white]
+    (-.125,-.4) arc(180:360:.025) --++(-.07,.5) arc(0:180:.025) --cycle;
+    \path[fill=white]
+    ( .075,-.4) arc(180:360:.025) --++( .07,.5) arc(0:180:.025) --cycle;
+  },
+  trash can lid/.pic={%
+    \path[fill=black,scale line widths,scale rounded corners,
+    rounded corners=.05cm]
+    (-.35,.23)--++(.7,0)--++(-.07,.07)--++(-.56,0)--cycle;
+    \path[fill=black]
+    (-.15,.3) --++(.05,0) --++(0,.05) --++(.2,0) --++(0,-.05)
+    --++(.05,0) --++(0,.05) arc(0:90:.05) --++(-.2,0) arc(90:180:.05)
+    --cycle;
+  },
+}
+%    \end{macrocode}
+%
+% Then, a closed and open trash can
+% 
+%    \begin{macrocode}
+\tikzset{
+  trash can/.pic={
+    \pic{trash can body};
+    \pic{trash can lid};
+  },
+  trash can open/.pic={
+    \pic{trash can body};
+    \pic[rotate=-30] at (0,.1) {trash can lid};
+  },
+}
+%    \end{macrocode}
+%
+% Now we can use that to generate some useful icons.
+% 
+%    \begin{macrocode}
+\tikzset{
+  eliminate icon/.pic={
+    \pic{trash can open};
+    \draw[trash can line,color=red!50!black]
+    (-.5,.2) to[looseness=1.5] (-.1,.23);
+  },
+  restore icon/.pic={
+    \pic{trash can open};
+    \draw[trash can line,<-,color=green!50!black]
+    (-.5,.2) to[looseness=1.5] (-.1,.23);
+  },
+  pool icon/.pic={
+    \pic{trash can};
+  },
+}
+%    \end{macrocode}
+%
+% These icons does not use the trash can picture. 
+% 
+%    \begin{macrocode}
+\tikzset{
+  flip icon/.pic={
+    \draw[scale line widths,scale rounded corners,
+    line width=1mm,->,color=blue!50!black]
+    (-.5,-.5) arc(180:0:.5);% (.5,-.5);
+  },
+  pics/oob icon/.style n args={2}{code={%
+      \begin{scope}[box/.style args={##1,##2,##3,##4}{
+          minimum width=##1cm,
+          minimum height=##2cm,
+          fill=##3,
+          anchor=##4,
+          draw=gray!50!black,
+          scale line widths,
+          line width=.5pt,
+          transform shape},
+        under/.style={
+          below=.05cm of ##1}
+        ]
+        \node[box={.5,.2,#1,north west,fill=#1}] (r1) at (.05,.45) {};
+        \node[under=r1.south west,box={.3,.25,#1,north west}] (r2) {};
+        \node[under=r2.south west,box={.2,.3, #1,north west}] (r3) {};
+        \node[box={.2,.4,#2,north east}] (l1) at (-.05,.45) {};
+      \end{scope}
+    }
+  }
+}      
+%    \end{macrocode}
+%
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/util/icons.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/wargame/util/misc.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/misc.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/misc.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,145 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*utils>
+% \fi
+% 
+% \subsubsection{Miscellaneous macros}
+% \begin{Macro}{\wargamelogo}
+%
+% This will produce the logo for this package.
+%
+%
+% \begin{center}
+%   \tikz{\wargamelogo}
+% \end{center}
+%
+%    \begin{macrocode}
+\tikzset{
+  wargame logo text/.style={
+    font=\sffamily\bfseries\fontsize{12}{14}\selectfont,
+    scale=2.8,
+    inner sep=0,
+    text width=1.8cm,
+    transform shape,
+    align=center},
+  wargame logo text content/.store in=\wg at logo@text at content,
+  wargame logo text content={{\huge\LaTeX} wargame},
+  wargame logo chit/.style={
+    chit={symbol={[
+        faction=friendly,
+        command=land,
+        echelon=division,
+        main=infantry]},
+      factors={chit/2 factors={4,3}},
+      left={chit/identifier=III},
+      right={chit/small identifier={10\textsuperscript{th}}},
+      color=white,
+      fill=red!50!black
+    }
+  },
+  wargame logo/.style={
+    transform shape,
+    every hex/.style={fill=gray!5!white,draw=gray!75!black},
+    hex/first row is=0,
+    hex/first column is=0,
+    hex/short top columns=none,
+    hex/short bottom columns=none,
+    hex/row direction is=normal,
+    hex/column direction is=normal
+  }
+}
+\newcommand\wargamelogo[1][]{% 
+  \begin{scope}[wargame logo,#1]
+    \node[hex={fill=gray!30!white}] (logo center)      at (hex cs:c=0,r=0) {}; 
+    \node[hex={terrain=light woods}](logo light woods) at (hex cs:c=0,r=1) {};
+    \node[hex={terrain=city}]       (logo city)        at (hex cs:c=0,r=-1){};
+    \node[hex={terrain=woods}]      (logo woods)       at (hex cs:c=-1,r=0){};
+    \node[hex={terrain=mountains}]  (logo mountains)   at (hex cs:c=-1,r=1){};
+    \node[hex={terrain=beach}]      (logo beach)       at (hex cs:c=1,r=1) {};
+    \node[hex={terrain=swamp}]      (logo swamp)       at (hex cs:c=1,r=0) {};
+    \node[wargame logo chit]        (logo chit)        at (hex cs:)        {};
+    \node[wargame logo text]        (logo text) {\wg at logo@text at content};
+  \end{scope}}
+%    \end{macrocode}
+%   
+% \end{Macro}
+% \begin{Macro}{\wg at dbg}
+% Debugging support.  The counter \cs{wargamedbglvl} sets the debug
+% level.  The package code then uses \cs{wg at dbg} to print out
+% debugging messages.   This macro takes two arguments --- the first
+% is the \emph{least} debug level at which the message is printed, and
+% the second is the message it self.  
+% 
+%    \begin{macrocode}
+\newcount\wargamedbglvl\wargamedbglvl=0
+\def\wg at dbg#1#2{%
+  \ifnum#1>\wargamedbglvl\relax\else\message{^^J#2}\fi}
+%    \end{macrocode}
+% \end{Macro}
+% 
+% \begin{Macro}{\wg at addto@macro}
+%
+%   The macro \cs{wg at addto@macro}\marg{macro}\marg{other} adds the
+%   definition of the macro \meta{other} to the macro \meta{macro}.
+%   This uses the \cs{toks} trick of storing the \emph{tokens} of the
+%   definition of a \meta{macro} and \meta{other} into \spec{@} and
+%   expanding that token into the definition of \meta{macro}.
+%   Effectively, this means that the top-level definition of
+%   \meta{macro} and \meta{other} are expanded (i.e., macros used in
+%   the definition of either macro is \emph{not} expanded) and then
+%   that becomes the new definition of \meta{macro}.
+%
+%   We will use this macro to do \emph{shallow} definitions of macros
+%   to contain keys and such.
+% 
+%    \begin{macrocode}
+\long\def\wg at addto@macro#1#2{%
+  \begingroup
+  \toks@\expandafter\expandafter\expandafter{\expandafter#1#2}%
+  \xdef#1{\the\toks@}%
+  \endgroup}
+%    \end{macrocode}
+% \end{Macro}
+%
+%
+% \begin{Macro}{\wg at sub@nchor}
+%   Get anchor from sub node.  We cannot use \cs{pgfpointanchor} since
+%   that returns the anchor coordinates in the global coordinate
+%   system.
+%
+%    \begin{macrocode}
+\def\wg at sub@nchor#1#2{%
+  \wg at dbg{3}{^^JGet `#2' in `#1'}%
+  \@ifundefined{pgf at sh@ns@#1}{%
+    \pgf at x=0cm\pgf at y=0cm}{%
+    \pgf at process{%
+      \csname pgf at sh@ma@#1\endcsname% MW
+      \csname pgf at sh@np@#1\endcsname%
+      \pgf at sh@reanchor{\csname pgf at sh@ns@#1\endcsname}{#2}}}%
+  \wg at dbg{10}{-> \the\pgf at x,\the\pgf at y}%
+}
+%    \end{macrocode}
+% \end{Macro}
+% 
+% 
+% Scratch dimensions
+%
+%    \begin{macrocode}
+\newdimen\wg at tmpa
+\newdimen\wg at tmpb
+\newdimen\wg at tmpc
+\newdimen\wg at tmpd
+%    \end{macrocode}
+%
+%
+% Macro to easy restore a saved path
+%
+%    \begin{macrocode}
+\def\settosave#1{
+  \pgfsyssoftpath at setcurrentpath{#1}}
+%    \end{macrocode}
+%    
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/util/misc.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/wargame/util/randomid.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/randomid.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/randomid.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,22 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*utils>
+% \fi
+% 
+% \subsubsection{Random IDs}
+%
+% This macro sets the macro \cs{wg at uuid} to some random hex number.
+% 
+%    \begin{macrocode}
+\def\wg at r@ndom at id{%
+  \def\wg at uuid{}
+  \foreach \i in {1,...,8}{%
+    \pgfmathparse{Hex(random(0,15))}
+    \xdef\wg at uuid{\wg at uuid\pgfmathresult}}}
+%    \end{macrocode}
+%
+% 
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/util/randomid.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/wargame/util/tikz.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/util/tikz.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/wargame/util/tikz.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -0,0 +1,196 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*utils>
+% \fi
+% 
+% \subsubsection{Other Tikz utilities}
+% 
+% \begin{TikzKey}{tikz/reverseclip}
+%   
+%   A reverse clipping path.  This is used to cut out stuff outside of
+%   path defined.
+%   
+%    \begin{macrocode}
+\tikzstyle{reverseclip}=[insert path={(current bounding box.north east) --
+  (current bounding box.south east) --
+  (current bounding box.south west) --
+  (current bounding box.north west) --
+  (current bounding box.north east)}]
+%    \end{macrocode}
+% \end{TikzKey}
+% 
+% \begin{TikzKey}{tikz/clip even odd rule}
+%   A reverse clipping path
+%
+%    \begin{macrocode}
+\tikzset{
+  clip even odd rule/.code={\pgfseteorule}, % Credit to Andrew Stacey
+}
+%    \end{macrocode}
+% \end{TikzKey}
+% 
+%
+% \begin{TikzKey}{tikz/invclip}
+%
+% Inverse clipping.  This should be an option \emph{after} the path to
+% do the inverse clipping by.  This works by adding a \emph{large}
+% (page) path to the current path, and then use that as clipping.
+% 
+%    \begin{macrocode}
+\tikzset{
+  invclip/.style={
+    clip,insert path=
+    [clip even odd rule]{
+      [reset cm](-\maxdimen,-\maxdimen)rectangle(\maxdimen,\maxdimen)
+    }
+  },
+}
+%    \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{save clip}
+%
+%   An option for use with sub-elements of NATO App 6(c) or chit
+%   nodes.  This will save the current path as a clipping path for the
+%   next paths to be drawn in the sub-element
+%
+%    \begin{macrocode}
+\newif\ifwg at s@ve\wg at s@vefalse
+\tikzset{
+  save clip/.is choice,
+  save clip/true/.code={\global\wg at s@vetrue},
+  save clip/false/.code={\global\wg at s@vefalse},
+  save clip/.default={true},
+  save clip/.initial={false},
+}
+%    \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{scale line widths}
+%
+%   Scales any line width specified in the node options.
+%
+%   Use like
+%
+% \begin{verbatim}
+%    \tikzset{
+%      some/.style={
+%        scale line widths,
+%        line width=1pt}
+%    }
+% \end{verbatim}
+%
+% Note that the order is important.
+%   
+%    \begin{macrocode}
+%      Save pgf rounded corners macro
+%      \let\wg at pgfsetcornersarced\pgfsetcornersarced
+\def\wg at setcornersarched#1{%
+  \ifx|#1|\else%
+  \edef\pgf at corner@arc{{#1}{#1}}%
+  \pgf at arccornerstrue%
+  \ifdim#1=0pt%
+    \pgf at arccornersfalse%
+    \fi\fi}
+\newdimen\wg at lw@scaled\wg at lw@scaled=1pt
+\def\wg at getscale{%
+  \pgfgettransformentries{%
+    \wg at jaca}{%
+    \wg at jacb}{%
+    \wg at jacc}{%
+    \wg at jacd}{%
+    \wg at tmp}{%
+    \wg at tmp}%
+  \pgfmathsetmacro{\wg at jac}{sqrt(abs(\wg at jaca*\wg at jacd-\wg at jacb*\wg at jacc))}%
+  \wg at dbg{4}{Scale is \wg at jac}
+  \xdef\wg at scale{\wg at jac}}
+\def\wg at scaled#1{%
+  \wg at getscale%
+  \wg at dbg{4}{Scaling #1 by \wg at scale}
+  \pgfmathsetmacro{\wg at tmp}{\wg at scale*#1}%
+  \xdef\wg at tmp{\wg at tmp}%
+  \xdef\wg at lw@scale{\wg at tmp}%
+  \wg at dbg{4}{Scaled #1 -> \wg at tmp}}
+%% \message{^^JRounded corners: \meaning\pgfsetcornersarced}
+\tikzset{
+  %% Get current scale and store in \wg at scale
+  get scale/.code={\wg at getscale},
+  scale line widths/.style={%
+    /utils/exec=\def\tikz at semiaddlinewidth##1{%
+      \wg at scaled{##1}
+      \wg at lw@scaled=\wg at tmp pt
+      \tikz at addoption{\pgfsetlinewidth{\wg at lw@scaled}}%
+      \wg at dbg{4}{Added scaled option \wg at tmp}
+      \pgfmathsetlength\pgflinewidth{\wg at tmp pt}
+      \wg at dbg{4}{Did set line width \wg at tmp pt}
+    }
+  },
+  scale rounded corners/.style={%
+    /utils/exec=\def\pgfsetcornersarced##1{%
+      \pgf at process{##1}%
+      \pgf at xa=\pgf at x%
+      \wg at scaled{\the\pgf at xa}%
+      % \tikz at addoption{\wg at setcornersarched{\wg at tmp pt}}%
+      \wg at dbg{4}{Scaled rounded corners: \the\pgf at xa -> \wg at tmp}%
+      \wg at setcornersarched{\wg at tmp pt}%
+    }
+  },
+  relative line width/.style={%
+    /utils/exec=\def\tikz at semiaddlinewidth##1{%
+      \wg at dbg{4}{Relative line width #1 times ##1}%
+      \pgfmathsetmacro{\wg at lv}{#1*##1}%
+      \tikz at addoption{\pgfsetlinewidth{\wg at lw pt}}%
+      \pgfmathsetlength\pgflinewidth{\wg at lw pt}}}
+}
+%    \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{sub pic actions}
+%
+%   This is key that propagates actions to sub pictures of pictures.
+%   The normal \texttt{pic actions} cannot be used as it causes an
+%   infinite loop.
+%
+%    \begin{macrocode}
+\tikzset{
+  sub pic actions/.code={%
+    \tikz at picmode%
+    \edef\opts{%
+      \iftikz at mode@draw draw,\else draw=none,\fi 
+      \iftikz at mode@fill fill\else fill=none\fi}
+    \wg at dbg{5}{^^JSub Mode: \meaning\tikz at picmode  \meaning\opts}
+    \pgfset{/tikz/.cd}
+    \pgfkeysalsofrom{\opts}
+  }}
+%    \end{macrocode}
+% \end{TikzKey}
+% 
+% \begin{TikzKey}{wg/debug show}
+%
+%   Show debugging information
+%   
+%    \begin{macrocode}
+\tikzset{
+  wg/debug show/.code={%
+    \extractcolorspec{pgfstrokecolor}{\wg at tmp@fg}
+    \def\wg at tmp@bg{none}
+    \@ifundefinedcolor{pgffillcolor}{}{
+      \extractcolorspec{pgffillcolor}{\wg at tmp@bg}}
+    \begingroup
+    \tikz at mode
+    \wargamedbglvl=#1
+    \wg at dbg{3}{Drawing with w/stroke `\wg at tmp@fg'
+      (\tikz at strokecolor,\iftikz at mode@draw\else not\space\fi drawing)
+      and fill `\wg at tmp@bg' (\tikz at fillcolor,\iftikz at mode@fill\else
+      not\space\fi  filling)}
+    \endgroup
+  }
+}
+%    \end{macrocode}
+% \end{TikzKey}
+%
+% 
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi


Property changes on: trunk/Master/texmf-dist/source/latex/wargame/util/tikz.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/latex/wargame/utils/wgexport.py
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/utils/wgexport.py	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/utils/wgexport.py	2023-03-30 20:06:53 UTC (rev 66713)
@@ -1,1885 +1,6192 @@
 #!/usr/bin/env python
+# Script collected from other scripts
 #
-from pprint import pprint
+#   ../vassal/vassal.py
+#   latexexporter.py
+#   main.py
+#
+# ====================================================================
+# From ../vassal/vassal.py
+# Script collected from other scripts
+#
+#   ../common/singleton.py
+#   ../common/verbose.py
+#   ../common/verboseguard.py
+#   base.py
+#   element.py
+#   globalkey.py
+#   gameelements.py
+#   mapelements.py
+#   globalproperty.py
+#   turn.py
+#   documentation.py
+#   player.py
+#   chessclock.py
+#   widget.py
+#   grid.py
+#   zone.py
+#   board.py
+#   map.py
+#   chart.py
+#   command.py
+#   trait.py
+#   withtraits.py
+#   traits/dynamicproperty.py
+#   traits/globalproperty.py
+#   traits/prototype.py
+#   traits/place.py
+#   traits/report.py
+#   traits/calculatedproperty.py
+#   traits/restrictcommand.py
+#   traits/label.py
+#   traits/layer.py
+#   traits/globalcommand.py
+#   traits/globalhotkey.py
+#   traits/nostack.py
+#   traits/deselect.py
+#   traits/restrictaccess.py
+#   traits/rotate.py
+#   traits/stack.py
+#   traits/mark.py
+#   traits/mask.py
+#   traits/trail.py
+#   traits/delete.py
+#   traits/sendto.py
+#   traits/moved.py
+#   traits/skel.py
+#   traits/submenu.py
+#   traits/basic.py
+#   traits/trigger.py
+#   traits/nonrect.py
+#   traits/click.py
+#   game.py
+#   buildfile.py
+#   moduledata.py
+#   save.py
+#   vsav.py
+#   vmod.py
+#   exporter.py
+#
+# ====================================================================
+# From ../common/singleton.py
+# ====================================================================
+class Singleton(type):
+    '''Meta base class for singletons'''
+    _instances = {}
+    def __call__(cls, *args, **kwargs):
+        '''Create the singleton object or returned existing
 
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''
+        if cls not in cls._instances:
+            cls._instances[cls] = \
+                super(Singleton, cls).__call__(*args, **kwargs)
+        return cls._instances[cls]
+
+#
+# EOF
+#
 # ====================================================================
-def add_pws(opw=None,upw=None):
-    '''Add password options to the poppler commands
+# From ../common/verbose.py
+# --------------------------------------------------------------------
 
-    Parameters
-    ----------
-    opw : str
-    	Owner password
-    upw : str
-    	User password
+class Verbose(metaclass=Singleton):
+    def __init__(self,verbose=False):
+        '''Singleton for writing message to screen, contigent on setting
 
-    Returns
-    -------
-    opts : list
-    	List of arguments 
-    '''
-    args = []
-    if  upw is not None:
-        args.extend(['-upw',upw])
+        Parameters
+        ----------
+        verbose : bool
+             Whether to show messages or not
+        '''
+        self._indent  = ''
+        self._verbose = verbose
 
-    if  opw is not None:
-        args.extend(['-opw',opw])
+    def setVerbose(self,verbose):
+        '''Set whether to print or not
 
-    return args
+        Parameters
+        ----------
+        verbose : bool
+             Whether to show messages or not
+        '''
+        self._verbose = verbose
 
-# --------------------------------------------------------------------
-def create_proc(args):
-    '''Create a process
+    @property
+    def verbose(self):
+        '''Test if this is verbose'''
+        return self._verbose
 
-    Parameters
-    ----------
-    args : list
-    	List of commmand line arguments
+    def message(self,*args,**kwargs):
+        '''Write messsage if verbose
 
-    Returns
-    -------
-    proc : subprocess.Process
-    	Process handle 
-    '''
-    from os import environ
-    from subprocess import Popen, PIPE, TimeoutExpired
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''        
+        if not self._verbose: return
+        if not kwargs.pop('noindent', False):
+            print(self._indent,end='')
+        print(*args,**kwargs)
 
-    return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
-    
+    def incr(self):
+        '''Increment indention'''
+        self._indent += ' '
+
+    def decr(self):
+        '''Decrement indention'''
+        if len(self._indent) > 0:
+            self._indent = self._indent[:-1]
+
+#
+# EOF
+#
+# ====================================================================
+# From ../common/verboseguard.py
 # --------------------------------------------------------------------
-def get_pdf_info(pdfname='export.pdf',upw=None,opw=None,timeout=None):
-    '''Get dictionary of PDF information
 
-    Parameters
-    ----------
-    pdfname : str
-    	File name
-    opw : str
-    	Owner password
-    upw : str
-    	User password
-    timeout : int
-    	Timeout in seconds or None
-    
-    Returns
-    -------
-    info : dict
-        PDF information
-    '''
-    from subprocess import TimeoutExpired
-   
-    args = ['pdfinfo', pdfname]
-    args.extend(add_pws(opw=opw,upw=upw))
+class VerboseGuard:
+    def __init__(self,*args,**kwargs):
+        '''A guard pattern that increases verbose indention
 
-    proc = create_proc(args)
+        This is a context manager.  The arguments passed are used for
+        an initial message, before increasinig indention.
 
-    try:
-        out, err = proc.communicate(timeout=timeout)
-    except Exception as e:
-        proc.kill()
-        proc.communicate()
-        raise RuntimeError(f'Failed to get PDF info: {e}')
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''                
+        Verbose().message(*args,**kwargs)
 
-    d = {}
-    for field in out.decode("utf8", "ignore").split("\n"):
-        sf         = field.split(":")
-        key, value = sf[0], ":".join(sf[1:])
-        if key != "":
-            d[key] = (int(value.strip()) if key == 'Pages' else value.strip())
+    def __bool_(self):
+        '''Test if verbose'''
+        return Verbose().verbose
+    
+    def __enter__(self):
+        '''Enter context'''
+        Verbose().incr()
+        return self
 
-    if "Pages" not in d:
-        raise ValueError
+    def __exit__(self,*args):
+        '''Exit context'''        
+        Verbose().decr()
 
-    return d
+    def __call__(self,*args,**kwargs):
+        '''Write a message at current indention level
         
-# --------------------------------------------------------------------
-def get_images_info(infofile):
-    '''Read in image information from file
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''                
+        Verbose().message(*args,**kwargs)
 
+#
+# EOF
+#
+# ====================================================================
+# From base.py
+# ====================================================================
+# Key encoding
+SHIFT = 65
+CTRL = 130
+ALT   = 520
+CTRL_SHIFT = CTRL+SHIFT
+ALT_SHIFT = ALT+SHIFT
+NONE = '\ue004'
+NONE_MOD = 0
+def key(let,mod=CTRL):
+
+    '''Encode a key sequence
+
     Parameters
     ----------
-    infofile : str
-         file name
+    let : str
+        Key code (Letter)
+    mod : int
+        Modifier mask
+    '''
+    return f'{ord(let)},{mod}'
+#
+def hexcolor(s):
+    if isinstance(s,str):
+        s = s.replace('0x','')
+        if len(s) == 3:
+            r, g, b = [int(si,16)/16 for si in s]
+        elif len(s) == 6:
+            r = int(s[0:2],16) / 256
+            g = int(s[2:4],16) / 256
+            b = int(s[4:6],16) / 256
+        else:
+            raise RuntimeError('3 or 6 hexadecimal digits for color string')
+    elif isinstance(s,int):
+        r = ((s >> 16) & 0xFF) / 256
+        g = ((s >>  8) & 0xFF) / 256
+        b = ((s >>  0) & 0xFF) / 256
+    else:
+        raise RuntimeError('Hex colour must be string or integer')
 
+    return rgb(int(r*256),int(g*256),int(b*256))
+    
+# Colour encoding 
+def rgb(r,g,b):
+    '''Encode RGB colour
+
+    Parameters
+    ----------
+    r : int
+        Red channel
+    g : int
+        Green channel
+    b : int
+        Blue channel
+
     Returns
     -------
-    images : list
-         List of image information 
+    colour : str
+        RGB colour as a string
     '''
-    #from csv import DictReader
-    from json import load 
+    return ','.join([str(r),str(g),str(b)])
 
-    with open(infofile,'r') as file:
-        info = load(file)
-        #reader = DictReader(file,fieldnames=['page','name','type','sub type'])
-        #return [l for l in reader]
+def rgba(r,g,b,a):
+    '''Encode RGBA colour
 
-    return info
-    
-
-# --------------------------------------------------------------------
-def convert_page(pdfname,page,opw=None,upw=None,timeout=None):
-    '''Convert a single PDF page to an image
-    
     Parameters
     ----------
-    pdfname : str
-    	File name
-    page : int
-        Page number 
-    opw : str
-    	Owner password
-    upw : str
-    	User password
-    timeout : int
-    	Timeout in seconds or None
+    r : int
+        Red channel
+    g : int
+        Green channel
+    b : int
+        Blue channel
+    a : int
+        Alpha channel
     
     Returns
     -------
-    img : bytes
-        The bytes of the image 
+    colour : str
+        RGBA colour as a string
     '''
-    args = ['pdftocairo',
-            '-singlefile',
-            '-f', str(page),
-            '-l', str(page),
-            '-png']
-    args.extend(add_pws(opw=opw,upw=upw))
-    args.append(pdfname)
-    args.append('-')
+    return ','.join([str(r),str(g),str(b),str(a)])
 
-    proc = create_proc(args)
+def dumpTree(node,ind=''):
+    '''Dump the tree of nodes
 
-    try:
-        out, err = proc.communicate(timeout=timeout)
-    except Exception as e:
-        proc.kill()
-        proc.communicate()
-        raise RuntimeError(f'Failed to convert page {page} of {pdfname}: {e}')
+    Parameters
+    ----------
+    node : xml.dom.Node
+        Node to dump
+    ind : str
+        Current indent 
+    '''
+    print(f'{ind}{node}')
+    for c in node.childNodes:
+        dumpTree(c,ind+' ')
 
-    from io import BytesIO
-    from PIL import Image
+#
+# EOF
+#
+# ====================================================================
+# From element.py
+
+# ====================================================================
+class Element:
+    BUILD  = 'VASSAL.build.'
+    MODULE = BUILD  + 'module.'
+    WIDGET = BUILD  + 'widget.'
+    MAP    = MODULE + 'map.'    
+    PICKER = MAP    + 'boardPicker.'
+    BOARD  = PICKER + 'board.'
+    def __init__(self,parent,tag,node=None,**kwargs):
+        '''Create a new element
+
+        Parameters
+        ----------
+        parent : Element
+            Parent element to add this element to
+        tag : str
+            Element tag
+        node : xml.dom.Node
+            If not None, then read attributes from that. Otherwise
+            set elements according to kwargs
+        kwargs : dict
+            Attribute keys and values.  Only used if node is None
+        '''
+        if parent is not None:
+            self._root = parent._root
+            self._node = (node if node is not None else
+                          parent.addNode(tag,**kwargs))
+        else:
+            self._root = None
+            self._node = None
+
+    # ----------------------------------------------------------------
+    # Attributes
+    def __contains__(self,key):
+        '''Check if element has attribute key'''        
+        return self.hasAttribute(key)
     
-    img = out #Image.open(BytesIO(out))
+    def __getitem__(self,key):
+        '''Get attribute key value'''
+        return self.getAttribute(key)
 
-    return img
+    def __setitem__(self,key,value):
+        '''Set attribute key value'''
+        self.setAttribute(key,value)
 
-# --------------------------------------------------------------------
-def ignore_entry(entry):
-    return entry['category'] in ['<<dummy>>','<<eol>>']
+    def hasAttribute(self,k):
+        '''Check if element has attribute '''
+        return self._node.hasAttribute(k)
 
-# --------------------------------------------------------------------
-def convert_pages(pdfname,infoname,
-                  opw=None,upw=None,timeout=None):
-    '''Convert pages in a PDF to images
+    def getAttribute(self,k):
+        '''Get attribute key value'''
+        return self._node.getAttribute(k)
+        
+    def setAttribute(self,k,v):
+        '''Set attribute key value'''
+        self._node.setAttribute(k,str(v).lower()
+                                if isinstance(v,bool) else str(v))
+        
+    def setAttributes(self,**kwargs):
+        '''Set attributes to dictionary key and value'''
+        for k,v in kwargs.items():
+            self.setAttribute(k,v)
 
-    This creates a list of dictionaries with the image name and data
-    (as bytes)
+    def getAttributes(self):
+        '''Get attributes as dict'''
+        return self._node.attributes
 
-    Parameters
-    ----------
-    pdfname : str
-    	File name of PDF to read images from 
-    infoname : str
-        File name of CSV to read image info from 
-    opw : str
-    	Owner password
-    upw : str
-    	User password
-    timeout : int
-    	Timeout in seconds or None
+    # ----------------------------------------------------------------
+    # Plain nodes
+    def getChildren(self):
+        '''Get child nodes (xml.dom.Node)'''
+        return self._node.childNodes
+
+    # ----------------------------------------------------------------
+    # Getters
+    #
+    # First generics 
+    def getAsDict(self,tag='',key=None,enable=True):
+        '''Get elements with a specific tag as a dictionary
+        where the key is given by attribute key'''
+        cont = self._node.getElementsByTagName(tag)
+        if not enable or key is None:
+            return cont
+
+        return {e.getAttribute(key): e for e in cont}
+
+    def getAsOne(self,tag='',single=True):
+        '''Get elements with a specific tag, as a list.
+        If single is true, then assume we only have one such
+        child element, or fail.'''
+        cont = self._node.getElementsByTagName(tag)
+        if single and len(cont) != 1:
+            return None
+        return cont
     
-    Returns
-    -------
-    imgs : list
-        List of images as dicts, info and the bytes of the images 
-    '''
+    def getElementsByKey(self,cls,key='',asdict=True):
+        '''Get elments of a specific class as a dictionary,
+        where the key is set by the key attribute.'''
+        cont = self.getAsDict(cls.TAG,key,asdict)
+        if cont is None: return None
+        
+        if not asdict: return [cls(self,node=n) for n in cont]
 
-    oargs   = {'opw':opw,'upw':upw,'timeout':timeout}
-    docinfo = get_pdf_info(pdfname,**oargs)
-    info    = get_images_info(infoname)
+        return {k : cls(self,node=n) for k, n in cont.items()}
 
-    if len(info)-1 != docinfo['Pages']:
-        raise RuntimeError(f'Number of pages in {pdfname} '
-                           f'({docinfo["Pages"]}) not {len(info)-1}')
+    def getAllElements(self,cls,single=True):
+        '''Get elements with a specific tag, as a list.  If single is
+        true, then assume we only have one such child element, or
+        fail.
 
-    for i in info:
-        if ignore_entry(i): continue
-        i['img'] = convert_page(pdfname,i['number'],**oargs)
+        '''
+        cont = self.getAsOne(cls.TAG,single=single)
+        if cont is None: return None
+        return [cls(self,node=n) for n in cont]
 
-    return info
+    def getSpecificElements(self,cls,key,*names,asdict=True):
+        '''Get all elements of specific class and that has the
+        attribute key, and the attribute value is in names
 
-# ====================================================================
-def remove_and_get(zipfname,*filenames):
-    '''Open zip file, and make a copy of it without the files listed
-    in entries.  Return the data of files listed in entry.'''
-    from tempfile import mkdtemp
-    from zipfile import ZipFile
-    from shutil import move, rmtree 
-    from os import path
+        '''
+        cont = self.getAsOne(cls.TAG,single=False)
+        cand = [cls(self,node=n) for n in cont
+                if n.getAttribute(key) in names]
+        if asdict:
+            return {c[key] : c for c in cand}
+        return cand
+    
+    def getParent(self,cls,checkTag=True):
+        if self._node.parentNode is None:
+            return None
+        if checkTag and self._node.parentNode.tagName != cls.TAG:
+            return None
+        return cls(self,node=self._node.parentNode)
 
-    tempdir = mkdtemp()
-    ret     = {}
-    try:
-        tempname = path.join(tempdir, 'new.zip')
-        with ZipFile(zipfname, 'r') as zipread:
+    def getParentOfClass(self,cls):
+        '''Searches back until we find the parent with the right
+        class, or none
+        '''
+        t = {c.TAG: c for c in cls}
+        p = self._node.parentNode
+        while p is not None:
+            c = t.get(p.tagName,None)
+            if c is not None: return c(self,node=p)
+            p = p.parentNode
+        return None
+        
+    # ----------------------------------------------------------------
+    # Adders
+    def addNode(self,tag,**attr):
+        '''Add a note to this element
 
-            with ZipFile(tempname, 'w') as zipwrite:
+        Parameters
+        ----------
+        tag : str
+            Node tag name
+        attr : dict
+            Attributes to set
+        '''
+        e = self._root.createElement(tag)
+        if self._node: self._node.appendChild(e)
 
-                for item in zipread.infolist():
-                    data = zipread.read(item.filename)
+        for k, v in attr.items():
+            e.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
 
-                    if item.filename not in filenames:
-                        zipwrite.writestr(item, data)
-                    else:
-                        ret[item.filename] = data
-                        
-        move(tempname, zipfname)
-    finally:
-        rmtree(tempdir)
+        return e
 
-    return ret
+    def addText(self,text):
+        '''Add a text child node to an element'''
+        t = self._root.createTextNode(text)
+        self._node.appendChild(t)
+        return t
 
+    def getText(self):
+        '''Get contained text node content'''
+        if self._node.firstChild is None or \
+           self._node.firstChild.nodeType != self._node.firstChild.TEXT_NODE:
+            return ''
+        return self._node.firstChild.nodeValue
+        
+
+    def add(self,cls,**kwargs):
+        '''Add an element and return wrapped in cls object'''
+        return cls(self,node=None,**kwargs)
+
+    def append(self,elem):
+        '''Append and element'''
+        if self._node.appendChild(elem._node):
+            return elem
+        return False
+
+    # ----------------------------------------------------------------
+    def remove(self,elem):
+        '''Remove an element'''
+        try:
+            self._node.removeChild(elem._node)
+        except:
+            return None
+        return elem
+    # ----------------------------------------------------------------
+    def insertBefore(self,toadd,ref):
+        '''Insert an element before another element'''
+        try:
+            self._node.insertBefore(toadd._node,ref._node)
+        except:
+            return None
+        return toadd
+
 # --------------------------------------------------------------------
-def add_back(zipfname,files):
-    '''Open a zip file for appending and add data in files
+class DummyElement(Element):
+    def __init__(self,parent,node=None,**kwargs):
+        '''A dummy element we can use to select elements of different
+        classes
 
-    Files is a dictionary of filenames mapping to file content,
-    as returned by remove_and_get
-    '''
-    from zipfile import ZipFile
-    
-    with ZipFile(zipfname, 'a') as zipadd:
-        for filename,data in files.items():
-            zipadd.writestr(filename,data)
+        '''  
+        super(DummyElement,self).__init__(parent,'Dummy',node=node)
 
+
+#
+# EOF
+#
 # ====================================================================
-# Contants used for tag names
-BUILD  = 'VASSAL.build.'
-MODULE = BUILD + 'module.'
-WIDGET = BUILD + 'widget.'
-MAP    = MODULE + 'map.'    
-PICKER = MAP + 'boardPicker.'
+# From globalkey.py
 
+# --------------------------------------------------------------------
+class GlobalKey(Element):
+    SELECTED = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS'
+    def __init__(self,
+                 parent,
+                 tag,
+                 node                 = None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = SELECTED,
+                 tooltip              = '',
+                 icon                 = ''):
+        '''
+        Parameters
+        ----------
+        - tag          The XML tag to use
+        - parent       Parent node
+        - node         Optionally existing node
+        - name         Name of key
+        - buttonHotkey Key in "global" scope
+        - hotkey       Key to send to targeted pieces
+        - buttonText   Text on button
+        - canDisable   If true, disabled when propertyGate is true
+        - deckCount    Number of decks (-1 is all)
+        - filter       Which units to target
+        - propertyGate When true, disable
+        - reportFormat Chat message
+        - reportSingle Also show single piece reports
+        - singleMap    Only originating map if True
+        - target       Preselection filter (default selected pieces)
+        - tooltip      Hover-over message
+        - icon         Image to use as icon
+
+        Default targets are selected units
+        '''
+        nme = (name    if len(name)    > 0 else
+               tooltip if len(tooltip) > 0 else '')
+        tip = (tooltip if len(tooltip) > 0 else
+               name    if len(name)    > 0 else '') 
+        super(GlobalKey,self).\
+            __init__(parent,
+                     tag,
+                     node                 = node,
+                     name                 = nme,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tip,
+                     icon                 = icon)
+#
+# EOF
+# 
 # ====================================================================
-def get_asdict(elem,tag,key,enable=True):
-    cont = elem.getElementsByTagName(tag)
-    if not enable:
-        return cont
+# From gameelements.py
 
-    return {e.getAttribute(key): e for e in cont}
+# --------------------------------------------------------------------
+class GameElementService:
+    def getGame(self):
+        return self.getParent(Game)
 
 # --------------------------------------------------------------------
-def get_asone(elem,tag,enable=True):
-    cont = elem.getElementsByTagName(tag)
-    if enable and len(cont) != 1:
-        return None
-    return cont
+class GameElement(Element,GameElementService):
+    def __init__(self,game,tag,node=None,**kwargs):
+        super(GameElement,self).__init__(game,tag,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class Notes(GameElement):
+    TAG = Element.MODULE+'NotesWindow'
+    def __init__(self,elem,node=None,
+                 name             = 'Notes',
+                 buttonText       = '',
+                 hotkey           = key('N',ALT),
+                 icon             = '/images/notes.gif',
+                 tooltip          = 'Show notes window'):
+        super(Notes,self).__init__(elem,self.TAG,node=node,
+                                   name       = name,
+                                   buttonText = buttonText,
+                                   hotkey     = hotkey,
+                                   icon       = icon,
+                                   tooltip    = tooltip)
+    def encode(self):
+        return ['NOTES\t\\','PNOTES']
+
+# --------------------------------------------------------------------
+class PredefinedSetup(GameElement):
+    TAG = Element.MODULE+'PredefinedSetup'
+    def __init__(self,elem,node=None,
+                 name             = '',
+                 file             = '',
+                 useFile          = False,
+                 isMenu           = False,
+                 description      = ''):
+        useFile = ((useFile or not isMenu) and
+                   (file is not None and len(file) > 0))
+        if file is None: file = ''
+        super(PredefinedSetup,self).__init__(elem,self.TAG,node=node,
+                                             name        = name,
+                                             file        = file,
+                                             useFile     = useFile,
+                                             isMenu      = isMenu,
+                                             description = description)
+    def addPredefinedSetup(self,**kwargs):
+        '''Add a `PredefinedSetup` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PredefinedSetup
+            The added element
+        '''
+        return self.add(PredefinedSetup,**kwargs)
+    def getPredefinedSetups(self,asdict=True):
+        '''Get all PredefinedSetup element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `PredefinedSetup` elements.  If `False`, return a list of all PredefinedSetup` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `PredefinedSetup` children
+        '''
+        return self.getElementsByKey(PredefinedSetup,'name',asdict)
+        
     
+        
+                   
+                  
 # --------------------------------------------------------------------
-def get_doc(string):
-    from xml.dom.minidom import parseString
+class GlobalTranslatableMessages(GameElement):
+    TAG=Element.MODULE+'properties.GlobalTranslatableMessages'
+    def __init__(self,elem,node=None):
+        '''Translations
 
-    return parseString(string)
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        '''
+        super(GlobalTranslatableMessages,self).\
+            __init__(elem,self.TAG,node=node)
 
 # --------------------------------------------------------------------
-def get_game(doc):
-    return doc.getElementsByTagName('VASSAL.build.GameModule')[0]
+class Language(GameElement):
+    TAG = 'VASSAL.i18n.Language'
+    def __init__(self,elem,node=None,**kwargs):
+        super(Languate,self).__init__(sele,self.TAG,node=none,**kwargs)
+        
+# --------------------------------------------------------------------
+class Chatter(GameElement):
+    TAG=Element.MODULE+'Chatter'
+    def __init__(self,elem,node=None,**kwargs):
+        '''Chat
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        kwargs : dict
+            Attributes
+        '''
+        super(Chatter,self).__init__(elem,self.TAG,node=node,**kwargs)
+        
 # --------------------------------------------------------------------
-def get_globalproperties(doc,one=True):
-    return get_asone(doc,MODULE+'properties.GlobalProperties',one)
+class KeyNamer(GameElement):
+    TAG=Element.MODULE+'KeyNamer'
+    def __init__(self,elem,node=None,**kwargs):
+        '''Key namer (or help menu)
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        kwargs : dict
+            Attributes
+        '''
+        super(KeyNamer,self).__init__(elem,self.TAG,node=node,**kwargs)
+        
+
 # --------------------------------------------------------------------
-def get_globalproperty(doc,asdict=True):
-    '''A little confusing - this returns a dictionary'''
-    return get_asdict(doc,MODULE+'properties.GlobalProperty','name',asdict)
+#    <VASSAL.build.module.GlobalOptions
+#      autoReport="Always"
+#      centerOnMove="Use Preferences Setting"
+#      chatterHTMLSupport="Always"
+#      hotKeysOnClosedWindows="Always"
+#      inventoryForAll="Never"
+#      nonOwnerUnmaskable="Always"
+#      playerIdFormat="$PlayerName$"
+#      promptString="Opponents can unmask pieces"
+#      sendToLocationMoveTrails="Always"
+#      storeLeadingZeroIntegersAsStrings="true">
+#        <option name="stepIcon">/images/StepForward16.gif</option>
+#        <option name="stepHotKey">39,130</option>
+#        <option name="undoIcon">/images/Undo16.gif</option>
+#        <option name="undoHotKey">90,130</option>
+#        <option name="serverControlsIcon">/images/connect.gif</option>
+#        <option name="serverControlsHotKey">65,195</option>
+#        <option name="debugControlsIcon"/>
+#        <option name="debugControlsHotKey">68,195</option>
+#    </VASSAL.build.module.GlobalOptions>
+class GlobalOptions(GameElement):
+    NEVER  = 'Never'
+    ALWAYS = 'Always'
+    PROMPT = 'Use Preferences Setting'
+    TAG    = Element.MODULE+'GlobalOptions'
+    def __init__(self,doc,node=None,
+                 autoReport               = PROMPT,
+                 centerOnMove             = PROMPT,
+                 chatterHTMLSupport       = ALWAYS,
+                 hotKeysOnClosedWindows   = NEVER,
+                 inventoryForAll          = ALWAYS,
+                 nonOwnerUnmaskable       = PROMPT,
+                 playerIdFormat           = "$playerName$",
+                 promptString             = "Opponents can unmask pieces",
+                 sendToLocationMoveTrails = NEVER,
+                 storeLeadingZeroIntegersAsStrings = False,
+                 description                       = 'Global options',
+                 dragThreshold                     = 10):
+        '''Set global options on the module
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        
+        autoReport                        : str='always'
+        centerOnMove                      : str Option
+        chatterHTMLSupport                : str='never'
+        hotKeysOnClosedWindows            : str='never'
+        inventoryForAll                   : str='always' 
+        nonOwnerUnmaskable                : str='never'
+        playerIdFormat                    : str='$PlayerName$'
+        promptString                      : str=?
+        sendToLocationMoveTrails          : bool=false
+        storeLeadingZeroIntegersAsStrings : bool=False
+        '''
+        super(GlobalOptions,self).\
+            __init__(doc,self.TAG,node=node,
+                     autoReport               = autoReport,
+                     centerOnMove             = centerOnMove,
+                     chatterHTMLSupport       = chatterHTMLSupport,
+                     hotKeysOnClosedWindows   = hotKeysOnClosedWindows,
+                     inventoryForAll          = inventoryForAll,
+                     nonOwnerUnmaskable       = nonOwnerUnmaskable,
+                     playerIdFormat           = playerIdFormat,
+                     promptString             = promptString,
+                     sendToLocationMoveTrails = sendToLocationMoveTrails,
+                     storeLeadingZeroIntegersAsStrings = storeLeadingZeroIntegersAsStrings,
+                     dragThreshold            = dragThreshold,
+                     description              = description)
+
+    def addOption(self,**kwargs):
+        '''Add a `Option` element to this
+
+        Options known
+        - stepIcon - image file name (/images/StepForward16.gif)
+        - stepHotKey - key 
+        - undoIcon - image file name (/images/Undo16.gif)
+        - undoHotKey - key
+        - serverControlsIcon - image file name (/images/connect.gif)
+        - serverControlsHotKey - key
+        - debugControlsIcon - image file name 
+        - debugControlsHotKey - key 
+        
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Option
+            The added element
+        '''
+        return self.add(Option,**kwargs)
+    def getOptions(self):
+        return self.getElementsByKey(Option,'name')
+
+    def addPreference(self,cls,**kwargs):
+        return self.add(cls,**kwargs)
+
+    def addIntPreference(self,**kwargs):
+        return self.add(IntPreference,**kwargs)
+
+    def addFloatPreference(self,**kwargs):
+        return self.add(FloatPreference,**kwargs)
+    
+    def addBoolPreference(self,**kwargs):
+        return self.add(BoolPreference,**kwargs)
+    
+    def addStrPreference(self,**kwargs):
+        return self.add(StrPreference,**kwargs)
+    
+    def addTextPreference(self,**kwargs):
+        return self.add(TextPreference,**kwargs)
+    
+    def addEnumPreference(self,**kwargs):
+        return self.add(EnumPreference,**kwargs)
+    
+    def getIntPreferences(self):
+        return self.getElementsByKey(IntPreference,'name')
+
+    def getFloatPreferences(self):
+        return self.getElementsByKey(FloatPreference,'name')
+
+    def getBoolPreferences(self):
+        return self.getElementsByKey(BoolPreference,'name')
+
+    def getStrPreferences(self):
+        return self.getElementsByKey(StrPreference,'name')
+
+    def getTextPreferences(self):
+        return self.getElementsByKey(TextPreference,'name')
+
+    def getEnumPreferences(self):
+        return self.getElementsByKey(EnumPreference,'name')
+
+    def getPreferences(self):
+        retd = {}
+        for cls in [IntPreference,
+                    FloatPreference,
+                    BoolPreference,
+                    StrPreference,
+                    TextPreference,
+                    EnumPreference]:
+            retd.update(self.getElementsByKey(cls,'name'))
+
+        return retd
+    
+
 # --------------------------------------------------------------------
-def get_turntrack(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.TurnTracker','name',asdict)
+class Option(Element):
+    TAG = 'option'
+    def __init__(self,doc,node=None,name='',value=''):
+        super(Option,self).__init__(doc,tag=self.TAG,node=node,name=name)
+        self.addText(value)
 
+    def getGlobalOptions(self):
+        return self.getParent(GlobalOptions)
+
 # --------------------------------------------------------------------
-def get_turncounter(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.CounterTurnLevel','property',asdict)
+class Preference(Element):
+    PREFS = 'VASSAL.preferences.'
+    def __init__(self,
+                 doc,
+                 tag,
+                 node    = None,
+                 name    = '',
+                 default = '',
+                 desc    = '',
+                 tab     = '',
+                 **kwargs):
+        '''Add a preference
 
+        Parameters
+        ----------
+        name : str
+            Name of property
+        default : str
+            Default value
+        desc : str
+            Description
+        tab : str
+            Preference tab to put in to
+        '''
+        super(Preference,self).__init__(doc,
+                                        tag     = tag,
+                                        node    = node,
+                                        name    = name,
+                                        default = default,
+                                        desc    = desc,
+                                        tab     = tab)
+
+    def getGlobalOptions(self):
+        return self.getParent(GlobalOptions)
+    
 # --------------------------------------------------------------------
-def get_turnlist(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.ListTurnLevel','property',asdict)
+class IntPreference(Preference):
+    TAG = Preference.PREFS+'IntegerPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = 0,
+                 desc    = '',
+                 tab     = ''):
+        super(IntPreference,self).__init__(doc,
+                                           tag     = self.TAG,
+                                           node    = node,
+                                           name    = name,
+                                           default = str(default),
+                                           desc    = desc,
+                                           tab     = tab)
+# --------------------------------------------------------------------
+class FloatPreference(Preference):
+    TAG = Preference.PREFS+'DoublePreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = 0.,
+                 desc    = '',
+                 tab     = ''):
+        super(FloatPreference,self).__init__(doc,
+                                             tag     = self.TAG,
+                                             node    = node,
+                                             name    = name,
+                                             default = str(default),
+                                             desc    = desc,
+                                             tab     = tab)
+# --------------------------------------------------------------------
+class BoolPreference(Preference):
+    TAG = Preference.PREFS+'BooleanPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = False,
+                 desc    = '',
+                 tab     = ''):
+        super(BoolPreference,self).__init__(doc,
+                                            tag     = self.TAG,
+                                            node    = node,
+                                            name    = name,
+                                            default = ('true' if default
+                                                       else 'false'),
+                                            desc    = desc,
+                                            tab     = tab)
+# --------------------------------------------------------------------
+class StrPreference(Preference):
+    TAG = Preference.PREFS+'StringPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = '',
+                 desc    = '',
+                 tab     = ''):
+        super(StrPreference,self).__init__(doc,
+                                           tag     = self.TAG,
+                                           node    = node,
+                                           name    = name,
+                                           default = default,
+                                           desc    = desc,
+                                           tab     = tab)
+# --------------------------------------------------------------------
+class TextPreference(Preference):
+    TAG = Preference.PREFS+'TextPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = '',
+                 desc    = '',
+                 tab     = ''):
+        super(TextPreference,self).__init__(doc,
+                                            tag     = self.TAG,
+                                            node    = node,
+                                            name    = name,
+                                            default = (default
+                                                       .replace('\n','
')),
+                                            desc    = desc,
+                                            tab     = tab)
+# --------------------------------------------------------------------
+class EnumPreference(Preference):
+    TAG = Preference.PREFS+'EnumPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 values  = [],
+                 default = '',
+                 desc    = '',
+                 tab     = ''):
+        ce = lambda v : str(v).replace(',',r'\,')
+        sl = [ce(v) for v in values]
+        df = ce(v)
+        assert df in sl, \
+            f'Default value "{default}" not in list {":".join(values)}'
+        super(EnumPreference,self).__init__(doc,
+                                            tag     = self.TAG,
+                                            node    = node,
+                                            name    = name,
+                                            default = df,
+                                            desc    = desc,
+                                            tab     = tab,
+                                            list    = sl)
 
+    
 # --------------------------------------------------------------------
-def get_turnhotkey(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.TurnGlobalHotkey','hotkey',asdict)
+# CurrentMap == "Board"
+class Inventory(GameElement):
+    TAG = Element.MODULE+'Inventory'
+    def __init__(self,doc,node=None,
+                  canDisable          = False,
+                  centerOnPiece       = True,
+                  disabledIcon        = '',
+                  drawPieces          = True,
+                  foldersOnly         = False,
+                  forwardKeystroke    = True,
+                  groupBy             = '',
+                  hotkey              = key('I',ALT),
+                  icon                = '/images/inventory.gif',
+                  include             = '{}',
+                  launchFunction      = 'functionHide',
+                  leafFormat          = '$PieceName$',
+                  name                = '',
+                  nonLeafFormat       = '$PropertyValue$',
+                  pieceZoom           = '0.33',
+                  pieceZoom2          = '0.5',
+                  pieceZoom3          = '0.6',
+                  propertyGate        = '',
+                  refreshHotkey       = key('I',ALT_SHIFT),
+                  showMenu            = True,
+                  sides               = '',
+                  sortFormat          = '$PieceName$',
+                  sortPieces          = True,
+                  sorting             = 'alpha',
+                  text                = '',
+                  tooltip             = 'Show inventory of all pieces',
+                  zoomOn              = False):
+        super(Inventory,self).__init__(doc,self.TAG,node=node,
+                                       canDisable          = canDisable,
+                                       centerOnPiece       = centerOnPiece,
+                                       disabledIcon        = disabledIcon,
+                                       drawPieces          = drawPieces,
+                                       foldersOnly         = foldersOnly,
+                                       forwardKeystroke    = forwardKeystroke,
+                                       groupBy             = groupBy,
+                                       hotkey              = hotkey,
+                                       icon                = icon,
+                                       include             = include,
+                                       launchFunction      = launchFunction,
+                                       leafFormat          = leafFormat,
+                                       name                = name,
+                                       nonLeafFormat       = nonLeafFormat,
+                                       pieceZoom           = pieceZoom,
+                                       pieceZoom2          = pieceZoom2,
+                                       pieceZoom3          = pieceZoom3,
+                                       propertyGate        = propertyGate,
+                                       refreshHotkey       = refreshHotkey,
+                                       showMenu            = showMenu,
+                                       sides               = sides,
+                                       sortFormat          = sortFormat,
+                                       sortPieces          = sortPieces,
+                                       sorting             = sorting,
+                                       text                = text,
+                                       tooltip             = tooltip,
+                                       zoomOn              = zoomOn)
+                  
+    
+        
 
+    
+    
+
+
+
+
+
+
 # --------------------------------------------------------------------
-def get_documentation(doc,one=True):
-    return get_asone(doc,MODULE+'Documentation',one)
+class Prototypes(GameElement):
+    TAG = Element.MODULE+'PrototypesContainer'
+    def __init__(self,game,node=None,**kwargs):
+        super(Prototypes,self).\
+            __init__(game,self.TAG,node=node,**kwargs)
 
+    def addPrototype(self,**kwargs):
+        '''Add a `Prototype` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Prototype
+            The added element
+        '''
+        return self.add(Prototype,**kwargs)
+    def getPrototypes(self,asdict=True):
+        '''Get all Prototype element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Prototype` elements.  If `False`, return a list of all Prototype` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Prototype` children
+        '''
+        return self.getElementsByKey(Prototype,'name',asdict=asdict)
+        
+    
+
+
 # --------------------------------------------------------------------
-def get_helpfiles(doc,key='fileName',asdict=True):
-    return get_asdict(doc,MODULE+'documentation.HelpFile',key,asdict)
+class DiceButton(GameElement):
+    TAG=Element.MODULE+'DiceButton'
+    def __init__(self,elem,node=None,
+                 addToTotal           = 0,
+                 canDisable           = False,
+                 hotkey               = key('6',ALT),
+                 icon                 = '/images/die.gif',
+                 keepCount            = 1,
+                 keepDice             = False,
+                 keepOption           = '>',
+                 lockAdd              = False,
+                 lockDice             = False,
+                 lockPlus             = False,
+                 lockSides            = False,
+                 nDice                = 1,
+                 nSides               = 6,
+                 name                 = '1d6',
+                 plus                 = 0,
+                 prompt               = False,
+                 propertyGate         = '',
+                 reportFormat         = '** $name$ = $result$ *** <$PlayerName$>;',
+                 reportTotal          = False,
+                 sortDice             = False,
+                 text                 = '1d6',
+                 tooltip              = 'Roll a 1d6'):
+        super(DiceButton,self).\
+            __init__(elem,self.TAG,node=node,
+                     addToTotal           = addToTotal,
+                     canDisable           = canDisable,
+                     hotkey               = hotkey,
+                     icon                 = icon,
+                     keepCount            = keepCount,
+                     keepDice             = keepDice,
+                     keepOption           = keepOption,
+                     lockAdd              = lockAdd,
+                     lockDice             = lockDice,
+                     lockPlus             = lockPlus,
+                     lockSides            = lockSides,
+                     nDice                = nDice,
+                     nSides               = nSides,
+                     name                 = name,
+                     plus                 = plus,
+                     prompt               = prompt,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportTotal          = reportTotal,
+                     sortDice             = sortDice,
+                     text                 = text,
+                     tooltip              = tooltip)
 
 # --------------------------------------------------------------------
-def get_tutorials(doc,key='name',asdict=True):
-    return get_asdict(doc,MODULE+'documentation.Tutorial',key,asdict)
+class GameMassKey(GlobalKey,GameElementService):
+    TAG = Element.MODULE+'GlobalKeyCommand'
+    def __init__(self,map,node=None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = GlobalKey.SELECTED,
+                 tooltip              = '',
+                 icon                 = ''):
+        '''Default targets are selected units'''
+        super(GameMassKey,self).\
+            __init__(map,
+                     self.TAG,
+                     node                 = node,
+                     name                 = name,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tooltip,
+                     icon                 = icon)
+        
+# --------------------------------------------------------------------
+class StartupMassKey(GlobalKey,GameElementService):
+    TAG = Element.MODULE+'StartupGlobalKeyCommand'
+    FIRST_LAUNCH = 'firstLaunchOfSession'
+    EVERY_LAUNCH = 'everyLaunchOfSession'
+    START_GAME   = 'startOfGameOnly'
+    def __init__(self,
+                 map,
+                 node                 = None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = GlobalKey.SELECTED,
+                 tooltip              = '',
+                 icon                 = '',
+                 whenToApply          = EVERY_LAUNCH):
+        '''Default targets are selected units'''
+        super(StartupMassKey,self).\
+            __init__(map,
+                     self.TAG,
+                     node                 = node,
+                     name                 = name,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tooltip,
+                     icon                 = icon)
+        if node is None:
+            self['whenToApply'] = whenToApply
 
 # --------------------------------------------------------------------
-def get_inventories(doc,key='name',asdict=False):
-    return get_asdict(doc,MODULE+'Inventory',key,asdict)
+class Menu(GameElement):
+    TAG = Element.MODULE+'ToolbarMenu'
+    def __init__(self,
+                 game,
+                 node                 = None,
+                 canDisable           = False,
+                 description          = '',
+                 disabledIcon         = '',
+                 hotkey               = '',
+                 icon                 = '',
+                 menuItems            = [],# Button texts
+                 propertyGate         = '',
+                 text                 = '',# Menu name
+                 tooltip              = ''):
+        if len(description) <= 0 and len(tooltip) > 0:
+            description = tooltip
+        if len(tooltip) <= 0 and len(description) > 0:
+            tooltip = description 
+        super(Menu,self).\
+            __init__(game,
+                     self.TAG,
+                     node                 = node,
+                     canDisable           = canDisable,
+                     description          = description,
+                     disabledIcon         = disabledIcon,
+                     hotkey               = hotkey,
+                     icon                 = icon,
+                     menuItems            = ','.join(menuItems),
+                     propertyGate         = propertyGate,
+                     text                 = text,
+                     tooltip              = tooltip)
+                     
+        
+# --------------------------------------------------------------------
+class SymbolicDice(GameElement):
+    TAG = Element.MODULE+'SpecialDiceButton'
+    def __init__(self,
+                 game,
+                 node                    = None,
+                 canDisable	         = False,
+                 disabledIcon            = '',
+                 hotkey                  = key('6',ALT),
+                 name                    = "Dice",  # GP prefix
+                 text                    = '', # Text on button
+                 icon                    = '/images/die.gif', # Icon on button
+                 format                  = '{name+": "+result1}', # Report 
+                 tooltip                 = 'Die roll', # Help
+                 propertyGate            = '', # Property to disable when T
+                 resultButton            = False, # Result on button?
+                 resultChatter           = True,  # Result in Chatter?
+                 resultWindow            = False, # Result window?
+                 backgroundColor	 = rgb(0xdd,0xdd,0xdd),  # Window background
+                 windowTitleResultFormat = "$name$", # Window title
+                 windowX                 = '67', # Window size
+                 windowY                 = '65'):
+        super(SymbolicDice,self).\
+            __init__(game,
+                     self.TAG,
+                     node                    = node,
+                     canDisable	             = canDisable,
+                     disabledIcon            = disabledIcon,
+                     hotkey                  = hotkey,
+                     name                    = name,
+                     text                    = text,
+                     icon                    = icon,
+                     format                  = format,
+                     tooltip                 = tooltip,
+                     propertyGate            = propertyGate,
+                     resultButton            = resultButton,
+                     resultChatter           = resultChatter,
+                     resultWindow            = resultWindow,
+                     backgroundColor	     = backgroundColor,
+                     windowTitleResultFormat = windowTitleResultFormat,
+                     windowX                 = windowX,
+                     windowY                 = windowY)
 
+    def addDie(self,**kwargs):
+        return self.add(SpecialDie,**kwargs)
+
+    def getSymbolicDice(self):
+        return self.getParent(SymbolicDice)
+        
+        
 # --------------------------------------------------------------------
-def get_maps(doc,asdict=True):
-    return get_asdict(doc,MODULE+'Map','mapName',asdict)
+class SpecialDie(GameElement):
+    TAG = Element.MODULE+'SpecialDie'
+    def __init__(self,
+                 symbolic,               # Symblic dice 
+                 node                    = None,
+                 name                    = '', # Name of dice (no GP)
+                 report                  = '{name+": "+result}',
+                 faces                   = None):
+        super(SpecialDie,self).\
+            __init__(symbolic,
+                     self.TAG,
+                     node = node,
+                     name = name,
+                     report = report)
+        if node is not None or faces is None:
+            return
+        if isinstance(faces,list):
+            faces = {i+1: f for i,f in enumerate(faces)}
+        for v,f in faces:
+            self.addFace(text = str(v), value = v, icon = f)
 
+    def addFace(self,**kwargs):
+        self.add(DieFace,**kwargs)
+
+    def getSymbolicDice(self):
+        return self.getParent(SymbolicDice)
+        
+
+        
+                     
 # --------------------------------------------------------------------
-def get_widgetmap(doc,asdict=True):
-    return get_asdict(doc,WIDGET+'WidgetMap','mapName',asdict)
+class DieFace(GameElement):
+    TAG = Element.MODULE+'SpecialDieFace'
+    def __init__(self,
+                 special,               # Special dice
+                 node,                  # existing node
+                 icon      = '',        # graphical representation
+                 text      = '',        # Text representation
+                 value     = 0):        # Value representation
+        super(DieFace,self).\
+            __init__(special,
+                     self.TAG,
+                     node      = node,
+                     icon      = icon,
+                     text      = text,
+                     value     = value)
+                     
+    def getSpecialDie(self):
+        return self.getParent(SpecialDie)
+#
+# EOF
+#
+# ====================================================================
+# From mapelements.py
 
 # --------------------------------------------------------------------
-def get_chartwindows(doc,asdict=True):
-    return get_asdict(doc,MODULE+'ChartWindow','name',asdict)
+class MapElementService:
+    def getMap(self):
+        '''Get map - either a Map or WidgetMap'''
+        return self.getParentOfClass([WidgetMap,Map])
+        # if self._parent is None:
+        #     return None
+        # 
+        # if 'WidgetMap' in self._parent.tagName:
+        #     return self.getParent(WidgetMap)
+        #     
+        # return self.getParent(Map)
+    def getGame(self):
+        m = self.getMap()
+        if m is not None: return m.getGame()
+        return None
 
 # --------------------------------------------------------------------
-def get_map(doc,name):
-    maps = get_maps(doc,True)
-    return maps.get(name, None) if maps else None
+class MapElement(Element,MapElementService):
+    def __init__(self,map,tag,node=None,**kwargs):
+        super(MapElement,self).__init__(map,tag,node=node,**kwargs)
 
+
 # --------------------------------------------------------------------
-def get_boards(doc,asdict=True):
-    return get_asdict(doc,PICKER+'Board','name',asdict)
+class StackMetrics(MapElement):
+    TAG=Element.MAP+'StackMetrics'
+    def __init__(self,map,node=None,
+                 bottom               = key('(',0),
+                 down                 = key('%',0),
+                 top                  = key('&',0),
+                 up                   = key("'",0),
+                 disabled             = False,
+                 exSepX               = 6,   # Expanded (after double click)
+                 exSepY               = 18,  # Expanded (after double click)
+                 unexSepX             = 8,   # Compact
+                 unexSepY             = 16): # Compact
+        super(StackMetrics,self).__init__(map,self.TAG,node=node,
+                                          bottom               = bottom,
+                                          disabled             = disabled,
+                                          down                 = down,
+                                          exSepX               = exSepX,
+                                          exSepY               = exSepY,
+                                          top                  = top,
+                                          unexSepX             = unexSepX,
+                                          unexSepY             = unexSepY,
+                                          up                   = up)
 
 # --------------------------------------------------------------------
-def get_zoned(doc,one=True):
-    return get_asone(doc,PICKER+'board.ZonedGrid',one)
+class ImageSaver(MapElement):
+    TAG=Element.MAP+'ImageSaver'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 canDisable           = False,
+                 hotkey               = '',
+                 icon                 = '/images/camera.gif',
+                 propertyGate         = '',
+                 tooltip              = 'Save map as PNG image'):
+        super(ImageSaver,self).__init__(map,self.TAG,node=node,
+                                        buttonText           = buttonText,
+                                        canDisable           = canDisable,
+                                        hotkey               = hotkey,
+                                        icon                 = icon,
+                                        propertyGate         = propertyGate,
+                                        tooltip              = tooltip)
+# --------------------------------------------------------------------
+class TextSaver(MapElement):
+    TAG=Element.MAP+'TextSaver'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 canDisable           = False,
+                 hotkey               = '',
+                 icon                 = '/images/camera.gif',
+                 propertyGate         = '',
+                 tooltip              = 'Save map as text'):
+        super(TextSaver,self).__init__(map,self.TAG,node=node,
+                                        buttonText           = buttonText,
+                                        canDisable           = canDisable,
+                                        hotkey               = hotkey,
+                                        icon                 = icon,
+                                        propertyGate         = propertyGate,
+                                        tooltip              = tooltip)
 
+
 # --------------------------------------------------------------------
-def get_zone(doc,asdict=True):
-    return get_asdict(doc,PICKER+'board.mapgrid.Zone','name',asdict)
+class ForwardToChatter(MapElement):
+    TAG=Element.MAP+'ForwardToChatter'
+    def __init__(self,map,node=None,**kwargs):
+        super(ForwardToChatter,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_hexgrid(doc,one=True):
-    return get_asone(doc,PICKER+'board.HexGrid',one)
+class MenuDisplayer(MapElement):
+    TAG=Element.MAP+'MenuDisplayer'
+    def __init__(self,map,node=None,**kwargs):
+        super(MenuDisplayer,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_squaregrid(doc,one=True):
-    return get_asone(doc,PICKER+'board.SquareGrid',one)
+class MapCenterer(MapElement):
+    TAG=Element.MAP+'MapCenterer'
+    def __init__(self,map,node=None,**kwargs):
+        super(MapCenterer,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_regiongrid(doc,one=True):
-    return get_asone(doc,PICKER+'board.RegionGrid',one)
+class StackExpander(MapElement):
+    TAG=Element.MAP+'StackExpander'
+    def __init__(self,map,node=None,**kwargs):
+        super(StackExpander,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_hexnumbering(doc,one=True):
-    return get_asone(doc,PICKER+'board.mapgrid.HexGridNumbering',one)
-        
+class PieceMover(MapElement):
+    TAG=Element.MAP+'PieceMover'
+    def __init__(self,map,node=None,**kwargs):
+        super(PieceMover,self).__init__(map,self.TAG,node=node,**kwargs)
+
 # --------------------------------------------------------------------
-def get_regions(doc,asdict=True):
-    return get_asdict(doc,PICKER+'board.Region', 'name', asdict)
+class SelectionHighlighters(MapElement):
+    TAG=Element.MAP+'SelectionHighlighters'
+    def __init__(self,map,node=None,**kwargs):
+        super(SelectionHighlighters,self).\
+            __init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_squarenumbering(doc,one=True):
-    return get_asone(doc,PICKER+'board.mapgrid.SquareGridNumbering', one)
+class KeyBufferer(MapElement):
+    TAG=Element.MAP+'KeyBufferer'
+    def __init__(self,map,node=None,**kwargs):
+        super(KeyBufferer,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_pieces(doc,asdict=True):
-    return get_asdict(doc,WIDGET+'PieceSlot','entryName',asdict)
+class HighlightLastMoved(MapElement):
+    TAG=Element.MAP+'HighlightLastMoved'
+    def __init__(self,map,node=None,
+                 color     = rgb(255,0,0),
+                 enabled   = True,
+                 thickness = 2):
+        super(HighlightLastMoved,self).__init__(map,self.TAG,node=node,
+                                                color     = color,
+                                                enabled   = enabled,
+                                                thickness = thickness)
 
 # --------------------------------------------------------------------
-def get_prototypecontainer(doc,one=True):
-    return get_asone(doc,MODULE+'PrototypesContainer',one)
+class CounterDetailViewer(MapElement):
+    TAG=Element.MAP+'CounterDetailViewer'
+    TOP_LAYER  = 'from top-most layer only'
+    ALL_LAYERS = 'from all layers'
+    INC_LAYERS = 'from listed layers only'
+    EXC_LAYERS = 'from layers other than those listed'
+    FILTER = 'by using a property filter'
+    def __init__(self,map,node=None,
+                 borderWidth          = 0,
+                 centerAll            = False,
+                 centerText           = False,
+                 combineCounterSummary = False,
+                 counterReportFormat  = '',
+                 delay                = 700,
+                 description          = '',
+                 display              = TOP_LAYER,
+                 emptyHexReportForma  = '$LocationName$',
+                 enableHTML           = True,
+                 extraTextPadding     = 0,
+                 fgColor              = rgb(0,0,0),
+                 fontSize             = 9,
+                 graphicsZoom         = 1.0,
+                 hotkey               = key('\n',0),
+                 layerList            = '',
+                 minDisplayPieces     = 2,
+                 propertyFilter       = '',
+                 showDeck             = False,
+                 showDeckDepth        = 1,
+                 showDeckMasked       = False,
+                 showMoveSelectde     = False,
+                 showNoStack          = False,
+                 showNonMovable       = False,
+                 showOverlap          = False,
+                 showgraph            = True,
+                 showgraphsingle      = False,
+                 showtext             = False,
+                 showtextsingle       = False,
+                 stretchWidthSummary  = False,
+                 summaryReportFormat  = '$LocationName$',
+                 unrotatePieces       = False,
+                 version              = 3,
+                 verticalOffset       = 0,
+                 verticalTopText      = 5,
+                 zoomlevel            = 1.0): # showTerrain attributes
+        super(CounterDetailViewer,self)\
+            .__init__(map,self.TAG,node=node,
+                      borderWidth           = borderWidth,
+                      centerAll             = centerAll,
+                      centerText            = centerText,
+                      combineCounterSummary = combineCounterSummary,
+                      counterReportFormat   = counterReportFormat,
+                      delay                 = delay,
+                      description           = description,
+                      display               = display,
+                      emptyHexReportForma   = emptyHexReportForma,
+                      enableHTML            = enableHTML,
+                      extraTextPadding      = extraTextPadding,
+                      fgColor               = fgColor,
+                      fontSize              = fontSize,
+                      graphicsZoom          = graphicsZoom,
+                      hotkey                = hotkey,
+                      layerList             = layerList,
+                      minDisplayPieces      = minDisplayPieces,
+                      propertyFilter        = propertyFilter,
+                      showDeck              = showDeck,
+                      showDeckDepth         = showDeckDepth,
+                      showDeckMasked        = showDeckMasked,
+                      showMoveSelectde      = showMoveSelectde,
+                      showNoStack           = showNoStack,
+                      showNonMovable        = showNonMovable,
+                      showOverlap           = showOverlap,
+                      showgraph             = showgraph,
+                      showgraphsingle       = showgraphsingle,
+                      showtext              = showtext,
+                      showtextsingle        = showtextsingle,
+                      stretchWidthSummary   = stretchWidthSummary,
+                      summaryReportFormat   = summaryReportFormat,
+                      unrotatePieces        = unrotatePieces,
+                      version               = version,
+                      verticalOffset        = verticalOffset,
+                      verticalTopText       = verticalTopText,
+                      zoomlevel             = zoomlevel)
 
 # --------------------------------------------------------------------
-def get_prototypes(doc,asdict=True):
-    return get_asdict(doc,MODULE+'PrototypeDefinition', 'name',asdict)
+class GlobalMap(MapElement):
+    TAG=Element.MAP+'GlobalMap'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 color                = rgb(255,0,0),
+                 hotkey               = key('O',CTRL_SHIFT),
+                 icon                 = '/images/overview.gif',
+                 scale                = 0.2,
+                 tooltip              = 'Show/Hide overview window'):
+        super(GlobalMap,self).\
+            __init__(map,self.TAG,node=node,
+                     buttonText           = buttonText,
+                     color                = color,
+                     hotkey               = hotkey,
+                     icon                 = icon,
+                     scale                = scale,
+                     tooltip              = 'Show/Hide overview window')
 
 # --------------------------------------------------------------------
-def get_masskey(map,asdict=True):
-    return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+class Zoomer(MapElement):
+    TAG = Element.MAP+'Zoomer'
+    def __init__(self,map,node=None,
+                 inButtonText         = '',
+                 inIconName           = '/images/zoomIn.gif',
+                 inTooltip            = 'Zoom in',
+                 outButtonText        = '',
+                 outIconName          = '/images/zoomOut.gif',
+                 outTooltip           = 'Zoom out',
+                 pickButtonText       = '',
+                 pickIconName         = '/images/zoom.png',
+                 pickTooltip          = 'Select Zoom',
+                 zoomInKey            = key('=',CTRL_SHIFT),
+                 zoomLevels           = [0.2,0.25,0.333,0.4,0.5,
+                                         0.555,0.625,0.75,1.0,1.25,1.6],
+                 zoomOutKey           = key('-'),
+                 zoomPickKey          = key('='),
+                 zoomStart            = 3):
+        '''Zoom start is counting from the back (with default zoom levels,
+        and zoom start, the default zoom is 1'''
+        lvls = ','.join([str(z) for z in zoomLevels])
+        super(Zoomer,self).\
+            __init__(map,self.TAG,node=node,
+                     inButtonText         = inButtonText,
+                     inIconName           = inIconName,
+                     inTooltip            = inTooltip,
+                     outButtonText        = outButtonText,
+                     outIconName          = outIconName,
+                     outTooltip           = outTooltip,
+                     pickButtonText       = pickButtonText,
+                     pickIconName         = pickIconName,
+                     pickTooltip          = pickTooltip,
+                     zoomInKey            = zoomInKey,
+                     zoomLevels           = lvls,
+                     zoomOutKey           = zoomOutKey,
+                     zoomPickKey          = zoomPickKey,
+                     zoomStart            = zoomStart)
 
 # --------------------------------------------------------------------
-def get_masskey(map,asdict=True):
-    return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+class HidePiecesButton(MapElement):
+    TAG=Element.MAP+'HidePiecesButton'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 hiddenIcon           = '/images/globe_selected.gif',
+                 hotkey               = key('O'),
+                 showingIcon          = '/images/globe_unselected.gif',
+                 tooltip              = 'Hide all pieces on this map'):
+        super(HidePiecesButton,self).\
+            __init__(map,self.TAG,node=node,
+                     buttonText           = buttonText,
+                     hiddenIcon           = hiddenIcon,
+                     hotkey               = hotkey,
+                     showingIcon          = showingIcon,
+                     tooltip              = tooltip)
+        
+# --------------------------------------------------------------------
+class MassKey(GlobalKey,MapElementService):
+    TAG = Element.MAP+'MassKeyCommand'
+    def __init__(self,map,node=None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = GlobalKey.SELECTED,
+                 tooltip              = '',
+                 icon                 = ''):
+        '''Default targets are selected units'''
+        super(MassKey,self).\
+            __init__(map,self.TAG,node=node,
+                     name                 = name,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tooltip,
+                     icon                 = icon)
 
 
 # --------------------------------------------------------------------
-def get_piece_parts(code):
-    '''Takes the code part of a pieceslot (the text node) and decodes
-    it into traits.
+class Flare(MapElement):
+    TAG=Element.MAP+'Flare'
+    def __init__(self,map,node=None,
+                 circleColor          = rgb(255,0,0),
+                 circleScale          = True,
+                 circleSize           = 100,
+                 flareKey             = 'keyAlt',
+                 flareName            = 'Map Flare',
+                 flarePulses          = 6,
+                 flarePulsesPerSec    = 3,
+                 reportFormat         = ''):
+        super(Flare,self).__init__(map,self.TAG,node=node,
+                                   circleColor          = circleColor,
+                                   circleScale          = circleScale,
+                                   circleSize           = circleSize,
+                                   flareKey             = flareKey,
+                                   flareName            = flareName,
+                                   flarePulses          = flarePulses,
+                                   flarePulsesPerSec    = flarePulsesPerSec,
+                                   reportFormat         = '')
 
-    One can use this to modify the pieces.  One could for example
-    remove a trait by calling this function, and remove the element
-    corresponding to the trait from the returned list. Then one must
-    collect up the definitions and states separately and pass each of
-    them to `enc_parts', which gives us two strings that can be passed
-    to `add_piece' (or `add_proto') function to get the new text node
-    content of the piece slot or prototype definition.  For example
+# --------------------------------------------------------------------
+class AtStart(MapElement):
+    TAG = Element.MODULE+'map.SetupStack'
+    def __init__(self,map,node=None,
+                 name            = '',
+                 location        = '',
+                 useGridLocation = True,
+                 owningBoard     = '',
+                 x               = 0,
+                 y               = 0):
+        '''Pieces are existing PieceSlot elements'''
+        super(AtStart,self).\
+            __init__(map,self.TAG,node=node,
+                     name            = name,
+                     location        = location,
+                     owningBoard     = owningBoard,
+                     useGridLocation = useGridLocation,
+                     x               = x,
+                     y               = y)
 
-        piece  = get_pieceslots(root,pieces)['foo']
-        code   = piece.childNodes[0].nodeValue
-        traits = get_piece_parts(code)
-        ... # Modify the list
-    
-        # Separate out definitions and states 
-        defs   = [enc_def(e['def'])   for e in traits]
-        states = [enc_def(e['state']) for e in traits]
+    def addPieces(self,*pieces):
+        '''Add a `Pieces` element to this
 
-        # Encode definitions and states as a single string, and then form body 
-        def    = enc_parts(*defs)
-        state  = enc_parts(*states)
-        body   = add_piece(def,state)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Pieces
+            The added element
+        '''
+        # copy pieces here
+        copies = []
+        for p in pieces:
+            c = self.addPiece(p)
+            if c is not None:
+                copies.append(c)
+        return copies
+        
+    def addPiece(self,piece):
+        '''Add a `Piece` element to this
 
-        # Set new definition 
-        piece.childNodes[0].nodeValue = body 
-        
-    See also dicts_to_piece
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Piece
+            The added element
+        '''
+        if not isinstance(piece,PieceSlot):
+            print(f'Trying to add {type(piece)} to AtStart')
+            return None
+            
+        p = piece.clone(self)
+        # self._node.appendChild(p._node)
+        return p
     
-    Parameters
-    ----------
-    code : str
-        Text node content of <PieceSlot> or <PrototypeDefinition> tags
-    
-    Returns
-    -------
-    traits : list of dict
-        All the traits in a list.  Each element of the list is a
-        dictionary with the keys 'def' and 'status'.
+    def getPieces(self,asdict=True):
+        '''Get all Piece element(s) from this
 
-        The key 'def' points to a list of the trait attributes used to
-        define the trait type.  The first element of the 'def' list is
-        the trait identifier.  The rest are the arguments for that
-        trait type.  Note, all fields are encoded as strings. 
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Piece`
+            elements.  If `False`, return a list of all Piece`
+            children.
 
-        The key 'status' encodes the status of the trait.  The format
-        of this depends on the trait.Note, all fields are encoded as
-        strings.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Piece` children
 
-    '''
-    cmd,iden,typ,sta = code.split('/')
-    defs = typ.split(r'	')
-    stas = sta.split(r'	')
-    defs = [d.rstrip("\\").split(';') for d in defs]
-    stas = [s.rstrip("\\").split(';') for s in stas]
-    ret  = [{'def': d, 'state': s} for d,s in zip(defs,stas)]
-    return ret
+        '''
+        return self.getElementsWithKey(PieceSlot,'entryName',asdict)
 
+#
+# EOF
+#
+# ====================================================================
+# From globalproperty.py
+
 # --------------------------------------------------------------------
-def dicts_to_piece(traits):
-    # Separate out definitions and states 
-    defs   = [enc_def(e['def'])   for e in traits]
-    states = [enc_def(e['state']) for e in traits]
+class GlobalProperties(Element):
+    TAG = Element.MODULE+'properties.GlobalProperties'
+    def __init__(self,elem,node=None,**named):
+        super(GlobalProperties,self).__init__(elem,self.TAG,node=node)
+        
+        for n, p in named:
+            self.addProperty(n, **p)
 
-    # Encode definitions and states as a single string, and then form body 
-    df     = enc_parts(*defs)
-    state  = enc_parts(*states)
-    body   = add_piece(df,state)
+    def getGame(self):
+        return self.getParent(Game)
+    def addProperty(self,**kwargs):
+        '''Add a `Property` element to this
 
-    return body
-    
-    
-# ====================================================================
-# Key encoding
-SHIFT = 65
-CTRL = 130
-ALT   = 520
-CTRL_SHIFT = CTRL+SHIFT
-ALT_SHIFT = ALT+SHIFT
-NONE = '\ue004'
-NONE_MOD = 0
-def key(let,mod=CTRL):
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Property
+            The added element
+        '''
+        return GlobalProperty(self,node=None,**kwargs)
+    def getProperties(self):
+        return getElementsByKey(GlobalProperty,'name')
+        
 
-    '''Encode a key sequence
+# --------------------------------------------------------------------
+class GlobalProperty(Element):
+    TAG = Element.MODULE+'properties.GlobalProperty'
+    def __init__(self,elem,node=None,
+                 name         = '',
+                 initialValue = '',
+                 isNumeric    = False,
+                 min          = "null",
+                 max          = "null",
+                 wrap         = False,
+                 description  = ""):
+        super(GlobalProperty,self).__init__(elem,self.TAG,
+                                            node         = node,
+                                            name         = name,
+                                            initialValue = initialValue,
+                                            isNumeric    = isNumeric,
+                                            min          = min,
+                                            max          = max,
+                                            wrap         = wrap,
+                                            description  = description)
 
-    Parameters
-    ----------
-    let : str
-        Key code (Letter)
-    mod : int
-        Modifier mask
-    '''
-    return f'{ord(let)},{mod}'
-# Colour encoding 
-def rgb(r,g,b):
-    return ','.join([str(r),str(g),str(b)])
+    def getGlobalProperties(self):
+        return self.getParent(GlobalProperties)
 
-def rgba(r,g,b,a):
-    return ','.join([str(r),str(g),str(b),str(a)])
 
+#
+# EOF
+#
+# ====================================================================
+# From turn.py
 
 # --------------------------------------------------------------------
-def set_node_attr(node,**attr):
-    for k,v in attr.items():
-        node.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
-    
-# --------------------------------------------------------------------
-def add_node(root,parent,tag,**attr):
-    e = root.createElement(tag)
-    if parent: parent.appendChild(e)
-    set_node_attr(e,**attr)
+class TurnLevel(Element):
+    def __init__(self,elem,tag,node=None,**kwargs):
+        super(TurnLevel,self).__init__(elem,tag,node=node,**kwargs)
 
-    return e
+    def addLevel(self,counter=None,phases=None):
+        '''Add a `Level` element to this
 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Level
+            The added element
+        '''
+        if counter is None and phases is None:
+            return self
+        
+        t = TurnCounter if counter is not None else TurnList
+        o = counter     if counter is not None else phases
 
-# --------------------------------------------------------------------
-def add_text(root,parent,text):
-    t = root.createTextNode(text)
-    parent.appendChild(t)
-    return t
+        subcounter = o.pop('counter',None)
+        subphases  = o.pop('phases',None)
 
-# --------------------------------------------------------------------
-def add_game(root,doc,
-             name,
-             version, 
-             ModuleOther1    = "",
-             ModuleOther2    = "",
-             VassalVersion   = "3.6.7",
-             description     = "",
-             nextPieceSlotId = 0):
-    return add_node(root,doc,BUILD+'GameModule',
-                    name            = name,
-                    version         = version,
-                    ModuleOther1    = ModuleOther1,
-                    ModuleOther2    = ModuleOther2,
-                    VassalVersion   = VassalVersion,
-                    description     = description,
-                    nextPieceSlotId = nextPieceSlotId)
+        s = t(self,node=None,**o)
 
-# --------------------------------------------------------------------
-def add_basiccommandencoder(root,doc):
-    return add_node(root,doc,MODULE+'BasicCommandEncoder')
+        return s.addLevel(subcounter, subphases)
 
-# --------------------------------------------------------------------
-def add_globalproperties(root,elem,*props,**named):
-    gp = add_node(root,elem,MODULE+'properties.GlobalProperties')
-    # Add elements where each is a dict with _at least_
-    # "{'name':NAME, 'initialValue':VALUE}"
-    for p in props:
-        add_globalproperty(root,gp,**p)
-    # Add elements where each is a named dict with _at least_
-    # "{'initialValue':VALUE}"
-    for n, p in named:
-        add_globalproperty(root,gp,name=n,**p)
+    def getUp(self):
+        return self.getParent(TurnLevel)
+    def addCounter(self,**kwargs):
+        '''Add a `Counter` element to this
 
-    return gp
-        
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Counter
+            The added element
+        '''
+        return self.add(self,TurnCounter,**kwargs)
+    def addList(self,**kwargs):
+        '''Add a `List` element to this
 
-# --------------------------------------------------------------------
-def add_globalproperty(root,elem,name,initialValue,
-                       isNumeric   = False,
-                       min         = "null",
-                       max         = "null",
-                       wrap        = False,
-                       description = ""):
-    return add_node(root,elem,MODULE+'properties.GlobalProperty',
-                    name         = name,
-                    initialValue = initialValue,
-                    isNumeric    = isNumeric,
-                    min          = min,
-                    max          = max,
-                    wrap         = wrap,
-                    description  = description)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : List
+            The added element
+        '''
+        return self.add(self,TurnList,**kwargs)
+    def getCounter(self):
+        return self.getAllElements(TurnCounter)
+    def getList(self):
+        return self.getAllElements(TurnList)
 
 # --------------------------------------------------------------------
-def add_turntrack(root, elem, name,
-                  buttonText       = 'Turn',
-                  hotkey           = '',
-                  icon             = '',
-                  length           = -1,
-                  lengthStyle      = 'Maximum',
-                  nexthotkey       = key('T',ALT),
-                  plusButtonSize   = 22,
-                  prevhotkey       = key('T',ALT_SHIFT),
-                  reportFormat     = '<$PlayerId$> Turn updated from $oldTurn$ to $newTurn$',
-                  turnButtonHeight = 22,
-                  turnFormat       = None,
-                  counter          = None,
-                  phases           = None):
-
-    levels = (counter if counter is not None else
-              phases if phases is not None else None)
-    if levels is not None:
-        lvl = 1
-        lvls = [f'$level{lvl}$']
-        sub  = levels
-        while True:
-            sub = sub.get('counter',sub.get('phases',None))
-            if sub is None:
-                break
-            lvl += 1
-            lvls.append(f'$level{lvl}$')
+class TurnTrack(TurnLevel):
+    TAG = Element.MODULE+'turn.TurnTracker'
+    def __init__(self,elem,node=None,
+                 name             = '',
+                 buttonText       = 'Turn',
+                 hotkey           = '',
+                 icon             = '',
+                 length           = -1,
+                 lengthStyle      = 'Maximum',
+                 nexthotkey       = key('T',ALT),
+                 plusButtonSize   = 22,
+                 prevhotkey       = key('T',ALT_SHIFT),
+                 reportFormat     = 'Turn updated from $oldTurn$ to $newTurn$',
+                 turnButtonHeight = 22,
+                 fwdOnly          = True,
+                 turnFormat       = None,
+                 counter          = None,
+                 phases           = None):
+        levels = (counter if counter is not None else
+                  phases if phases is not None else None)
+        if levels is not None:
+            lvl = 1
+            lvls = [f'$level{lvl}$']
+            sub  = levels
+            while True:
+                sub = sub.get('counter',sub.get('phases',None))
+                if sub is None:
+                    break
+                lvl += 1
+                lvls.append(f'$level{lvl}$')
             
-        turnFormat = ' '.join(lvls)
+            turnFormat = ' '.join(lvls)
         
-    if turnFormat is None:
-        turnFormat = '$level1$ $level2$ $level3$ $level4$'        
+        if turnFormat is None:
+            turnFormat = '$level1$ $level2$ $level3$ $level4$'        
         
-    t = add_node(root, elem, MODULE+'turn.TurnTracker',
-                 name             = name,
-                 buttonText       = buttonText,
-                 hotkey           = hotkey,
-                 icon             = icon,
-                 length           = length,
-                 lengthStyle      = lengthStyle,
-                 nexthotkey       = nexthotkey,
-                 plusButtonSize   = plusButtonSize,
-                 prevhotkey       = prevhotkey,
-                 reportFormat     = reportFormat,
-                 turnButtonHeight = turnButtonHeight,
-                 turnFormat       = turnFormat)
+        super(TurnTrack,self).__init__(elem, self.TAG,
+                                       node             = node,
+                                       name             = name,
+                                       buttonText       = buttonText,
+                                       hotkey           = hotkey,
+                                       icon             = icon,
+                                       length           = length,
+                                       lengthStyle      = lengthStyle,
+                                       nexthotkey       = nexthotkey,
+                                       plusButtonSize   = plusButtonSize,
+                                       prevhotkey       = prevhotkey,
+                                       reportFormat     = reportFormat,
+                                       turnButtonHeight = turnButtonHeight,
+                                       turnFormat       = turnFormat)
 
-    add_turnlevel(root, t, counter=counter, phases=phases)
-    
-    return t
+        self.addLevel(counter=counter, phases=phases)
 
-# --------------------------------------------------------------------
-def add_turnlevel(root,parent,counter=None,phases=None):
-    f,s = ((add_turncounter,counter) if counter is not None else
-           (add_turnlist,phases)     if phases  is not None else (None,None))
-    if f is None: return parent
+    def getGame(self):
+        return self.getParent(Game)
+    def getLists(self,asdict=True):
+        '''Get all List element(s) from this
 
-    subcounter = s.pop('counter',None)
-    subphases  = s.pop('phases',None)
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `List`
+            elements.  If `False`, return a list of all List`
+            children.
+        
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `List` children
 
-    sub = f(root,parent,**s)
+        '''
+        return self.getElementsByKey(TurnList,'property',asdict=asdict)
+    def getCounters(self,asdict=True):
+        '''Get all Counter element(s) from this
 
-    return add_turnlevel(root,sub,counter=subcounter,phases=subphases)
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Counter`
+            elements.  If `False`, return a list of all Counter`
+            children.
 
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Counter` children
+
+        '''
+        return self.getElementsByKey(TurnCounter,'property',asdict=asdict)
+    def addHotkey(self,**kwargs):
+        '''Add a `Hotkey` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Hotkey
+            The added element
+        '''
+        return self.add(TurnGlobalHotkey,**kwargs)
+    def getHotkeys(self,asdict):
+        return self.getElementsByKey(TurnGlobalHotkey,'name',asdict=asdict)
+    def encode(self):
+        ret = f'TURN{self["name"]}\t'
+        
+        return []
+
+        
 # --------------------------------------------------------------------
-def add_turncounter(root,elem,property,
-                    incr          = 1,
-                    loop          = False,
-                    loopLimit     = -1,
-                    turnFormat    = "$value$"):
-    c = add_node(root,elem,MODULE+"turn.CounterTurnLevel",
-                 property       = property,
-                 incr           = incr,
-                 loop           = loop,
-                 loopLimit      = loopLimit,
-                 turnFormat     = turnFormat)
-    return c
+class TurnCounter(TurnLevel):
+    TAG = Element.MODULE+"turn.CounterTurnLevel"
+    def __init__(self,elem,node=None,
+                 property      = '',
+                 start         = 1,
+                 incr          = 1,
+                 loop          = False,
+                 loopLimit     = -1,
+                 turnFormat    = "$value$"):
+        super(TurnCounter,self).__init__(elem,self.TAG,node=node,
+                                         property       = property,
+                                         start          = start,
+                                         incr           = incr,
+                                         loop           = loop,
+                                         loopLimit      = loopLimit,
+                                         turnFormat     = turnFormat)
                     
 # --------------------------------------------------------------------
-def add_turnlist(root,elem,property,names,
+class TurnList(TurnLevel):
+    TAG = Element.MODULE+"turn.ListTurnLevel"
+    def __init__(self,elem,node=None,
+                 property      = '',
+                 names         = [],
                  configFirst   = False,
                  configList    = False,
                  turnFormat    = '$value$'):
-    c = add_node(root,elem,MODULE+"turn.ListTurnLevel",
-                 property       = property,
-                 list           = ','.join([str(p) for p in names]),
-                 configFirst    = configFirst,
-                 configList     = configList,
-                 turnFormat     = turnFormat)
-    return c
+        super(TurnList,self).\
+            __init__(elem,self.TAG,node=node,
+                     property       = property,
+                     list           = ','.join([str(p) for p in names]),
+                     configFirst    = configFirst,
+                     configList     = configList,
+                     turnFormat     = turnFormat)
                   
 # --------------------------------------------------------------------
-def add_turnhotkey(root,elem,
-                   hotkey,
-                   match        = '{phase=="first"}',
-                   reportFormat = '{PlayerName}',
-                   name         = ''):
-    return add_node(root,elem,MODULE+'turn.TurnGlobalHotkey',
-                    hotkey       = hotkey,
-                    match        = match,
-                    reportFormat = reportFormat,
-                    name         = name)
+class TurnGlobalHotkey(Element):
+    TAG = Element.MODULE+'turn.TurnGlobalHotkey'
+    def __init__(self,elem,
+                 node         = None,
+                 hotkey       = '',
+                 match        = '{true}',
+                 reportFormat = '',
+                 name         = ''):
+        '''Global key activated by turn change
 
-                   
-                  
-# --------------------------------------------------------------------
-def add_translatable(root,elem):
-    return add_node(root,elem,MODULE+'properties.GlobalTranslatableMessages')
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        hotkey : str
+            What to send (global command)
+        match : str
+            When to send
+        reportFormat : str
+            What to what
+        name : str
+            A free form name
+        '''
+        super(TurnGlobalHotkey,self).__init__(elem,self.TAG,
+                                              node         = node,
+                                              hotkey       = hotkey,
+                                              match        = match,
+                                              reportFormat = reportFormat,
+                                              name         = name)
 
-# --------------------------------------------------------------------
-def add_language(root,elem):
-    return add_node(root,elem,'VASSAL.i18n.Language')
+    def getTurnTrack(self):
+        '''Get the turn track'''
+        return self.getParent(TurnTrack)
 
+#
+# EOF
+#
+# ====================================================================
+# From documentation.py
+
+# ====================================================================
+def createKeyHelp(*args,**kwargs):
+    '''Creates a help file with key-bindings
+
+    See Documentation.createKeyHelp
+    '''
+    return Documentation.createKeyHelp(*args,**kwargs)
+
 # --------------------------------------------------------------------
-def add_chatter(root,elem):
-    return add_node(root,elem,MODULE+'Chatter')
+class Documentation(GameElement):
+    TAG=Element.MODULE+'Documentation'
+    def __init__(self,doc,node=None,**kwargs):
+        '''Documentation (or help menu)
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        kwargs : dict
+            Attributes
+        '''
+        super(Documentation,self).__init__(doc,self.TAG,node=node,**kwargs)
+
+    def addAboutScreen(self,**kwargs):
+        '''Add a `AboutScreen` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : AboutScreen
+            The added element
+        '''
+        return self.add(AboutScreen,**kwargs)
+    def addHelpFile(self,**kwargs):
+        '''Add a `HelpFile` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HelpFile
+            The added element
+        '''
+        return self.add(HelpFile,**kwargs)
+    def addBrowserHelpFile(self,**kwargs):
+        '''Add a `BrowserHelpFile` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BrowserHelpFile
+            The added element
+        '''
+        return self.add(HelpBrowserFile,**kwargs)
+    def addBrowserPDFFile(self,**kwargs):
+        '''Add a `BrowserPDFFile` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BrowserPDFFile
+            The added element
+        '''
+        return self.add(BrowserPDFFile,**kwargs)
+    def addTutorial(self,**kwargs):
+        '''Add a `Tutorial` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Tutorial
+            The added element
+        '''
+        return self.add(Tutorial,**kwargs)
+    def getAboutScreens(self):
+        return self.getElementsByKey(AboutScreen,'title')
+    def getHelpFiles(self):
+        return self.getElementsByKey(HelpFile,'title')
+    def getBrowserHelpFiles(self):
+        return self.getElementsByKey(BrowserHelpFile,'title')
+    def getBrowserPDFFiles(self):
+        return self.getElementsByKey(BrowserPDFFile,'title')
+    def getTutorials(self):
+        return self.getElementsByKey(Tutorial,'name')
+
+    @classmethod
+    def createKeyHelp(cls,keys,title='',version=''):
+        '''Creates a help file with key-bindings
+        
+        Parameters
+        ----------
+        keys : list of list of str
+             List of key-binding documentation
+        title : str
+             Title of help file
+        version : str
+             Version number
+        
+        Returns
+        -------
+        txt : str
+            File content
+        '''
+        txt = f'''
+        <html>
+         <body>
+          <h1>{title} (Version {version}) Key bindings</h1>
+          <table>
+          <tr><th>Key</th><th>Where</th><th>Effect</th></tr>'''
+        
+        for key, where, description in keys:
+            txt += (f'<tr><td>{key}</td>'
+                    f'<td>{where}</td>'
+                    f'<td>{description}</td></tr>')
+        
+        txt += '''
+          </table>
+         </body>
+        </html>'''
+        
+        return txt 
+
+
 # --------------------------------------------------------------------
-def add_keynamer(root,elem):
-    return add_node(root,elem,MODULE+'KeyNamer')
+class AboutScreen(Element):
+    TAG = Element.MODULE+'documentation.AboutScreen'
+    def __init__(self,doc,node=None,title='',fileName=""):
+        '''Create an about screen element that shows image
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        fileName : str
+            Internal file name        
+        '''
+        super(AboutScreen,self).__init__(doc,
+                                         self.TAG,
+                                         node     = node,
+                                         fileName = fileName,
+                                         title    = title)
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+
 # --------------------------------------------------------------------
-def add_documentation(root,doc):
-    return add_node(root,doc,MODULE+'Documentation')
+class BrowserPDFFile(Element):
+    TAG = Element.MODULE+'documentation.BrowserPDFFile'
+    def __init__(self,doc,node=None,title='',pdfFile=''):
+        '''Create help menu item that opens an embedded PDF
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        pdfFile : str
+            Internal file name
+        '''
+        super(BrowserPDFFile,self).__init__(doc,self.TAG,
+                                            node    = node,
+                                            pdfFile = pdfFile,
+                                            title   = title)
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
 
 # --------------------------------------------------------------------
-def add_about(root,doc,title,fileName=""):
-    return add_node(root,doc,MODULE+'documentation.AboutScreen',
-                    fileName = fileName,
-                    title = title)
-# --------------------------------------------------------------------
-def add_pdffile(root,doc,title,pdfFile):
-    return add_node(root,doc,MODULE+'documentation.BrowserPDFFile',
-                    pdfFile = pdfFile, title=title)
-# --------------------------------------------------------------------
-def add_helpfile(root,doc,title,fileName,fileType="archive"):
-    return add_node(root,doc,MODULE+'documentation.HelpFile',
-                    fileName = fileName,
-                    fileType = fileType,
-                    title    = title)
+class HelpFile(Element):
+    TAG = Element.MODULE+'documentation.HelpFile'
+    ARCHIVE = 'archive'
+    def __init__(self,doc,node=None,
+                 title='',
+                 fileName='',
+                 fileType=ARCHIVE):
+        '''Create a help menu entry that opens an embeddded file
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        fileName : str
+            Internal file name
+        fileType : str
+            How to find the file 
+        '''
+        super(HelpFile,self).__init__(doc,self.TAG,node=node,
+                                      fileName = fileName,
+                                      fileType = fileType,
+                                      title    = title)
 
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+
 # --------------------------------------------------------------------
-def add_htmlfile(root,doc,title,startingPage='index.html'):
-    return add_node(root,doc,MODULE+'documentation.BrowserHelpFile',
-                    startingPage=startingPage,
-                    title=title)
+class BrowserHelpFile(Element):
+    TAG = Element.MODULE+'documentation.BrowserHelpFile'
+    def __init__(self,doc,node=None,
+                 title='',
+                 startingPage='index.html'):
+        '''Create a help menu entry that opens an embeddded HTML
+        page (with possible sub-pages) file
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        startingPage : str
+            which file to start at
+        '''
+        super(BrowserHelpFile,self).__init__(doc,self.TAG,node=node,
+                                             startingPage=startingPage,
+                                             title=title)
 
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+        
 # --------------------------------------------------------------------
-def add_tutorial(root,doc,
+class Tutorial(Element):
+    TAG = Element.MODULE+'documentation.Tutorial'
+    def __init__(self,doc,node=None,
                  name            = 'Tutorial',
                  logfile         = 'tutorial.vlog',
                  promptMessage   = 'Load the tutorial?',
                  welcomeMessage  = 'Press "Step forward" (PnDn) to step through the tutorial',
                  launchOnStartup = True):
-    return add_node(root,doc,MODULE+'documentation.Tutorial',
-                    name            = name,
-                    logfile         = logfile,
-                    promptMessage   = promptMessage,
-                    welcomeMessage  = welcomeMessage,
-                    launchOnStartup = launchOnStartup)
+        '''Add a help menu item that loads the tutorial
 
+        Also adds the start-up option to run the tutorial
+
+
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        name : str
+            Name of entry
+        logfile : str
+            Internal file name
+        promptMessage : str
+            What to show
+        launchOnStartup : bool
+            By default, launch tutorial first time running module
+        '''
+        super(Tutorial,self).__init__(doc,self.TAG,node=node,
+                                      name            = name,
+                                      logfile         = logfile,
+                                      promptMessage   = promptMessage,
+                                      welcomeMessage  = welcomeMessage,
+                                      launchOnStartup = launchOnStartup)
+
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+
+
+#
+# EOF
+#
+# ====================================================================
+# From player.py
+
 # --------------------------------------------------------------------
-def add_roster(root,doc,buttonKeyStroke='',
+class PlayerRoster(GameElement):
+    TAG = Element.MODULE+'PlayerRoster'
+    def __init__(self,doc,node=None,buttonKeyStroke='',
                buttonText='Retire',
                buttonToolTip='Switch sides, become observer, or release faction'):
-    return add_node(root,doc,MODULE+'PlayerRoster',
-                    buttonKeyStroke = buttonKeyStroke,
-                    buttonText      = buttonText,
-                    buttonToolTip   = buttonToolTip)
+        '''Add a player roster to the module
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        buttonText : str
+            Text on button
+        buttonTooltip : str
+            Tool tip to show when hovering over button
+        '''
+        super(PlayerRoster,self).__init__(doc,self.TAG,node=node,
+                                          buttonKeyStroke = buttonKeyStroke,
+                                          buttonText      = buttonText,
+                                          buttonToolTip   = buttonToolTip)
+    def addSide(self,name):
+        '''Add a `Side` element to this
 
-# --------------------------------------------------------------------
-def add_entry(root,doc,text):
-    n = add_node(root,doc,'entry')
-    add_text(root,n,text)
-    return n
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Side
+            The added element
+        '''
+        return self.add(PlayerSide,name=name)
+    def getSides(self):
+        '''Get all sides'''
+        return self.getAllElements(PlayerSide,False)
+    def encode(self):
+        '''Encode for save'''
+        return ['PLAYER\ta\ta\t<observer>']
 
 # --------------------------------------------------------------------
-def add_globaloptions(root,doc,
-                      autoReport               = "Use Preferences Setting",
-                      centerOnMove             = "Use Preferences Setting",
-                      chatterHTMLSupport       = "Always",
-                      hotKeysOnClosedWindows   = "Never",
-                      inventoryForAll          = "Always",
-                      nonOwnerUnmaskable       = "Use Preferences Setting",
-                      playerIdFormat           = "$playerName$",
-                      promptString             = "Opponents can unmask pieces",
-                      sendToLocationMoveTrails = "Never"):
-    return add_node(root,doc,MODULE+'GlobalOptions',
-                      autoReport               = autoReport,
-                      centerOnMove             = centerOnMove,
-                      chatterHTMLSupport       = chatterHTMLSupport,
-                      hotKeysOnClosedWindows   = hotKeysOnClosedWindows,
-                      inventoryForAll          = inventoryForAll,
-                      nonOwnerUnmaskable       = nonOwnerUnmaskable,
-                      playerIdFormat           = playerIdFormat,
-                      promptString             = promptString,
-                      sendToLocationMoveTrails = sendToLocationMoveTrails)
+class PlayerSide(Element):
+    TAG = 'entry'
+    def __init__(self,doc,node=None,name=''):
+        '''Adds a side to the player roster
 
-# --------------------------------------------------------------------
-def add_option(root,doc,name,value):
-    n = add_node(root,doc,'option',name=name)
-    add_text(n,root,value)
-    return n
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        name : str
+            Name of side 
+        '''
+        super(PlayerSide,self).__init__(doc,self.TAG,node=node)
+        if node is None:
+            self.addText(name)
 
-# --------------------------------------------------------------------
-# CurrentMap == "Board"
-def add_inventory(root,doc,
-                  canDisable          = False,
-                  centerOnPiece       = True,
-                  disabledIcon        = '',
-                  drawPieces          = True,
-                  foldersOnly         = False,
-                  forwardKeystroke    = True,
-                  groupBy             = '',
-                  hotkey              = key('I',ALT),
-                  icon                = '/images/inventory.gif',
-                  include             = '{}',
-                  launchFunction      = 'functionHide',
-                  leafFormat          = '$PieceName$',
-                  name                = '',
-                  nonLeafFormat       = '$PropertyValue$',
-                  pieceZoom           = '0.33',
-                  pieceZoom2          = '0.5',
-                  pieceZoom3          = '0.6',
-                  propertyGate        = '',
-                  refreshHotkey       = key('I',ALT_SHIFT),
-                  showMenu            = True,
-                  sides               = '',
-                  sortFormat          = '$PieceName$',
-                  sortPieces          = True,
-                  sorting             = 'alpha',
-                  text                = '',
-                  tooltip             = 'Show inventory of all pieces',
-                  zoomOn              = False):
-    return add_node(root,doc,MODULE+'Inventory',
-                    canDisable          = canDisable,
-                    centerOnPiece       = centerOnPiece,
-                    disabledIcon        = disabledIcon,
-                    drawPieces          = drawPieces,
-                    foldersOnly         = foldersOnly,
-                    forwardKeystroke    = forwardKeystroke,
-                    groupBy             = groupBy,
-                    hotkey              = hotkey,
-                    icon                = icon,
-                    include             = include,
-                    launchFunction      = launchFunction,
-                    leafFormat          = leafFormat,
-                    name                = name,
-                    nonLeafFormat       = nonLeafFormat,
-                    pieceZoom           = pieceZoom,
-                    pieceZoom2          = pieceZoom2,
-                    pieceZoom3          = pieceZoom3,
-                    propertyGate        = propertyGate,
-                    refreshHotkey       = refreshHotkey,
-                    showMenu            = showMenu,
-                    sides               = sides,
-                    sortFormat          = sortFormat,
-                    sortPieces          = sortPieces,
-                    sorting             = sorting,
-                    text                = text,
-                    tooltip             = tooltip,
-                    zoomOn              = zoomOn)
+    def getPlayerRoster(self):
+        '''Get Parent element'''
+        return self.getParent(PlayerRoster)
+
+
+#
+# EOF
+#
+# ====================================================================
+# From chessclock.py
+
+# ====================================================================
+class ChessClock(Element):
+    TAG=Element.MODULE+'chessclockcontrol.ChessClock'
+    def __init__(self,
+                 doc,
+                 node                   = None,
+                 icon                   = '',
+                 description            = '',
+                 side                   = '',
+                 tooltip                = 'Individual clock control',
+                 buttonText             = '',
+                 startHotkey            = '',
+                 stopHotkey             = '',
+                 tickingBackgroundColor = rgb(255,255,0),
+                 tickingFontColor       = rgb(0,0,0),
+                 tockingFontColor       = rgb(51,51,51)):
+        '''Individual clock for a side
+
+        When the clock is running, the background colour may be
+        changed, and the colour of the numbers alternate between
+        `tickingFontColor` and `tockingFontColor`.
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent element 
+        node : xml.dom.Element 
+            Read from this node
+        icon : str
+            File name of button icon
+        description : str
+            Note on this clock
+        side : str
+            Name of side this clock belongs to
+        tooltop : str
+            Hover help text
+        buttonText : str
+            Text on button
+        startHotkey : str (key code)
+            Key or command to start timer
+        stopHotkey : str (key code)
+            Key or command to stop timer
+        tickingBackgroundColor : str (color)
+            Background color of time display when clock is running
+        tickingFontColor : str (color)
+            First color of numbers in display when clock is running.
+        tockingFontColor : str (color)
+            Second color of numbers in display when clock is running.
+        '''
+        super(ChessClock,self).__init__(#ChessClock
+                 doc,
+                 self.TAG,
+                 node                   = node,
+                 icon                   = icon,
+                 description            = description,
+                 side                   = side,
+                 tooltip                = tooltip,
+                 buttonText             = buttonText,
+                 startHotkey            = startHotkey,
+                 stopHotkey             = stopHotkey,
+                 tickingBackgroundColor = tickingBackgroundColor,
+                 tickingFontColor       = tickingFontColor,
+                 tockingFontColor       = tockingFontColor)
+            
+    def getControl(self):
+        '''Get Parent element'''
+        return self.getParent(ChessClockControl)
+                 
+# ====================================================================
+class ChessClockControl(GameElement):
+    TAG=Element.MODULE+'ChessClockControl'
+    ALWAYS = 'Always'
+    AUTO   = 'Auto'
+    NEVER  = 'Never'
+    def __init__(self,
+                 doc,
+                 node              = None,
+                 name              = 'Chess clock',
+                 description       = '',
+                 buttonIcon        = 'chess_clock.png',
+                 buttonText        = '',
+                 buttonTooltip     = 'Show/stop/hide chess clocks',
+                 showHotkey        = key('U',ALT),
+                 pauseHotkey       = key('U',CTRL_SHIFT),
+                 nextHotkey        = key('U'),
+                 startOpponentKey  = '',
+                 showTenths        = AUTO,
+                 showSeconds       = AUTO,
+                 showHours         = AUTO,
+                 showDays          = AUTO,
+                 allowReset        = False,
+                 addClocks         = True):
+        '''A set of chess clocs
+
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        name : str
+            Name of clock control
+        description : str
+            Note on the chess clocks control
+        buttonIcon : str
+            Icon file name for button (chess_clock.png)
+        buttonText : str
+            Text on button 
+        buttonTooltip : str
+            Hower help
+        showHotkey : str (key code)
+            Show or hide interface hot key
+        nextHotkey : str (key code)
+            Start the next clock hot key
+        pauseHotkey : str (key code)
+            Pause all clocks hot key 
+        startOpponentKey : str (key code)
+            Start opponens clock 
+        showTenths : one of AUTO, ALWAYS, NEVER
+            Whether to show tenths of seconds
+        showSeconds : one of AUTO, ALWAYS, NEVER
+            Whether to show seconds in clock 
+        showHours : one of AUTO, ALWAYS, NEVER
+            Whether to show hours in clock
+        showDays : one of AUTO, ALWAYS, NEVER
+            Whether to show days in clock
+        allowReset : boolean
+            If true, allow manual reset of all clocks
+        '''
+        super(ChessClockControl,self).__init__(# ChessclockControl
+            doc,
+            self.TAG,
+            node              = node,
+            name              = name,
+            description       = description,
+            buttonIcon        = buttonIcon,
+            buttonText        = buttonText,
+            buttonTooltip     = buttonTooltip,
+            showHotkey        = showHotkey,
+            pauseHotkey       = pauseHotkey,
+            nextHotkey        = nextHotkey,
+            startOpponentKey  = startOpponentKey,
+            showTenths        = showTenths,
+            showSeconds       = showSeconds,
+            showHours         = showHours,
+            showDays          = showDays,
+            allowReset        = allowReset)
+        print(node,addClocks)
+        if node is not None or not addClocks:
+            return
+        
+        print('--- Will add clocks')
+        game   = self.getGame()
+        roster = game.getPlayerRoster()[0]
+        sides  = roster.getSides()
+        for side in sides:
+            name = side.getText()
+            self.addClock(side        = name,
+                          tooltip     = f'Clock for {name}',
+                          buttonText  = name,
+                          startHotkey = key('U'),
+                          stopHotkey  = key('U'))
     
-                  
+    def addClock(self,**kwargs):
+        '''Add a clock element to this
 
-# --------------------------------------------------------------------
-def add_map(root,doc,
-            mapName,
-            allowMultiple        = 'false',
-            backgroundcolor      = rgb(255,255,255),
-            buttonName           = '',
-            changeFormat         = '$message$',
-            color                = rgb(0,0,0),
-            createFormat         = '$pieceName$ created in $location$ *',
-            edgeHeight           = '0',
-            edgeWidth            = '0',
-            hideKey              = '',
-            hotkey               = key('M',ALT),
-            icon                 = '/images/map.gif',
-            launch               = 'false',
-            markMoved            = 'Always',
-            markUnmovedHotkey    = '',
-            markUnmovedIcon      = '/images/unmoved.gif',
-            markUnmovedReport    = '',
-            markUnmovedText      = '',
-            markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
-            moveKey              = '',
-            moveToFormat         = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
-            moveWithinFormat     = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
-            showKey              = '',
-            thickness            = '3',
-            cls                  = MODULE+'Map'):
-    return add_node(root,doc,cls,
-                    allowMultiple        = allowMultiple,
-                    backgroundcolor      = backgroundcolor,
-                    buttonName           = buttonName,
-                    changeFormat         = changeFormat,
-                    color                = color,
-                    createFormat         = createFormat,
-                    edgeHeight           = edgeHeight,
-                    edgeWidth            = edgeWidth,
-                    hideKey              = hideKey,
-                    hotkey               = hotkey,
-                    icon                 = icon,
-                    launch               = launch,
-                    mapName              = mapName,
-                    markMoved            = markMoved,
-                    markUnmovedHotkey    = markUnmovedHotkey,
-                    markUnmovedIcon      = markUnmovedIcon,
-                    markUnmovedReport    = markUnmovedReport,
-                    markUnmovedText      = markUnmovedText,
-                    markUnmovedTooltip   = markUnmovedTooltip,
-                    moveKey              = moveKey,
-                    moveToFormat         = moveToFormat,
-                    moveWithinFormat     = moveWithinFormat,
-                    showKey              = showKey,
-                    thickness            = thickness)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : AboutScreen
+            The added element
+        '''
+        return self.add(ChessClock,**kwargs)
+    def getClocks(self,asdict=True):
+        '''Return dictionary of clocs'''
+        return self.getElementsByKey(ChessClock,'side',asdict)
 
+
+#
+# EOF
+#
+# ====================================================================
+# From widget.py
+
 # --------------------------------------------------------------------
-def add_widgetmap(root,doc, mapName,**attr):
-    return add_map(root,doc,mapName,cls=WIDGET+'WidgetMap',**attr)
+class WidgetElement:
+    def __init__(self):
+        pass
 
+    def addTabs(self,**kwargs):
+        '''Add a `Tabs` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Tabs
+            The added element
+        '''
+        return self.add(TabWidget,**kwargs)
+    def addCombo(self,**kwargs):
+        '''Add a drop-down menu to this
+
+        Parameters
+        ----------
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Combo
+            The added element
+        '''
+        return self.add(ComboWidget,**kwargs)
+    def addPanel(self,**kwargs):
+        '''Add a `Panel` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Panel
+            The added element
+        '''
+        return self.add(PanelWidget,**kwargs)
+    def addList(self,**kwargs):
+        '''Add a `List` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : List
+            The added element
+        '''
+        return self.add(ListWidget,**kwargs)
+    def addMapWidget(self,**kwargs):
+        '''Add a `MapWidget` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MapWidget
+            The added element
+        '''
+        return self.add(MapWidget,**kwargs)
+    def addChart(self,**kwargs):
+        '''Add a `Chart` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chart
+            The added element
+        '''
+        return self.add(Chart,**kwargs)
+    def addPieceSlot(self,**kwargs):
+        '''Add a `PieceSlot` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PieceSlot
+            The added element
+        '''
+        return self.add(PieceSlot,**kwargs)
+    def addPiece(self,piece):
+        '''Add a `Piece` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Piece
+            The added element
+        '''
+        if not isinstance(piece,PieceSlot):
+            print(f'Trying to add {type(piece)} to ListWidget')
+            return None
+            
+        p = piece.clone(self)
+        return p
+    def getTabs(self,asdict=True):
+        '''Get all Tab element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Tab` elements.  If `False`, return a list of all Tab` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Tab` children
+        '''
+        return self.getElementsByKey(TabWidget,'entryName',asdict)
+    def getCombos(self,asdict=True):
+        '''Get all Combo element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Tab` elements.  If `False`, return a list of all Tab` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Tab` children
+        '''
+        return self.getElementsByKey(ComboWidget,'entryName',asdict)
+    def getLists(self,asdict=True):
+        '''Get all List element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `List` elements.  If `False`, return a list of all List` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `List` children
+        '''
+        return self.getElementsByKey(ListWidget,'entryName',asdict)
+    def getPanels(self,asdict=True):
+        '''Get all Panel element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Panel` elements.  If `False`, return a list of all Panel` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Panel` children
+        '''
+        return self.getElementsByKey(PanelWidget,'entryName',asdict)
+    def getMapWidgets(self,asdict=True):
+        '''Get all MapWidget element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `MapWidget` elements.  If `False`, return a list of all MapWidget` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `MapWidget` children
+        '''
+        return self.getElementsByKey(MapWidget,'entryName',asdict)
+    def getCharts(self,asdict=True):
+        '''Get all Chart element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Chart` elements.  If `False`, return a list of all Chart` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Chart` children
+        '''
+        return self.getElementsByKey(Chart,'chartName',asdict)
+    def getPieceSlots(self,asdict=True):
+        '''Get all PieceSlot element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `PieceSlot` elements.  If `False`, return a list of all PieceSlot` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `PieceSlot` children
+        '''
+        return self.getElementsByKey(PieceSlot,'entryName',asdict)
+
 # --------------------------------------------------------------------
-def add_boardpicker(root,doc,
-                    addColumnText        = 'Add column',
-                    addRowText           = 'Add row',
-                    boardPrompt          = 'Select board',
-                    slotHeight           = 125,
-                    slotScale            = 0.2,
-                    slotWidth            = 350,
-                    title                = 'Choose Boards'):
-    return add_node(root,doc,MAP+'BoardPicker',
-                    addColumnText        = addColumnText,
-                    addRowText           = addRowText,
-                    boardPrompt          = boardPrompt,
-                    slotHeight           = slotHeight,
-                    slotScale            = slotScale,
-                    slotWidth            = slotWidth,
-                    title                = title) 
+class PieceWindow(GameElement,WidgetElement):
+    TAG=Element.MODULE+'PieceWindow'
+    def __init__(self,elem,node=None,
+                 name         = '',
+                 defaultWidth = 0,
+                 hidden       = False,
+                 hotkey       = key('C',ALT),
+                 scale        = 1.,
+                 text         = '',
+                 tooltip      = 'Show/hide piece window',
+                 icon         = '/images/counter.gif'):
+        super(PieceWindow,self).__init__(elem,self.TAG,node=node,
+                                         name = name,
+                                         defaultWidth = defaultWidth,
+                                         hidden       = hidden,
+                                         hotkey       = hotkey,
+                                         scale        = scale,
+                                         text         = text,
+                                         tooltip      = tooltip,
+                                         icon         = icon)
 
 # --------------------------------------------------------------------
-def add_setup(root,picker,mapName,maxColumns,boardNames):
-    '''Add given boards in row-first up to maxColumns per row'''
-    s = add_node(root,picker,'setup')
-    col = 0
-    row = 0
-    lst = [f'{mapName}BoardPicker']
-    for bn in boardNames:
-        lst.extend([bn,str(col),str(row)])
-        col += 1
-        if col >= maxColumns:
-            col = 0
-            row += 1
-    txt = r'	'.join(lst)
-    add_text(root,s,txt)
-            
+class ComboWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'BoxWidget'
+    def __init__(self,elem,node=None,entryName='',width=0,height=0):
+        super(ComboWidget,self).__init__(elem,
+                                       self.TAG,
+                                       node = node,
+                                       entryName = entryName,
+                                       width     = width,
+                                       height    = height)
+        
 # --------------------------------------------------------------------
-def add_board(root,picker,name,
-              image      = '',
-              reversible = False,
-              color      = rgb(255,255,255),
-              width      = 0,
-              height     = 0):
-    return add_node(root,picker,PICKER+'Board',
-                    image      = image,
-                    name       = name,
-                    reversible = reversible,
-                    color      = color,
-                    width      = width,
-                    height     = height)
+class TabWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'TabWidget'
+    def __init__(self,elem,node=None,entryName=''):
+        super(TabWidget,self).__init__(elem,
+                                       self.TAG,
+                                       node      = node,
+                                       entryName = entryName)
 
 # --------------------------------------------------------------------
-def add_zoned(root,board):
-    return add_node(root,board,PICKER+'board.ZonedGrid')
+class ListWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'ListWidget'
+    def __init__(self,elem,node = None,
+                 entryName = '',
+                 height    = 0,
+                 width     = 300,
+                 scale     = 1.,
+                 divider   = 150):
+        super(ListWidget,self).__init__(elem,self.TAG,node=node,
+                                        entryName = entryName,
+                                        height    = height,
+                                        width     = width,
+                                        scale     = scale,
+                                        divider   = divider)
 
 # --------------------------------------------------------------------
-def add_zonehighlighter(root,zoned):
-    return add_node(root,zoned,PICKER+'board.mapgrid.ZonedGridHighlighter')
+class PanelWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'PanelWidget'
+    def __init__(self,elem,node=None,
+                 entryName = '',
+                 fixed     = False,
+                 nColumns  = 1,
+                 vert      = False):
+        super(PanelWidget,self).__init__(elem,self.TAG,node=node,
+                                         entryName = entryName,
+                                         fixed     = fixed,
+                                         nColumns  = nColumns,
+                                         vert      = vert)
 
 # --------------------------------------------------------------------
-def add_zone(root,zoned,
-             name              = "",
-             highlightProperty = "",
-             locationFormat    = "$gridLocation$",
-             path              = "0,0;976,0;976,976;0,976",
-             useHighlight      = False,
-             useParentGrid     = False):
-    return add_node(root,zoned,PICKER+'board.mapgrid.Zone',
-                    name              = name,
-                    highlightProperty = highlightProperty,
-                    locationFormat    = locationFormat,
-                    path              = path,
-                    useHighlight      = useHighlight,
-                    useParentGrid     = useParentGrid)
+class MapWidget(Element):
+    TAG=Element.WIDGET+'MapWidget'
+    def __init__(self,elem,node=None,entryName=''):
+        super(MapWidget,self).__init__(elem,self.TAG,
+                                       node      = node,
+                                       entryName = entryName)
 
+    def addWidgetMap(self,**kwargs):
+        '''Add a `WidgetMap` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : WidgetMap
+            The added element
+        '''
+        return self.add(WidgetMap,**kwargs)
+    def getWidgetMaps(self,asdict=True):
+        '''Get all WidgetMap element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `WidgetMap` elements.  If `False`, return a list of all WidgetMap` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `WidgetMap` children
+        '''
+        return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict)
+    
+#
+# EOF
+#
+# ====================================================================
+# From grid.py
+
 # --------------------------------------------------------------------
 HEX_WIDTH = 88.50779626676963
 HEX_HEIGHT = 102.2
-def add_hexgrid(root,zone,
-                color        = rgb(0,0,0),
-                cornersLegal = False,
-                dotsVisible  = False,
-                dx           = HEX_WIDTH,
-                dy           = HEX_HEIGHT,
-                edgesLegal   = False,
-                sideways     = False,
-                snapTo       = True,
-                visible      = True,
-                x0           = 0,
-                y0           = 32,
-                cls          = PICKER+'board.HexGrid'):
-    return add_node(root,zone,cls,
-                    color        = color,
-                    cornersLegal = cornersLegal,
-                    dotsVisible  = dotsVisible,
-                    dx           = dx,
-                    dy           = dy,
-                    edgesLegal   = edgesLegal,
-                    sideways     = sideways,
-                    snapTo       = snapTo,
-                    visible      = visible,
-                    x0           = x0,
-                    y0           = y0)
-
+RECT_WIDTH  = 80
+RECT_HEIGHT = 80
 # --------------------------------------------------------------------
-def add_hexnumbering(root,grid,
-                     color                = rgb(255,0,0),
-                     first                = 'H',
-                     fontSize             = 24,
-                     hDescend             = False,
-                     hDrawOff             = 0,
-                     hLeading             = 1,
-                     hOff                 = 0,
-                     hType                = 'A',
-                     locationFormat       = '$gridLocation$',
-                     rotateText           = 0,
-                     sep                  = '',
-                     stagger              = True,
-                     vDescend             = False,
-                     vDrawOff             = 32,
-                     vLeading             = 0,
-                     vOff                 = 0,
-                     vType                = 'N',
-                     visible              = True,
-                     cls                  = PICKER+'board.mapgrid.HexGridNumbering'):
-    return add_node(root,grid,cls,
-                    color                = color,
-                    first                = first,
-                    fontSize             = fontSize,
-                    hDescend             = hDescend,
-                    hDrawOff             = hDrawOff,
-                    hLeading             = hLeading,
-                    hOff                 = hOff,
-                    hType                = hType,
-                    locationFormat       = locationFormat,
-                    rotateText           = rotateText,
-                    sep                  = sep,
-                    stagger              = stagger,
-                    vDescend             = vDescend,
-                    vDrawOff             = vDrawOff,
-                    vLeading             = vLeading,
-                    vOff                 = vOff,
-                    vType                = vType,
-                    visible              = visible)
+class BaseGrid(Element):
+    def __init__(self,zone,tag,node=None,
+                 color        = rgb(0,0,0),
+                 cornersLegal = False,
+                 dotsVisible  = False,
+                 dx           = HEX_WIDTH,  # Meaning seems reversed!
+                 dy           = HEX_HEIGHT,
+                 edgesLegal   = False,
+                 sideways     = False,
+                 snapTo       = True,
+                 visible      = True,
+                 x0           = 0,
+                 y0           = 32):
+        super(BaseGrid,self).__init__(zone,tag,node=node,
+                                      color        = color,
+                                      cornersLegal = cornersLegal,
+                                      dotsVisible  = dotsVisible,
+                                      dx           = dx,
+                                      dy           = dy,
+                                      edgesLegal   = edgesLegal,
+                                      sideways     = sideways,
+                                      snapTo       = snapTo,
+                                      visible      = visible,
+                                      x0           = x0,
+                                      y0           = y0)
+    def getZone(self):
+        z = self.getParent(Zone)
+        return z
+    def getZonedGrid(self):
+        z = self.getZone()
+        if z is not None:
+            return z.getZonedGrid()
+        return None
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return self.getParent(Board)
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        b = self.getPicker()
+        if b is not None:
+            return b.getMap()
+        return None
+    def getNumbering(self):
+        pass 
+    def getLocation(self,loc):
+        numbering = self.getNumbering()
+        if numbering is None or len(numbering) < 1:
+            return None
 
+        return numbering[0].getLocation(loc)
+    
 # --------------------------------------------------------------------
-RECT_WIDTH  = 80
-RECT_HEIGHT = 80
-def add_squaregrid(root,zone,
-                   dx           = RECT_WIDTH,
-                   dy           = RECT_HEIGHT,
-                   edgesLegal   = False,
-                   x0           = 0,
-                   y0           = int(0.4*RECT_HEIGHT),
-                   **attr):
-    return add_hexgrid(root,zone,cls=PICKER+'board.SquareGrid',
-                       dx           = dx,
-                       dy           = dy,
-                       edgesLegal   = edgesLegal,
-                       x0           = x0,
-                       y0           = y0,
-                       **attr)
+class BaseNumbering(Element):
+    def __init__(self,grid,tag,node=None,
+                 color                = rgb(255,0,0),
+                 first                = 'H',
+                 fontSize             = 24,
+                 hDescend             = False,
+                 hDrawOff             = 0,
+                 hLeading             = 1,
+                 hOff                 = 0,
+                 hType                = 'A',
+                 locationFormat       = '$gridLocation$',
+                 rotateText           = 0,
+                 sep                  = '',
+                 stagger              = True,
+                 vDescend             = False,
+                 vDrawOff             = 32,
+                 vLeading             = 0,
+                 vOff                 = 0,
+                 vType                = 'N',
+                 visible              = True):
+        super(BaseNumbering,self).__init__(grid,tag,node=node,
+                                           color           = color,
+                                           first           = first,
+                                           fontSize        = fontSize,
+                                           hDescend        = hDescend,
+                                           hDrawOff        = hDrawOff,
+                                           hLeading        = hLeading,
+                                           hOff            = hOff,
+                                           hType           = hType,
+                                           locationFormat  = locationFormat,
+                                           rotateText      = rotateText,
+                                           sep             = sep,
+                                           stagger         = stagger,
+                                           vDescend        = vDescend,
+                                           vDrawOff        = vDrawOff,
+                                           vLeading        = vLeading,
+                                           vOff            = vOff,
+                                           vType           = vType,
+                                           visible         = visible)
+    def getGrid(self): return getParent(BaseGrid)
 
+    def _getMatcher(self,tpe,leading):
+        if tpe == 'A':
+            return \
+                '-?(?:A+|B+|C+|D+|E+|F+|G+|H+|I+|'  + \
+                'J+|K+|L+|M+|N+|O+|P+|Q+|R+|S+|T+|' + \
+                'U+|V+|W+|X+|Y+|Z+)'
+
+        return f'-?[0-9]{{{int(leading)+1},}}'
+
+    def _getIndex(self,name,tpe):
+        if tpe == 'A':
+            negative = name.startswith('-')
+            if negative:
+                name = name[1:]
+
+            value = 0
+            for num,let in enumerate(name):
+                if not let.isupper():
+                    continue
+                if num < len(name) - 1:
+                    value += 26
+                else:
+                    value += ord(let)-ord('A')
+
+            if negative:
+                value *= -1
+
+            return value
+
+        return int(name)
+
+    def _getCenter(self,col,row):
+        '''Convert col and row index to picture coordinates'''
+        print('Dummy GetCenter')
+        pass
+    
+    def getLocation(self,loc):
+        '''Get picture coordinates from grid location'''
+        from re import match
+
+        first   = self['first']
+        vType   = self['vType']
+        hType   = self['hType']
+        vOff    = int(self['vOff'])
+        hOff    = int(self['hOff'])
+        colPat  = self._getMatcher(hType,self['hLeading'])
+        rowPat  = self._getMatcher(vType,self['vLeading'])
+        patts   = ((colPat,rowPat) if first == 'H' else (rowPat,colPat))
+        colGrp  = 1 if first == 'H' else 2
+        rowGrp  = 2 if first == 'H' else 1
+        patt    = ''.join([f'({p})' for p in patts])
+        matched = match(patt,loc)
+        if not matched:
+            return None
+
+        rowStr  = matched[rowGrp]
+        colStr  = matched[colGrp]
+        rowNum  = self._getIndex(rowStr,vType)
+        colNum  = self._getIndex(colStr,hType)
+
+        return self._getCenter(colNum-hOff, rowNum-vOff);
+       
+    
 # --------------------------------------------------------------------
-def add_squarenumbering(root,grid,
-                        hType                = 'N',
-                        **attr):
-    return add_hexnumbering(root,grid,
-                            cls=PICKER+'board.mapgrid.SquareGridNumbering',
-                            hType                = hType,
-                            **attr)
+class HexGrid(BaseGrid):
+    TAG = Element.BOARD+'HexGrid'
+    def __init__(self,zone,node=None,**kwargs):
+        super(HexGrid,self).__init__(zone,self.TAG,node=node,**kwargs)
 
+    def addNumbering(self,**kwargs):
+        '''Add a `Numbering` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Numbering
+            The added element
+        '''
+        return self.add(HexNumbering,**kwargs)
+    def getNumbering(self):
+        return self.getAllElements(HexNumbering)
+    def getDeltaX(self):
+        return float(self['dx'])
+    def getDeltaY(self):
+        return float(self['dy'])
+    def getXOffset(self):
+        return int(self['x0'])
+    def getYOffset(self):
+        return int(self['y0'])
+    def getMaxRows(self):
+        from math import floor
+        height    = self.getZone().getHeight()
+        return floor(height / self.getDeltaX() + .5)
+    def getMaxCols(self):
+        from math import floor
+        width    = self.getZone().getWidth()
+        return floor(width / self.getDeltaY()  + .5)
+
 # --------------------------------------------------------------------
-def add_regiongrid(root,zone,snapto=True,fontsize=9,visible=True):
-    # print(f'Make region grid visible: {visible}')
-    return add_node(root,zone,PICKER+'board.RegionGrid',
-                    fontsize = fontsize,
-                    snapto   = snapto,
-                    visble   = visible)
+class SquareGrid(BaseGrid):
+    TAG = Element.BOARD+'SquareGrid'
+    def __init__(self,zone,node=None,
+                 dx           = RECT_WIDTH,
+                 dy           = RECT_HEIGHT,
+                 edgesLegal   = False,
+                 x0           = 0,
+                 y0           = int(0.4*RECT_HEIGHT),
+                 **kwargs):
+        super(SquareGrid,self).__init__(zone,self.TAG,node=node,
+                                        dx         = dx,
+                                        dy         = dy,
+                                        edgesLegal = edgesLegal,
+                                        x0         = 0,
+                                        y0         = int(0.4*RECT_HEIGHT),
+                                        **kwargs)
+    def addNumbering(self,**kwargs):
+        '''Add a `Numbering` element to this
 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Numbering
+            The added element
+        '''
+        return self.add(SquareNumbering,**kwargs)
+    def getNumbering(self):
+        return self.getAllElements(SquareNumbering)
+    def getDeltaX(self):
+        return float(self['dx'])
+    def getDeltaY(self):
+        return float(self['dy'])
+    def getXOffset(self):
+        return int(self['x0'])
+    def getYOffset(self):
+        return int(self['y0'])
+    def getMaxRows(self):
+        from math import floor
+        height    = self.getZone().getHeight()
+        return floor(height / self.getDeltaY() + .5)
+    def getMaxCols(self):
+        from math import floor
+        width    = self.getZone().getWidth()
+        return floor(width / self.getDeltaX()  + .5)
+
 # --------------------------------------------------------------------
-def get_region_piece(root,grid,name,alsoPiece=True,piece=None,verbose=False):
-    if not alsoPiece:
-        return None, None
+class HexNumbering(BaseNumbering):
+    TAG = Element.BOARD+'mapgrid.HexGridNumbering'
+    def __init__(self,grid,node=None,**kwargs):
+        super(HexNumbering,self).__init__(grid,self.TAG,node=node,**kwargs)
+    def getGrid(self):
+        g = self.getParent(HexGrid)
+        return g
 
-    # Find the parent map element by stepping up the tree 
-    from functools import reduce
+    def _getCenter(self,col,row):
+        '''Convert col and row index to picture coordinates'''
+        from math import floor
+        
+        stagger  = self['stagger'] == 'true'
+        sideways = self.getGrid()['sideways'] == 'true'
+        hDesc    = self['hDescend'] == 'true'
+        vDesc    = self['vDescend'] == 'true'
+        xOff     = self.getGrid().getXOffset()
+        yOff     = self.getGrid().getYOffset()
+        hexW     = self.getGrid().getDeltaX()
+        hexH     = self.getGrid().getDeltaY()
+        zxOff    = self.getGrid().getZone().getXOffset()
+        zyOff    = self.getGrid().getZone().getYOffset()
+        maxRows  = self.getGrid().getMaxRows()
+        maxCols  = self.getGrid().getMaxCols()
+        # print(f'  Col:         {col}')
+        # print(f'  Row:         {row}')
+        # print(f'  Stagger:     {stagger}')
+        # print(f'  Sideways:    {sideways}')
+        # print(f'  hDesc:       {hDesc}')
+        # print(f'  vDesc:       {vDesc}')
+        # print(f'  maxRows:     {maxRows}')
+        # print(f'  maxCols:     {maxCols}')
 
-    map = reduce(lambda nn, _: None if nn is None else nn.parentNode,
-                 range(5), grid)
-    if map is None or \
-       map.tagName not in [MODULE+'Map',WIDGET+'WidgetMap']:
-        if map is None:
-            print(f'      = Map not found from grid')
-        return None, None
+        if sideways:
+            maxRows, maxCols = maxCols, maxRows
+            
+        if stagger:
+            if sideways:
+                if col % 2 != 0:
+                    row += 1 if hDesc else -1
+            else:
+                if col % 2 != 0:
+                    row += 1 if vDesc else -1
 
-    if piece is not None:
-        print(f'      Using supplied piece')
-        return piece, map
+        if hDesc:
+            col = maxCols - col
+        if vDesc:
+            row = maxRows - row
 
-    # Find the piece by searching from the top of the tree
-    piece = get_pieces(root,root).get(name,None)
-    # if piece is None:
-    #     print(list(get_pieces(root,root).keys()))
-    if verbose:
-        print(f'       Possible piece for region {name}: {piece}')
-    return piece, map
+        x = col * hexW + xOff
+        y = row * hexH + yOff + (hexH/2 if col % 2 != 0 else 0)
 
+        x = int(floor(x + .5))
+        y = int(floor(y + .5))
+        if sideways:
+            # print(f'Swap coordinates because {sideways}')
+            x, y = y, x
+
+        return x,y
+
 # --------------------------------------------------------------------
-def check_region_name(grid,name):
-    post = ''
-    targ = name
-    same = 0
-    regs = list(get_regions(grid).keys())
-    poss = len([e for e in regs if e.startswith(name)])
-    if poss == 0:
-        return name
+class SquareNumbering(BaseNumbering):
+    TAG = Element.BOARD+'mapgrid.SquareGridNumbering'
+    def __init__(self,grid,node=None,hType='N',**kwargs):
+        super(SquareNumbering,self).__init__(grid,self.TAG,node=node,
+                                             hType=hType,**kwargs)
+    def getGrid(self):
+        return self.getParent(SquareGrid)
 
-    return name + f' {poss}'
-    # Loop over keys, sorted
-    # regs.sort()
-    # for r in regs:
-    #     if targ == r:
-    #         same += 1
-    #         targ =  name + f' {same:02d}'
-    #         print(f'        Found region named {r} -> {targ}')
+    def getCenter(self,col,row):
+        hDesc    = self['hDescend'] == 'true'
+        vDesc    = self['vDescend'] == 'true'
+        xOff     = self.getGrid().getXOffset()
+        yOff     = self.getGrid().getYOffset()
+        squareW  = self.getGrid().getDeltaX()
+        squareH  = self.getGrid().getDeltaY()
+        maxRows  = self.getGrid().getMaxRows()
+        maxCols  = self.getGrid().getMaxCols()
 
-    return targ
+        if vDesc:  row = maxRows - row
+        if hDesc:  col = maxCols - col
+
+        x = col * squareW + xOff
+        y = row * squareH + yOff
+
+        return x,y
+        
     
 # --------------------------------------------------------------------
-def add_region(root,grid,name,originx,originy,gpid,
-               alsoPiece=True,piece=None,verbose=True):
-    # First thing is to check if we have a piece
-    piece, map = get_region_piece(root,grid,name,alsoPiece,piece,verbose)
-    if verbose:
-        print(f'       Region {name} -> piece {piece}')
-    
-            
-    nam = check_region_name(grid,name)
-    if verbose:
-        print(f'       Region {name} -> real name {nam}')
-    n   = add_node(root,grid,PICKER+'board.Region',
-                   name     = nam,
-                   originx  = originx,
-                   originy  = originy)
-    if piece is None:
-        return n, gpid
+class RegionGrid(Element):
+    TAG = Element.BOARD+'RegionGrid'
+    def __init__(self,zone,node=None,snapto=True,fontsize=9,visible=True):
+        super(RegionGrid,self).__init__(zone,self.TAG,node=node,
+                                        snapto   = snapto,
+                                        fontsize = fontsize,
+                                        visible  = visible)
 
-    if verbose:
-        print(f'       Add piece {name} to region')
-    _, gpid = add_atstart(root,map,name,piece,
-                          location        = nam,
-                          useGridLocation = True,
-                          owningBoard     = map.getAttribute('mapName'),
-                          x               = 0,
-                          y               = 0,
-                          gpid            = gpid)
-    return n, gpid
-    
+    def getZone(self):
+        return self.getParent(Zone)
+    def getZoneGrid(self):
+        z = self.getZone()
+        if z is not None:
+            return z.getBoard()
+        return None
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return self.getParent(Board)
+    def getMap(self):
+        b = self.getBoard()
+        if b is not None:
+            return b.getMap()
+        return None
+    def getRegions(self):
+        return self.getElementsByKey(Region,'name')    
+    def checkName(self,name):
+        '''Get unique name'''
+        poss = len([e for e in self.getRegions()
+                    if e == name or e.startswith(name+'_')])
+        if poss == 0:
+            return name
 
-# --------------------------------------------------------------------
-def add_stacking(root,map,
-                 bottom               = '40,0',
-                 disabled             = False,
-                 down                 = '37,0',
-                 exSepX               = 6,
-                 exSepY               = 18,
-                 top                  = '38,0',
-                 unexSepX             = 8,
-                 unexSepY             = 16,
-                 up                   = '39,0'):
-    return add_node(root,map,MAP+'StackMetrics',
-                    bottom               = bottom,
-                    disabled             = disabled,
-                    down                 = down,
-                    exSepX               = exSepX,
-                    exSepY               = exSepY,
-                    top                  = top,
-                    unexSepX             = unexSepX,
-                    unexSepY             = unexSepY,
-                    up                   = up)
+        return name + f'_{poss}'
+    def addRegion(self,**kwargs):
+        '''Add a `Region` element to this
 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Region
+            The added element
+        '''
+        return self.add(Region,**kwargs)
+    def getLocation(self,loc):
+        for r in self.getRegions().values():
+            if loc == r['name']:
+                return int(r['originx']),int(r['originy'])
+
+        return None
+        
 # --------------------------------------------------------------------
-def add_camera(root,map,
-               buttonText           = '',
-               canDisable           = False,
-               hotkey               = '',
-               icon                 = '/images/camera.gif',
-               propertyGate         = '',
-               tooltip              = 'Save map as PNG image'):
-    return add_node(root,map,MAP+'ImageSaver',
-               buttonText           = buttonText,
-               canDisable           = canDisable,
-               hotkey               = hotkey,
-               icon                 = icon,
-               propertyGate         = propertyGate,
-               tooltip              = tooltip)
+class Region(Element):
+    TAG = Element.BOARD+'Region'
+    def __init__(self,grid,node=None,
+                 name      = '',
+                 originx   = 0,
+                 originy   = 0,
+                 alsoPiece = True,
+                 piece     = None,
+                 prefix    = ''):
+        fullName = name + ("@"+prefix if len(prefix) else "")
+        realName = grid.checkName(fullName) if node is None else fullName
+        super(Region,self).__init__(grid,
+                                    self.TAG,
+                                    node    = node,
+                                    name    = realName,
+                                    originx = originx,
+                                    originy = originy)
 
+        if node is None and alsoPiece:
+            m = self.getMap()
+            b = self.getBoard()
+            if m is not None and b is not None:
+                if piece is None:
+                    g      = m.getGame()
+                    pieces = g.getSpecificPieces(name,asdict=False)
+                    piece  = pieces[0] if len(pieces) > 0 else None
+             
+                if piece is not None:
+                    # bname = m['mapName']
+                    bname = b['name']
+                    #print(f'Adding at-start name={name} location={realName} '
+                    #      f'owning board={bname}')
+                    a = m.addAtStart(name            = name,
+                                     location        = realName,
+                                     useGridLocation = True,
+                                     owningBoard     = bname,
+                                     x               = 0,
+                                     y               = 0)
+                    p = a.addPiece(piece)
+                    if p is None:
+                        print(f'EEE Failed to add piece {name} ({piece}) to add-start {a}')
+                    #if p is not None:
+                    #    print(f'Added piece {name} in region')
+                #else:
+                #    print(f'Could not find piece {name}')
+            
+    def getGrid(self):
+        return self.getParent(RegionGrid)
+    def getZone(self):
+        g = self.getGrid()
+        if g is not None:
+            return g.getZone()
+        return None
+    def getZonedGrid(self):
+        z = self.getZone()
+        if z is not None:
+            return z.getZonedGrid()
+        return None
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return self.getParent(Board)
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        b = self.getPicker()
+        if b is not None:
+            return b.getMap()
+        return None
+
+#
+# EOF
+#
+# ====================================================================
+# From zone.py
+
 # --------------------------------------------------------------------
-def add_forwardtochatter(root,map):
-    return add_node(root,map,MAP+'ForwardToChatter')
+class ZonedGrid(Element):
+    TAG=Element.BOARD+'ZonedGrid'
+    def __init__(self,board,node=None):
+        super(ZonedGrid,self).__init__(board,self.TAG,node=node)
 
+    def getBoard(self):
+        b = self.getParent(Board)
+        # print(f'Get Board of Zoned: {b}')        
+        return b
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        z = self.getPicker()
+        if z is not None:
+            return z.getMap()
+        return None
+    def addHighlighter(self,**kwargs):
+        '''Add a `Highlighter` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Highlighter
+            The added element
+        '''
+        return self.add(ZonedGridHighlighter,**kwargs)
+    def getHighlighters(self,single=True):
+        '''Get all or a sole `ZonedGridHighlighter` element(s) from this
+        
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Highligter` child, otherwise fail.
+            If `False` return all `Highligter` children in this element
+        Returns
+        -------
+        children : list
+            List of `Highligter` children (even if `single=True`)
+        '''
+        return self.getAllElements(ZonedGridHighliger,single=single)
+    def addZone(self,**kwargs):
+        '''Add a `Zone` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Zone
+            The added element
+        '''
+        return self.add(Zone,**kwargs)
+    def getZones(self,asdict=True):
+        '''Get all Zone element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Zone` elements.  If `False`, return a list of all Zone` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Zone` children
+        '''
+        return self.getElementsByKey(Zone,'name',asdict=asdict)
+
 # --------------------------------------------------------------------
-def add_menudisplayer(root,map):
-    return add_node(root,map,MAP+'MenuDisplayer')
+class ZonedGridHighlighter(Element):
+    TAG=Element.BOARD+'mapgrid.ZonedGridHighlighter'
+    def __init__(self,zoned,node=None):
+        super(ZonedGridHighlighter,self).__init__(zoned,self.TAG,node=node)
+    def getZonedGrid(self): return self.getParent(ZonedGrid)
 
+
 # --------------------------------------------------------------------
-def add_mapcenterer(root,map):
-    return add_node(root,map,MAP+'MapCenterer')
+class Zone(Element):
+    TAG = Element.BOARD+'mapgrid.Zone'
+    def __init__(self,zoned,node=None,
+                 name              = "",
+                 highlightProperty = "",
+                 locationFormat    = "$gridLocation$",
+                 path              = "0,0;976,0;976,976;0,976",
+                 useHighlight      = False,
+                 useParentGrid     = False):
+        super(Zone,self).\
+            __init__(zoned,self.TAG,node=node,
+                     name              = name,
+                     highlightProperty = highlightProperty,
+                     locationFormat    = locationFormat,
+                     path              = path,
+                     useHighlight      = useHighlight,
+                     useParentGrid     = useParentGrid)
 
+    def getZonedGrid(self):
+        z = self.getParent(ZonedGrid)
+        # print(f'Get Zoned of Zone {self["name"]}: {z}')        
+        return z
+    
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return None
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        z = self.getPicker()
+        if z is not None:
+            return z.getMap()
+        return None    
+    def addHexGrid(self,**kwargs):
+        '''Add a `HexGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HexGrid
+            The added element
+        '''
+        return self.add(HexGrid,**kwargs)
+    def addSquareGrid(self,**kwargs):
+        '''Add a `SquareGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : SquareGrid
+            The added element
+        '''
+        return self.add(SquareGrid,**kwargs)
+    def addRegionGrid(self,**kwargs):
+        '''Add a `RegionGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : RegionGrid
+            The added element
+        '''
+        return self.add(RegionGrid,**kwargs)
+    def getHexGrids(self,single=True):
+        '''Get all or a sole `HexGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `HexGrid` child, otherwise fail.
+            If `False` return all `HexGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `HexGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(HexGrid,single=single)
+    def getSquareGrids(self,single=True):
+        '''Get all or a sole `SquareGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `SquareGrid` child, otherwise fail.
+            If `False` return all `SquareGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `SquareGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(SquareGrid,single=single)
+    def getRegionGrids(self,single=True):
+        '''Get all or a sole `RegionGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `RegionGrid` child, otherwise fail.
+            If `False` return all `RegionGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `RegionGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(RegionGrid,single=single)
+    def getGrids(self,single=True):
+        '''Get all or a sole `Grid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Grid` child, otherwise fail.
+            If `False` return all `Grid` children in this element
+        Returns
+        -------
+        children : list
+            List of `Grid` children (even if `single=True`)
+        '''
+        g = self.getHexGrids(single=single)
+        if g is not None: return g
+
+        g = self.getSquareGrids(single=single)
+        if g is not None: return g
+
+        g = self.getRegionGrids(single=single)
+        if g is not None: return g
+
+        return g
+    
+    def getPath(self):
+        p  = self['path'].split(';')
+        r  = []
+        for pp in p:
+            c = pp.split(',')
+            r.append([int(c[0]),int(c[1])])
+        return r
+    def getBB(self):
+        from functools import reduce
+        path = self.getPath()
+        llx  = reduce(lambda old,point:min(point[0],old),path,100000000000)
+        lly  = reduce(lambda old,point:min(point[1],old),path,100000000000)
+        urx  = reduce(lambda old,point:max(point[0],old),path,-1)
+        ury  = reduce(lambda old,point:max(point[1],old),path,-1)
+        return llx,lly,urx,ury
+    def getWidth(self):
+        llx,_,urx,_ = self.getBB()
+        return urx-llx
+    def getHeight(self):
+        _,lly,_,ury = self.getBB()
+        return ury-lly
+    def getXOffset(self):
+        return self.getBB()[0]
+    def getYOffset(self):
+        return self.getBB()[1]
+
+#
+# EOF
+#
+# ====================================================================
+# From board.py
+
 # --------------------------------------------------------------------
-def add_stackexpander(root,map):
-    return add_node(root,map,MAP+'StackExpander')
+class BoardPicker(MapElement):
+    TAG = Element.MAP+'BoardPicker'
+    def __init__(self,doc,node=None,
+                 addColumnText        = 'Add column',
+                 addRowText           = 'Add row',
+                 boardPrompt          = 'Select board',
+                 slotHeight           = 125,
+                 slotScale            = 0.2,
+                 slotWidth            = 350,
+                 title                = 'Choose Boards'):
+        super(BoardPicker,self).__init__(doc,self.TAG,node=node,
+                                         addColumnText        = addColumnText,
+                                         addRowText           = addRowText,
+                                         boardPrompt          = boardPrompt,
+                                         slotHeight           = slotHeight,
+                                         slotScale            = slotScale,
+                                         slotWidth            = slotWidth,
+                                         title                = title)
 
+    def addSetup(self,**kwargs):
+        '''Add a `Setup` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Setup
+            The added element
+        '''
+        if 'mapName' not in kwargs:
+            m = self.getMap()
+            kwargs['mapName'] = m.getAttribute('mapName')
+            
+        return self.add(Setup,**kwargs)
+    def getSetups(self,single=False):
+        '''Get all or a sole `Setup` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Setup` child, otherwise fail.
+            If `False` return all `Setup` children in this element
+        Returns
+        -------
+        children : list
+            List of `Setup` children (even if `single=True`)
+        '''
+        return self.getAllElements(Setup,single=single)
+    def addBoard(self,**kwargs):
+        '''Add a `Board` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Board
+            The added element
+        '''
+        return self.add(Board,**kwargs)
+    def getBoards(self,asdict=True):
+        '''Get all Board element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board` elements.  If `False`, return a list of all Board` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+        '''
+        return self.getElementsByKey(Board,'name',asdict=asdict)
+    def encode(self):
+        setups = self.getSetups()
+        if setups is not None and len(setups)>0:
+            return [setups[0]._node.childNodes[0].nodeValue]
+        
+        ret    = []
+        for bn in self.getBoards().keys():
+            ret.append(bn+'BoardPicker\t'+bn+'\t0\t0')
+
+        return ret
+
 # --------------------------------------------------------------------
-def add_piecemover(root,map):
-    return add_node(root,map,MAP+'PieceMover')
+class Setup(Element):
+    TAG = 'setup'
+    def __init__(self,picker,node=None,
+                 mapName = '',
+                 maxColumns = 1,
+                 boardNames = []):
+        super(Setup,self).__init__(picker,self.TAG,node=node)
+        col = 0
+        row = 0
+        lst = [f'{mapName}BoardPicker']
+        for bn in boardNames:
+            lst.extend([bn,str(col),str(row)])
+            col += 1
+            if col >= maxColumns:
+                col = 0
+                row += 1
+                
+        txt = r'	'.join(lst)
+        self.addText(txt)
 
+    def getPicker(self): return self.getParent(BoardPicker)
+            
 # --------------------------------------------------------------------
-def add_selectionhighlighters(root,map):
-    return add_node(root,map,MAP+'SelectionHighlighters')
+class Board(Element):
+    TAG = Element.PICKER+'Board'
+    def __init__(self,picker,node=None,
+                 name       = '',
+                 image      = '',
+                 reversible = False,
+                 color      = rgb(255,255,255),
+                 width      = 0,
+                 height     = 0):
+        super(Board,self).__init__(picker,self.TAG,node=node,
+                                   image      = image,
+                                   name       = name,
+                                   reversible = reversible,
+                                   color      = color,
+                                   width      = width,
+                                   height     = height)
 
+    def getPicker(self): return self.getParent(BoardPicker)
+    def getMap(self):
+        z = self.getPicker()
+        if z is not None:
+            return z.getMap()
+        return None
+    def addZonedGrid(self,**kwargs):
+        '''Add a `ZonedGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ZonedGrid
+            The added element
+        '''
+        return self.add(ZonedGrid,**kwargs)
+    def getZonedGrids(self,single=True):
+        '''Get all or a sole `ZonedGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `ZonedGrid` child, otherwise fail.
+            If `False` return all `ZonedGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `ZonedGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(ZonedGrid,single=single)
+    def getZones(self,asdict=True):
+        '''Get all Zone element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Zone` elements.  If `False`, return a list of all Zone` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Zone` children
+        '''
+        zoned = self.getZonedGrids(single=True)
+        if zoned is None: return None
+
+        return zoned[0].getZones(asdict=asdict)
+
+    def getWidth(self):
+        # print(f'Getting width of {self}: {self["width"]}')
+        if 'width' in self and int(self['width']) != 0:
+            return int(self['width'])
+        return 0
+
+    def getHeight(self):
+        # print(f'Getting height of {self}: {self["height"]}')
+        if 'height' in self and int(self['height']) != 0:
+            return int(self['height'])
+        return 0
+
+
+#
+# EOF
+#
+# ====================================================================
+# From map.py
+
 # --------------------------------------------------------------------
-def add_keybufferer(root,map):
-    return add_node(root,map,MAP+'KeyBufferer')
+class BaseMap(Element):
+    def __init__(self,doc,tag,node=None,
+                 mapName              = '',
+                 allowMultiple        = 'false',
+                 backgroundcolor      = rgb(255,255,255),
+                 buttonName           = '',
+                 changeFormat         = '$message$',
+                 color                = rgb(0,0,0), # Selected pieces
+                 createFormat         = '$pieceName$ created in $location$ *',
+                 edgeHeight           = '0',
+                 edgeWidth            = '0',
+                 hideKey              = '',
+                 hotkey               = key('M',ALT),
+                 icon                 = '/images/map.gif',
+                 launch               = 'false',
+                 markMoved            = 'Always',
+                 markUnmovedHotkey    = '',
+                 markUnmovedIcon      = '/images/unmoved.gif',
+                 markUnmovedReport    = '',
+                 markUnmovedText      = '',
+                 markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
+                 moveKey              = '',
+                 moveToFormat         = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 moveWithinFormat     = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 showKey              = '',
+                 thickness            = '3'):
+        super(BaseMap,self).__init__(doc,tag,node=node,
+                                     allowMultiple        = allowMultiple,
+                                     backgroundcolor      = backgroundcolor,
+                                     buttonName           = buttonName,
+                                     changeFormat         = changeFormat,
+                                     color                = color,
+                                     createFormat         = createFormat,
+                                     edgeHeight           = edgeHeight,
+                                     edgeWidth            = edgeWidth,
+                                     hideKey              = hideKey,
+                                     hotkey               = hotkey,
+                                     icon                 = icon,
+                                     launch               = launch,
+                                     mapName              = mapName,
+                                     markMoved            = markMoved,
+                                     markUnmovedHotkey    = markUnmovedHotkey,
+                                     markUnmovedIcon      = markUnmovedIcon,
+                                     markUnmovedReport    = markUnmovedReport,
+                                     markUnmovedText      = markUnmovedText,
+                                     markUnmovedTooltip   = markUnmovedTooltip,
+                                     moveKey              = moveKey,
+                                     moveToFormat         = moveToFormat,
+                                     moveWithinFormat     = moveWithinFormat,
+                                     showKey              = showKey,
+                                     thickness            = thickness)
 
+    def getGame(self):
+        '''Get the game'''
+        return self.getParentOfClass([Game])
+    def addPicker(self,**kwargs):
+        '''Add a `Picker` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Picker
+            The added element
+        '''
+        return self.add(BoardPicker,**kwargs)
+    def getBoardPicker(self,single=True):
+        '''Get all or a sole `BoardPicker` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `BoardPicker` child, otherwise fail.
+            If `False` return all `BoardPicker` children in this element
+        Returns
+        -------
+        children : list
+            List of `BoardPicker` children (even if `single=True`)
+        '''
+        return self.getAllElements(BoardPicker,single)
+    def getPicker(self,single=True):
+        '''Get all or a sole `BoardPicker` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `BoardPicker` child, otherwise fail.
+            If `False` return all `BoardPicker` children in this element
+        Returns
+        -------
+        children : list
+            List of `BoardPicker` children (even if `single=True`)
+        '''
+        return self.getAllElements(BoardPicker,single)
+    def getStackMetrics(self,single=True):
+        '''Get all or a sole `StackMetric` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `StackMetric` child, otherwise fail.
+            If `False` return all `StackMetric` children in this element
+        Returns
+        -------
+        children : list
+            List of `StackMetric` children (even if `single=True`)
+        '''
+        return self.getAllElements(StackMetrics,single)
+    def getImageSaver(self,single=True):
+        '''Get all or a sole `ImageSaver` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `ImageSaver` child, otherwise fail.
+            If `False` return all `ImageSaver` children in this element
+        Returns
+        -------
+        children : list
+            List of `ImageSaver` children (even if `single=True`)
+        '''
+        return self.getAllElements(ImageSaver,single)
+    def getTextSaver(self,single=True):
+        '''Get all or a sole `TextSaver` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `TextSaver` child, otherwise fail.
+            If `False` return all `TextSaver` children in this element
+        Returns
+        -------
+        children : list
+            List of `TextSaver` children (even if `single=True`)
+        '''
+        return self.getAllElements(TextSaver,single)
+    def getForwardToChatter(self,single=True):
+        '''Get all or a sole `ForwardToChatter` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `ForwardToChatter` child, otherwise fail.
+            If `False` return all `ForwardToChatter` children in this element
+        Returns
+        -------
+        children : list
+            List of `ForwardToChatter` children (even if `single=True`)
+        '''
+        return self.getAllElements(ForwardToChatter,single)
+    def getMenuDisplayer(self,single=True):
+        '''Get all or a sole `MenuDi` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `MenuDi` child, otherwise fail.
+            If `False` return all `MenuDi` children in this element
+        Returns
+        -------
+        children : list
+            List of `MenuDi` children (even if `single=True`)
+        '''
+        return self.getAllElements(MenuDisplayer,single)
+    def getMapCenterer(self,single=True):
+        '''Get all or a sole `MapCenterer` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `MapCenterer` child, otherwise fail.
+            If `False` return all `MapCenterer` children in this element
+        Returns
+        -------
+        children : list
+            List of `MapCenterer` children (even if `single=True`)
+        '''
+        return self.getAllElements(MapCenterer,single)
+    def getStackExpander(self,single=True):
+        '''Get all or a sole `StackExpander` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `StackExpander` child, otherwise fail.
+            If `False` return all `StackExpander` children in this element
+        Returns
+        -------
+        children : list
+            List of `StackExpander` children (even if `single=True`)
+        '''
+        return self.getAllElements(StackExpander,single)
+    def getPieceMover(self,single=True):
+        '''Get all or a sole `PieceMover` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `PieceMover` child, otherwise fail.
+            If `False` return all `PieceMover` children in this element
+        Returns
+        -------
+        children : list
+            List of `PieceMover` children (even if `single=True`)
+        '''
+        return self.getAllElements(PieceMover,single)
+    def getSelectionHighlighters(self,single=True):
+        '''Get all or a sole `SelectionHighlighter` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `SelectionHighlighter` child, otherwise fail.
+            If `False` return all `SelectionHighlighter` children in this element
+        Returns
+        -------
+        children : list
+            List of `SelectionHighlighter` children (even if `single=True`)
+        '''
+        return self.getAllElements(SelectionHighlighters,single)
+    def getKeyBufferer(self,single=True):
+        return self.getAllElements(KeyBufferer,single)
+    def getHighlightLastMoved(self,single=True):
+        '''Get all or a sole `HighlightLa` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `HighlightLa` child, otherwise fail.
+            If `False` return all `HighlightLa` children in this element
+        Returns
+        -------
+        children : list
+            List of `HighlightLa` children (even if `single=True`)
+        '''
+        return self.getAllElements(HighlightLastMoved,single)
+    def getCounterDetailViewer(self,single=True):
+        '''Get all or a sole `CounterDetailViewer` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `CounterDetailViewer` child, otherwise fail.
+            If `False` return all `CounterDetailViewer` children in this element
+        Returns
+        -------
+        children : list
+            List of `CounterDetailViewer` children (even if `single=True`)
+        '''
+        return self.getAllElements(CounterDetailViewer,single)
+    def getGlobalMap(self,single=True):
+        '''Get all or a sole `GlobalMap` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `GlobalMap` child, otherwise fail.
+            If `False` return all `GlobalMap` children in this element
+        Returns
+        -------
+        children : list
+            List of `GlobalMap` children (even if `single=True`)
+        '''
+        return self.getAllElements(GlobalMap,single)
+    def getZoomer(self,single=True):
+        '''Get all or a sole `Zoomer` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Zoomer` child, otherwise fail.
+            If `False` return all `Zoomer` children in this element
+        Returns
+        -------
+        children : list
+            List of `Zoomer` children (even if `single=True`)
+        '''
+        return self.getAllElements(Zoomer,single)
+    def getHidePiecesButton(self,single=True):
+        '''Get all or a sole `HidePiece` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `HidePiece` child, otherwise fail.
+            If `False` return all `HidePiece` children in this element
+        Returns
+        -------
+        children : list
+            List of `HidePiece` children (even if `single=True`)
+        '''
+        return self.getAllElements(HidePiecesButton,single)
+    def getMassKeys(self,asdict=True):
+        '''Get all MassKey element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `MassKey` elements.  If `False`, return a list of all MassKey` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `MassKey` children
+        '''
+        return self.getElementsByKey(MassKey,'name',asdict)
+    def getFlare(self,single=True):
+        '''Get all or a sole `Flare` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Flare` child, otherwise fail.
+            If `False` return all `Flare` children in this element
+        Returns
+        -------
+        children : list
+            List of `Flare` children (even if `single=True`)
+        '''
+        return self.getAllElements(Flare,single)
+    def getAtStarts(self,single=True):
+        '''Get all or a sole `AtStart` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `AtStart` child, otherwise fail.
+            If `False` return all `AtStart` children in this element
+        Returns
+        -------
+        children : list
+            List of `AtStart` children (even if `single=True`)
+        '''
+        return self.getAllElements(AtStart,single)
+    def getBoards(self,asdict=True):
+        '''Get all Board element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board` elements.  If `False`, return a list of all Board` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+        '''
+        picker = self.getPicker()
+        if picker is None:  return None
+        return picker[0].getBoards(asdict=asdict)
+    def addBoardPicker(self,**kwargs):
+        '''Add a `BoardPicker` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BoardPicker
+            The added element
+        '''
+        return self.add(BoardPicker,**kwargs)
+    def addStackMetrics(self,**kwargs):
+        '''Add a `StackMetrics` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : StackMetrics
+            The added element
+        '''
+        return self.add(StackMetrics,**kwargs)
+    def addImageSaver(self,**kwargs):
+        '''Add a `ImageSaver` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ImageSaver
+            The added element
+        '''
+        return self.add(ImageSaver,**kwargs)
+    def addTextSaver(self,**kwargs):
+        '''Add a `TextSaver` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : TextSaver
+            The added element
+        '''
+        return self.add(TextSaver,**kwargs)
+    def addForwardToChatter(self,**kwargs):
+        '''Add a `ForwardToChatter` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ForwardToChatter
+            The added element
+        '''
+        return self.add(ForwardToChatter,**kwargs)
+    def addMenuDisplayer(self,**kwargs):
+        '''Add a `MenuDisplayer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MenuDisplayer
+            The added element
+        '''
+        return self.add(MenuDisplayer,**kwargs)
+    def addMapCenterer(self,**kwargs):
+        '''Add a `MapCenterer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MapCenterer
+            The added element
+        '''
+        return self.add(MapCenterer,**kwargs)
+    def addStackExpander(self,**kwargs):
+        '''Add a `StackExpander` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : StackExpander
+            The added element
+        '''
+        return self.add(StackExpander,**kwargs)
+    def addPieceMover(self,**kwargs):
+        '''Add a `PieceMover` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PieceMover
+            The added element
+        '''
+        return self.add(PieceMover,**kwargs)
+    def addSelectionHighlighters(self,**kwargs):
+        '''Add a `SelectionHighlighters` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : SelectionHighlighters
+            The added element
+        '''
+        return self.add(SelectionHighlighters,**kwargs)
+    def addKeyBufferer(self,**kwargs):
+        '''Add a `KeyBufferer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : KeyBufferer
+            The added element
+        '''
+        return self.add(KeyBufferer,**kwargs)
+    def addHighlightLastMoved(self,**kwargs):
+        '''Add a `HighlightLastMoved` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HighlightLastMoved
+            The added element
+        '''
+        return self.add(HighlightLastMoved,**kwargs)
+    def addCounterDetailViewer(self,**kwargs):
+        '''Add a `CounterDetailViewer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : CounterDetailViewer
+            The added element
+        '''
+        return self.add(CounterDetailViewer,**kwargs)
+    def addGlobalMap(self,**kwargs):
+        '''Add a `GlobalMap` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : GlobalMap
+            The added element
+        '''
+        return self.add(GlobalMap,**kwargs)
+    def addZoomer(self,**kwargs):
+        '''Add a `Zoomer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Zoomer
+            The added element
+        '''
+        return self.add(Zoomer,**kwargs)
+    def addHidePiecesButton(self,**kwargs):
+        '''Add a `HidePiecesButton` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HidePiecesButton
+            The added element
+        '''
+        return self.add(HidePiecesButton,**kwargs)
+    def addMassKey(self,**kwargs):
+        '''Add a `MassKey` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MassKey
+            The added element
+        '''
+        return self.add(MassKey,**kwargs)
+    def addStartupMassKey(self,**kwargs):
+        '''Add a `StartupMassKey` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : StartupMassKey
+            The added element
+        '''
+        return self.add(MassKey,**kwargs)
+    def addFlare(self,**kwargs):
+        '''Add a `Flare` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Flare
+            The added element
+        '''
+        return self.add(Flare,**kwargs)
+    def addAtStart(self,**kwargs):
+        '''Add a `AtStart` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : AtStart
+            The added element
+        '''
+        return self.add(AtStart,**kwargs)
+     
 # --------------------------------------------------------------------
-def add_highlightlastmoved(root,map,color=rgb(255,0,0),enabled=True,thickness=2):
-    return add_node(root,map,MAP+'HighlightLastMoved',
-                    color=color,enabled=enabled,thickness=thickness)
+class Map(BaseMap):
+    TAG = Element.MODULE+'Map'
+    def __init__(self,doc,node=None,
+                 mapName              = '',
+                 allowMultiple        = 'false',
+                 backgroundcolor      = rgb(255,255,255),
+                 buttonName           = '',
+                 changeFormat         = '$message$',
+                 color                = rgb(0,0,0),
+                 createFormat         = '$pieceName$ created in $location$ *',
+                 edgeHeight           = '0',
+                 edgeWidth            = '0',
+                 hideKey              = '',
+                 hotkey               = key('M',ALT),
+                 icon                 = '/images/map.gif',
+                 launch               = 'false',
+                 markMoved            = 'Always',
+                 markUnmovedHotkey    = '',
+                 markUnmovedIcon      = '/images/unmoved.gif',
+                 markUnmovedReport    = '',
+                 markUnmovedText      = '',
+                 markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
+                 moveKey              = '',
+                 moveToFormat         = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 moveWithinFormat     = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 showKey              = '',
+                 thickness            = '3'):
+        super(Map,self).__init__(doc,self.TAG,node=node,
+                                 allowMultiple        = allowMultiple,
+                                 backgroundcolor      = backgroundcolor,
+                                 buttonName           = buttonName,
+                                 changeFormat         = changeFormat,
+                                 color                = color,
+                                 createFormat         = createFormat,
+                                 edgeHeight           = edgeHeight,
+                                 edgeWidth            = edgeWidth,
+                                 hideKey              = hideKey,
+                                 hotkey               = hotkey,
+                                 icon                 = icon,
+                                 launch               = launch,
+                                 mapName              = mapName,
+                                 markMoved            = markMoved,
+                                 markUnmovedHotkey    = markUnmovedHotkey,
+                                 markUnmovedIcon      = markUnmovedIcon,
+                                 markUnmovedReport    = markUnmovedReport,
+                                 markUnmovedText      = markUnmovedText,
+                                 markUnmovedTooltip   = markUnmovedTooltip,
+                                 moveKey              = moveKey,
+                                 moveToFormat         = moveToFormat,
+                                 moveWithinFormat     = moveWithinFormat,
+                                 showKey              = showKey,
+                                 thickness            = thickness)
 
+    def getGame(self):
+        return self.getParent(Game)
+        
 # --------------------------------------------------------------------
-def add_counterviewer(root,map,
-                      borderWidth          = 0,
-                      centerAll            = False,
-                      centerText           = False,
-                      combineCounterSummary = False,
-                      counterReportFormat  = '',
-                      delay                = 700,
-                      description          = '',
-                      display              = 'from top-most layer only',
-                      emptyHexReportForma  = '$LocationName$',
-                      enableHTML           = True,
-                      extraTextPadding     = 0,
-                      fgColor              = rgb(0,0,0),
-                      fontSize             = 9,
-                      graphicsZoom         = 1.0,
-                      hotkey               = key('\n',0),
-                      layerList            = '',
-                      minDisplayPieces     = 2,
-                      propertyFilter       = '',
-                      showDeck             = False,
-                      showDeckDepth        = 1,
-                      showDeckMasked       = False,
-                      showMoveSelectde     = False,
-                      showNoStack          = False,
-                      showNonMovable       = False,
-                      showOverlap          = False,
-                      showgraph            = True,
-                      showgraphsingle      = False,
-                      showtext             = False,
-                      showtextsingle       = False,
-                      stretchWidthSummary  = False,
-                      summaryReportFormat  = '$LocationName$',
-                      unrotatePieces       = False,
-                      version              = 3,
-                      verticalOffset       = 0,
-                      verticalTopText      = 5,
-                      zoomlevel            = 1.0):
-    return add_node(root,map,MAP+'CounterDetailViewer',
-                    borderWidth          = borderWidth,
-                    centerAll            = centerAll,
-                    centerText           = centerText,
-                    combineCounterSummary = combineCounterSummary,
-                    counterReportFormat  = counterReportFormat,
-                    delay                = delay,
-                    description          = description,
-                    display              = display,
-                    emptyHexReportForma  = emptyHexReportForma,
-                    enableHTML           = enableHTML,
-                    extraTextPadding     = extraTextPadding,
-                    fgColor              = fgColor,
-                    fontSize             = fontSize,
-                    graphicsZoom         = graphicsZoom,
-                    hotkey               = hotkey,
-                    layerList            = layerList,
-                    minDisplayPieces     = minDisplayPieces,
-                    propertyFilter       = propertyFilter,
-                    showDeck             = showDeck,
-                    showDeckDepth        = showDeckDepth,
-                    showDeckMasked       = showDeckMasked,
-                    showMoveSelectde     = showMoveSelectde,
-                    showNoStack          = showNoStack,
-                    showNonMovable       = showNonMovable,
-                    showOverlap          = showOverlap,
-                    showgraph            = showgraph,
-                    showgraphsingle      = showgraphsingle,
-                    showtext             = showtext,
-                    showtextsingle       = showtextsingle,
-                    stretchWidthSummary  = stretchWidthSummary,
-                    summaryReportFormat  = summaryReportFormat,
-                    unrotatePieces       = unrotatePieces,
-                    version              = version,
-                    verticalOffset       = verticalOffset,
-                    verticalTopText      = verticalTopText,
-                    zoomlevel            = zoomlevel)
+class WidgetMap(BaseMap):
+    TAG = Element.WIDGET+'WidgetMap'
+    def __init__(self,doc,node=None,**attr):
+        super(WidgetMap,self).__init__(doc,self.TAG,node=node,**attr)
 
+    def getGame(self):
+        return self.getParentOfClass([Game])
+    def getMapWidget(self):
+        return self.getParent(MapWidget)
+
+
+#
+# EOF
+#
+# ====================================================================
+# From chart.py
+
 # --------------------------------------------------------------------
-def add_globalmap(root,map,
-                  buttonText           = '',
-                  color                = rgb(255,0,0),
-                  hotkey               = key('O',CTRL_SHIFT),
-                  icon                 = '/images/overview.gif',
-                  scale                = 0.2,
-                  tooltip              = 'Show/Hide overview window'):
-    return add_node(root,map,MAP+'GlobalMap',
-                    buttonText           = buttonText,
-                    color                = color,
-                    hotkey               = hotkey,
-                    icon                 = icon,
-                    scale                = scale,
-                    tooltip              = 'Show/Hide overview window')
+class ChartWindow(GameElement,WidgetElement):
+    TAG=Element.MODULE+'ChartWindow'
+    def __init__(self,elem,node=None,
+                 name        = '',
+                 hotkey      = key('A',ALT),
+                 description = '',
+                 text        = '',
+                 tooltip     = 'Show/hide Charts',
+                 icon        = '/images/chart.gif'):
+        super(ChartWindow,self).__init__(elem,self.TAG,node=node,
+                                         name        = name,
+                                         hotkey      = hotkey,
+                                         description = description,
+                                         text        = text,
+                                         tooltip     = tooltip,
+                                         icon        = icon)
 
+    def addChart(self,**kwargs):
+        '''Add a `Chart` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chart
+            The added element
+        '''
+        return self.add(Chart,**kwargs)
+    def getCharts(self,asdict=True):
+        '''Get all Chart element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Chart` elements.  If `False`, return a list of all Chart` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Chart` children
+        '''
+        return self.getElementsById(Chart,'chartName',asdict=asdict)
+    
+    
+
 # --------------------------------------------------------------------
-def add_zoomer(root,map,
-               inButtonText         = '',
-               inIconName           = '/images/zoomIn.gif',
-               inTooltip            = 'Zoom in',
-               outButtonText        = '',
-               outIconName          = '/images/zoomOut.gif',
-               outTooltip           = 'Zoom out',
-               pickButtonText       = '',
-               pickIconName         = '/images/zoom.png',
-               pickTooltip          = 'Select Zoom',
-               zoomInKey            = key('=',CTRL_SHIFT),
-               zoomLevels           = [0.2,0.25,0.333,0.4,0.5,0.555,0.625,0.75,1.0,1.25,1.6],
-               zoomOutKey           = key('-'),
-               zoomPickKey          = key('='),
-               zoomStart            = 3):
-    '''Zoom start is counting from the back (with default zoom levels,
-    and zoom start, the default zoom is 1'''
-    lvls = ','.join([str(z) for z in zoomLevels])
-    return add_node(root,map,MAP+'Zoomer',
-                    inButtonText         = inButtonText,
-                    inIconName           = inIconName,
-                    inTooltip            = inTooltip,
-                    outButtonText        = outButtonText,
-                    outIconName          = outIconName,
-                    outTooltip           = outTooltip,
-                    pickButtonText       = pickButtonText,
-                    pickIconName         = pickIconName,
-                    pickTooltip          = pickTooltip,
-                    zoomInKey            = zoomInKey,
-                    zoomLevels           = lvls,
-                    zoomOutKey           = zoomOutKey,
-                    zoomPickKey          = zoomPickKey,
-                    zoomStart            = zoomStart)
+class Chart(Element):
+    TAG=Element.WIDGET+'Chart'
+    def __init__(self,elem,node=None,
+                 chartName   = '',
+                 fileName    = '',
+                 description = ''):
+        super(Chart,self).__init__(elem,self.TAG,node=node,
+                                   chartName   = chartName,
+                                   description = description,
+                                   fileName    = fileName)
 
+#
+# EOF
+#
+# ====================================================================
+# From command.py
+
 # --------------------------------------------------------------------
-def add_hidepieces(root,map,
-                   buttonText           = '',
-                   hiddenIcon           = '/images/globe_selected.gif',
-                   hotkey               = key('O'),
-                   showingIcon          = '/images/globe_unselected.gif',
-                   tooltip              = 'Hide all pieces on this map'):
-    return add_node(root,map,MAP+'HidePiecesButton',
-                    buttonText           = buttonText,
-                    hiddenIcon           = hiddenIcon,
-                    hotkey               = hotkey,
-                    showingIcon          = showingIcon,
-                    tooltip              = tooltip)
+class Command:
+    def __init__(self,what,iden,tpe,state):
+        self.cmd = '/'.join([what,iden,tpe,state])
+    
 # --------------------------------------------------------------------
-def add_masskey(root,map,
-                name,                
-                buttonHotkey,
-                hotkey,
-                buttonText           = '',
-                canDisable           = False,
-                deckCount            = '-1',
-                filter               = '',
-                propertyGate         = '',
-                reportFormat         = '',
-                reportSingle         = False,
-                singleMap            = True,
-                target               = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS',
-                tooltip              = '',
-                icon                 = ''):
-    '''Default targets are selected units'''
-    return add_node(root,map,MAP+'MassKeyCommand',
-                    name                 = name,                
-                    buttonHotkey         = buttonHotkey, # This hot key
-                    hotkey               = hotkey,       # Target hot key
-                    buttonText           = buttonText,
-                    canDisable           = canDisable,
-                    deckCount            = deckCount,
-                    filter               = filter,
-                    propertyGate         = propertyGate,
-                    reportFormat         = reportFormat,
-                    reportSingle         = reportSingle,
-                    singleMap            = singleMap,
-                    target               = target,
-                    tooltip              = tooltip,
-                    icon                 = icon)
+class AddCommand(Command):
+    ID = '+'
+    def __init__(self,iden,tpe,state):
+        super(AddCommand,self).__init__(self.ID,iden,tpe,state)
+        
 
+#
+# EOF
+#
+# ====================================================================
+# From trait.py
+# ====================================================================
+class Trait:
+    known_traits = []
+    def __init__(self):
+        '''Base class for trait capture.  Unlike the Element classes,
+        this actually holds state that isn't reflected elsewhere in
+        the DOM.  This means that the data here is local to the
+        object.   So when we do
+        
+            piece  = foo.getPieceSlots()[0]
+            traits = p.getTraits()
+            for trait in traits:
+                if trait.ID == 'piece': 
+                    trait["gpid"] = newPid
+                    trait["lpid"] = newPid
+        
+        we do not actually change anything in the DOM.  To do that, we
+        must add back _all_ the traits as
+        
+            piece.setTraits(traits)
+        
+        We can add traits to a piece, like
+        
+            piece.addTrait(MarkTrait('Hello','World'))
+        
+        But it is not particularly efficient.  Better to do
+        (continuing from above)
+        
+            traits.append(MarkTrait('Hello','World;)
+            piece.setTraits(traits)
+        
+        '''
+        self._type  = None
+        self._state = None
 
+    def setType(self,**kwargs):
+        '''Set types.  Dictionary of names and values.  Dictonary keys
+        defines how we access the fields, which is internal here.
+        What matters is the order of the values.
+
+        '''
+        self._type   = list(kwargs.values())
+        self._tnames = list(kwargs.keys())
+    def setState(self,**kwargs):
+        '''Set states.  Dictionary of names and values.  Dictonary keys
+        defines how we access the fields, which is internal here.
+        What matters is the order of the values.
+        '''
+        self._state  = list(kwargs.values())
+        self._snames = list(kwargs.keys())
+
+    
+    def __getitem__(self,key):
+        '''Look up item in either type or state'''
+        try:
+            return self._type[self._tnames.index(key)]
+        except:
+            pass
+        return self._state[self._snames.index(key)]
+
+    def __setitem__(self,key,value):
+        '''Set item in either type or state'''
+        try:
+            self._type[self._tnames.index(key)] = value
+            return
+        except:
+            pass
+        self._state[self._snames.index(key)] = value
+
+    def encode(self,term=False):
+        '''
+        returns type and state encoded'''
+        t = self.encodeFields(self.ID,*self._type,term=term)
+        s = self.encodeFields(*self._state,term=term)
+        return t,s
+
+    @classmethod
+    def findTrait(cls,traits,ID,key=None,value=None,verbose=False):
+        for trait in traits:
+            if trait.ID != ID:
+                continue
+            if verbose:
+                print(f' {trait.ID}')
+            if key is None or value is None:
+                if verbose:
+                    print(f' Return {trait.ID}')
+                return trait
+            if verbose:
+                print(f' Check {key}={value}: {trait[key]}')
+            if trait[key] == value:
+                return trait
+        if verbose:
+            print(f' Trait of type {ID} with {key}={value} not found')
+        return None
+        
+    @classmethod
+    def take(cls,iden,t,s):
+        '''If the first part of the string t matches the ID, then take it.
+
+        t and s are lists of strings.
+        ''' 
+        if iden != cls.ID: return None
+
+        ret = cls()
+        ret._type = t
+        ret._state = s
+        ret.check() # Check if we're reasonable, or raise
+        #print(f'Took {iden} {cls}\n'
+        #      f'  {ret._tnames}\n'
+        #      f'  {ret._snames}')
+        return ret
+
+    def check(self):
+        '''Implement if trait should check that all is OK when cloning'''
+        pass
+
+    @classmethod
+    def encodeFields(cls,*args,term=False):
+        return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
+                         for e in args])+(';' if term else '')
+
+    @classmethod
+    def decodeFields(cls,s):
+        from re import split
+        return split(r'(?<!\\);',s)
+        # return s.split(';') # Probably too simple-minded 
+
+    @classmethod
+    def encodeKeys(cls,keys,sep=','):
+        return sep.join([k.replace(',','\\'+f'{sep}') for k in keys])
+        
+    @classmethod
+    def decodeKeys(cls,keys,sep=','):
+        from re import split
+        ks = split(r'(?<!\\)'+f'{sep}',keys)
+        return [k.replace('\\'+f'{sep}',f'{sep}') for k in ks]
+
+    @classmethod
+    def flatten(cls,traits,game=None,prototypes=None):
+        if prototypes is None:
+            if game is None:
+                print(f'Warning: Game or prototypes not passed')
+                return None
+            prototypes = game.getPrototypes()[0].getPrototypes()
+
+        ret   = []        
+        return cls._flatten(traits,ret,prototypes,False)
+    
+    @classmethod
+    def _flatten(cls,traits,ret,prototypes,nobasic=True):
+        '''Expand all prototype traits in traits'''
+        for trait in traits:
+            # Ignore recursive basic traits
+            if nobasic and trait.ID == BasicTrait.ID:
+                continue
+            # Add normal traits
+            if trait.ID != PrototypeTrait.ID:
+                ret.append(trait)
+                continue
+
+            # Find prototype
+            proto = prototypes.get(trait['name'],None)
+            if proto is None:
+                print(f'Warning, prototype {trait["name"]} not found')
+                continue
+
+            # Recursive call to add prototype traits (and possibly
+            # more recursive calls 
+            cls._flatten(proto.getTraits(), ret, prototypes,not nobasic)
+
+        return ret
+#
+# EOF
+#
+# ====================================================================
+# From withtraits.py
+
 # --------------------------------------------------------------------
-def add_flare(root,map,
-              circleColor          = rgb(255,0,0),
-              circleScale          = True,
-              circleSize           = 100,
-              flareKey             = 'keyAlt',
-              flareName            = 'Map Flare',
-              flarePulses          = 6,
-              flarePulsesPerSec    = 3,
-              reportFormat         = ''):
-    return add_node(root,map,MAP+'Flare',
-                    circleColor          = circleColor,
-                    circleScale          = circleScale,
-                    circleSize           = circleSize,
-                    flareKey             = flareKey,
-                    flareName            = flareName,
-                    flarePulses          = flarePulses,
-                    flarePulsesPerSec    = flarePulsesPerSec,
-                    reportFormat         = '')
+#
+# Traits of this kind of object are
+#
+# - Evaluated from the start of the list to the end of the list,
+#   skipping over report and trigger traits
+# - Then evaluated from the end of the list to the start, only
+#   evaluating report and trigger traits
+# - The list _must_ end in a BasicTrait
+#
+# Traits are copied when making a copy of objects of this class, and
+# are done so using a full decoding and encoding.  This means that
+# copying is a bit susceptible to expansions of the strings of the traits,
+# in particular if they contain special characters such as ',' or '/'.
+#
+class WithTraits(Element):
+    def __init__(self,parent,tag,node=None,traits=[],**kwargs):
+        '''Base class for things that have traits
 
-# --------------------------------------------------------------------
-def add_atstart(root,map,name,*pieces,
-                location        = '',
-                useGridLocation = True,
-                owningBoard     = '',
-                x               = 0,
-                y               = 0,
-                gpid            = None):
-    '''Pieces are existing PieceSlot elements'''
-    a = add_node(root,map,MODULE+'map.SetupStack',
-                 name            = name,
-                 location        = location,
-                 owningBoard     = owningBoard,
-                 useGridLocation = useGridLocation,
-                 x               = x,
-                 y               = y)
-    # copy pieces here 
-    for p in pieces:
-        c = p.cloneNode(True)
-        a.appendChild(c)
-        if gpid is not None:
-            # Update gpid in element
-            c.setAttribute('gpid',str(gpid))
-            # Update gpid in traits state 
-            traits = get_piece_parts(p.childNodes[0].nodeValue)
-            for t in traits:
-                code = t['def']
-                stat = t['state']
-                if code[0] == 'piece':
-                    stat[3] = gpid
-            # Put back the traits 
-            c.childNodes[0].nodeValue = dicts_to_piece(traits)
+        Parameters
+        ----------
+        parent : Element
+            Parent to add this to
+        node : xml.minidom.Element
+            If not None, XML element to read definition from.
+            Rest of the arguments are ignored if not None.
+        traits : list of Trait objects
+            The traits to set on this object
+        kwargs : dict
+            More attributes to set on element
+        '''
+        super(WithTraits,self).__init__(parent,tag,node=node,**kwargs)
+        if node is None: self.setTraits(*traits)
+        
+    def addTrait(self,trait):
+        '''Add a `Trait` element to this.  Note that this re-encodes
+        all traits.
+
+        Parameters
+        ----------
+        trait : Trait
+            The trait to add 
+        '''
+        traits = self.getTraits()
+        traits.append(trait)
+        self.setTraits(*traits)
+
+    def getTraits(self):
+        '''Get all element traits as objects.  This decodes the trait
+        definitions.  This is useful if we have read the element from
+        the XML file, or similar.
+
+        Note that the list of traits returned are _not_ tied to the
+        XML nodes content.  Therefore, if one makes changes to the list,
+        or to elements of the list, and these changes should be
+        reflected in this object, then we _must_ call
+
+            setTraits(traits)
+
+        with the changed list of traits. 
+
+        Returns
+        -------
+        traits : list of Trait objects
+            The decoded traits
+
+        '''
+        from re import split
+        code                = self._node.childNodes[0].nodeValue
+        cmd, iden, typ, sta = split(fr'(?<!\\)/',code) #code.split('/')
+        types               = typ.split(r'	')
+        states              = sta.split(r'	')
+        types               = [t.strip('\\').split(';') for t in types]
+        states              = [s.strip('\\').split(';') for s in states]
+        traits              = []
+        
+        for t, s in zip(types,states):
+            tid   = t[0]
+            trem  = t[1:]
+            known = False
             
-            gpid += 1
+            for c in Trait.known_traits:
+                t = c.take(tid,trem,s) # See if we have it
+                if t is not None:
+                    traits.append(t)  # Got it
+                    known = True
+                    break
+                
+            if not known:
+                print(f'Warning: Unknown trait {tid}')
 
-    return a, gpid
+        return traits
+    
+    def setTraits(self,*traits,iden='null'):
+        '''Set traits on this element.  This encodes the traits into
+        this object.
+        
+        Parameters
+        ----------
+        traits : tuple of Trait objects
+            The traits to set on this object.
+        iden : str
+            Identifier
 
+        '''
+        types = []
+        states = []
+        for trait in traits:
+            if trait is None:
+                print(f'Trait is None (traits: {traits})')
+                continue
+            tpe, state = trait.encode()
+            types.append(tpe)
+            states.append(state)
+
+        tpe   = WithTraits.encodeParts(*types)
+        state = WithTraits.encodeParts(*states)
+        add   = AddCommand(str(iden),tpe,state)
+        if len(self._node.childNodes) < 1:
+            self.addText('')
+        self._node.childNodes[0].nodeValue = add.cmd
+
+    def removeTrait(self,ID,key=None,value=None,verbose=False):
+        '''Remove a trait from this object.
+
+        Parameters
+        ----------
+        ID : str
+            The type of trait to remove.  Must be a valid
+            ID of a class derived from Trait.
+        key : str
+            Optional key to inspect to select trait that has 
+            this key and the traits key value is the argument value,
+        value :
+            If specified, then only traits which key has this value
+            are removed
+        verbose : bool
+            Be verbose if True
+
+        Returns
+        -------
+        trait : Trait
+            The removed trait or None
+        '''
+        traits = self.getTraits()
+        trait  = Trait.findTrait(traits,ID,key,value,verbose)
+        if trait is not None:
+            traits.remove(trait)
+            self.setTraits(traits)
+        return trait
+
+    def addTraits(self,*toadd):
+        '''Add traits to this.  Note that this will
+        decode and reencode the traits.  Only use this when
+        adding traits on-mass.  Repeated use of this is inefficient.
+
+        This member function takes care to push any basic trait to
+        the end of the list.
+
+        The added traits will not override existing triats. 
+
+        Paramters
+        ---------
+        toAdd : tuple of Trait objects
+            The traits to add 
+
+        '''
+        traits = self.getTraits()
+        basic  = Trait.findTrait(traits,BasicTrait.ID)
+        if basic:
+            traits.remove(basic)
+        traits.extend(toAdd)
+        if basic:
+            traits.append(basic)
+        self.setTraits(traits)
+        
+        
+    @classmethod
+    def encodeParts(cls,*parts):
+        '''Encode parts of a full piece definition
+
+        Each trait (VASSAL.counter.Decorator,
+        VASSAL.counter.BasicPiece) definition or state is separated by
+        a litteral TAB character.  Beyond the first TAB separator,
+        additional escape characters (BACKSLAH) are added in front of
+        the separator.  This is to that VASSAL.utils.SequenceDecoder
+        does not see consequitive TABs as a single TAB.
+        '''
+        ret = ''
+        sep = r'	'
+        for i, p in enumerate(parts):
+            if i != 0:
+                ret += '\\'*(i-1) + sep
+            ret += str(p)
+
+        return ret
+        
+        
+    def cloneNode(self,parent):
+        '''This clones the underlying XML node.
+
+        Parameters
+        ----------
+        parent : Element
+            The element to clone this element into
+
+        Returns
+        -------
+        copy : xml.minidom.Element
+            The newly created clone of this object's node
+        '''
+        copy = self._node.cloneNode(deep=True)
+        if parent is not None:
+            parent._node.appendChild(copy)
+        else:
+            print('WARNING: No parent to add copy to')
+        return copy
+    
 # --------------------------------------------------------------------
-def add_pieceslot(root,elem,
-                  entryName,
-                  gpid,
-                  code,
-                  height=72,
-                  width=72,
-                  icon = ''):
-    p = add_node(root,elem,WIDGET+'PieceSlot',
-                 entryName = entryName,
-                 gpid      = gpid,
-                 height    = height,
-                 width     = width,
-                 icon      = icon)
-    add_text(root,p,code)
-    return p
+class DummyWithTraits(WithTraits):
+    TAG = 'dummy'
+    def __init__(self,parent,node=None,traits=[]):
+        '''An empty element.  Used when making searching'''
+        super(DummyWithTraits,self).__init__(tag       = self.TAG,
+                                             parent    = parent,
+                                             node      = node,
+                                             traits    = traits)
+        if parent is not None:
+            parent.remove(self)
 
+    
 # --------------------------------------------------------------------
-def add_prototypes(root,elem,*defs):
-    '''Defs are existing definition elements'''
-    p = add_node(root,elem,MODULE+'PrototypesContainer')
-    
-    for d in defs:
-        c = d.cloneNode(True)
-        p.appendChild(c)
+class PieceSlot(WithTraits):
+    TAG = Element.WIDGET+'PieceSlot'
+    def __init__(self,parent,node=None,
+                 entryName      = '',
+                 traits         = [],
+                 gpid           = 0,
+                 height         = 72,
+                 width          = 72,
+                 icon           = ''):
+        '''A piece slot.  Used all the time.
 
-    return p
+        Parameters
+        ----------
+        parent : Element
+            Parent to add this to
+        node : xml.minidom.Element
+            If not None, XML element to read definition from.
+            Rest of the arguments are ignored if not None.
+        entryName : str
+            Name of this
+        traits : list of Trait objects
+            The traits to set on this object
+        gpid : int
+            Global Piece identifier. If 0, will be set by Game
+        height : int
+            Height size of the piece (in pixels)
+        width : int
+            Width size of the piece (in pixels)
+        icon : str
+            Piece image file name within 'image' sub-dir of archive
+        '''
+        super(PieceSlot,self).\
+            __init__(parent,self.TAG,node=node,
+                     traits    = traits,
+                     entryName = entryName,
+                     gpid      = gpid,
+                     height    = height,
+                     width     = width,
+                     icon      = icon)
+
+    def clone(self,parent):
+        '''Adds copy of self to parent, possibly with new GPID'''
+        game  = self.getParentOfClass([Game])
+        gpid  = game.nextPieceSlotId()
+        #opid  = int(self.getAttribute('gpid'))
+        #print(f'Using GPID={gpid} for clone {opid}')
         
+        node  = self.cloneNode(parent)
+        piece = PieceSlot(parent,node=node)
+        piece.setAttribute('gpid',gpid)
+        
+        traits = piece.getTraits()
+        for trait in traits:
+            if isinstance(trait,BasicTrait):
+                trait['gpid'] = gpid
+
+        piece.setTraits(*traits)
+        return piece
+        
+
 # --------------------------------------------------------------------
-def add_prototype(root,elem,name,code,description=''):
-    d = add_node(root,elem,MODULE+'PrototypeDefinition',
-                 name        = name,
-                 description = description)
-    add_text(root,d,code)
-    return d
+class Prototype(WithTraits):
+    TAG = Element.MODULE+'PrototypeDefinition'
+    def __init__(self,cont,node=None,
+                 name          = '',
+                 traits        = [],
+                 description   = ''):
+        '''A prototype.  Used all the time.
+
+        Parameters
+        ----------
+        cont : Element
+            Parent to add this to
+        node : xml.minidom.Element
+            If not None, XML element to read definition from.
+            Rest of the arguments are ignored if not None.
+        name : str
+            Name of this
+        traits : list of Trait objects
+            The traits to set on this object
+        description : str
+            A free-form description of this prototype
+        '''
+        super(Prototype,self).__init__(cont,self.TAG,node=node,
+                                       traits      = traits,
+                                       name        = name,
+                                       description = description)
     
+#
+# EOF
+#
+# ====================================================================
+# From traits/dynamicproperty.py
+
 # --------------------------------------------------------------------
-def add_piecewindow(root,elem,name,
-                    defaultWidth = 0,
-                    hidden       = False,
-                    hotkey       = key('C',ALT),
-                    scale        = 1.,
-                    text         = '',
-                    tooltip      = 'Show/hide piece window',
-                    icon         = '/images/counter.gif'):
-    return add_node(root,elem,MODULE+'PieceWindow',
-                    name = name,
-                    defaultWidth = defaultWidth,
-                    hidden       = hidden,
-                    hotkey       = hotkey,
-                    scale        = scale,
-                    text         = text,
-                    tooltip      = tooltip,
-                    icon         = icon)
+# Base class for property (piece or global) change traits.  Encodes
+# constraints and commands.
+class ChangePropertyTrait(Trait):
+    DIRECT = 'P'
+    def __init__(self,
+                 *commands,
+                 numeric     = False,
+                 min         = 0,
+                 max         = 100,
+                 wrap        = False):
+        # assert name is not None and len(name) > 0, \
+        #     'No name specified for ChangePropertyTriat'
+        super(ChangePropertyTrait,self).__init__()
+        self._constraints = self.encodeConstraints(numeric,wrap,min,max)
+        self._commands    = self.encodeCommands(commands)
 
+    def encodeConstraints(self,numeric,wrap,min,max):
+        isnum             = f'{numeric}'.lower()
+        iswrap            = f'{wrap}'.lower()
+        return f'{isnum},{min},{max},{iswrap}'
+
+    def decodeConstraints(self,constraints):
+        f = Trait.decodeKeys(constraints)
+        return f[0]=='true',f[3]=='true',int(f[1]),int(f[2])
+    
+    def encodeCommands(self,commands):
+        cmds              = []
+        for cmd in commands:
+            # print(cmd)
+            com = cmd[0] + ':' + cmd[1].replace(',',r'\,') + ':' + cmd[2]
+            if cmd[2] == self.DIRECT:
+                com += f'\,'+cmd[3].replace(':',r'\:')
+            cmds.append(com)
+        return ','.join(cmds)
+
+    def decodeCommands(self,commands):
+        cmds = Trait.decodeKeys(commands)
+        ret  = []
+        for cmd in cmds:
+            parts = Trait.decodeKeys(cmd,':')
+            # print('parts',parts)
+            if parts[-1][0] == self.DIRECT:
+                parts = parts[:-1]+Trait.decodeKeys(parts[-1],',')
+            ret.append(parts)
+        # print(commands,parts)
+        return ret
+    
+    def getCommands(self):
+        return self.decodeCommands(self['commands'])
+
+    def setCommands(self,commands):
+        self['commands'] = self.encodeCommands(commands)
+        
+    def check(self):
+        assert len(self['name']) > 0,\
+            f'No name given for ChangePropertyTrait'
+        
+        
 # --------------------------------------------------------------------
-def add_tabs(root,elem,entryName):
-    return add_node(root,elem,WIDGET+'TabWidget',entryName=entryName)
+class DynamicPropertyTrait(ChangePropertyTrait):
+    ID = 'PROP'
+    def __init__(self,
+                 *commands,
+                 name        = '',
+                 value       = 0,
+                 numeric     = False,
+                 min         = 0,
+                 max         = 100,
+                 wrap        = False,
+                 description = ''):
+        '''Commands are
 
+            - menu
+            - key
+            - Type (only 'P' for now)
+            - Expression
+        '''
+        super(DynamicPropertyTrait,self).__init__(*commands,
+                                                  numeric = numeric,
+                                                  min     = min,
+                                                  max     = max,
+                                                  wrap    = wrap)
+        # print(commands,'Name',name)
+        self.setType(name        = name,
+                     constraints = self._constraints,
+                     commands    = self._commands,
+                     description = description)
+        self.setState(value=value)
+
+    
+Trait.known_traits.append(DynamicPropertyTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalproperty.py
+
 # --------------------------------------------------------------------
-def add_list(root,elem,
-             entryName,
-             height  = 0,
-             width   = 0,
-             scale   = 1.,
-             divider = 100):
-    return add_node(root,elem,WIDGET+'ListWidget',
-                    entryName = entryName,
-                    height    = height,
-                    width     = width,
-                    scale     = scale,
-                    divider   = divider)
+class GlobalPropertyTrait(ChangePropertyTrait):
+    # The real value of CURRENT_ZONE causes problems when copying the
+    # trait, since it contains slashes.  Maybe a solition is to make
+    # it a raw string with escaped slashes?  No, that's already done
+    # below when setting the type.  However, the default in the Java
+    # code is the CURRENT_ZONE real value, so setting this to the
+    # empty string should make it be that value.
+    ID = 'setprop'
+    CURRENT_ZONE = 'Current Zone/Current Map/Module'
+    NAMED_ZONE   = 'Named Zone'
+    NAMED_MAP    = 'Named Map'
+    DIRECT       = 'P'
+    def __init__(self,
+                 *commands,
+                 name        = '',
+                 numeric     = False,
+                 min         = 0,
+                 max         = 100,
+                 wrap        = False,
+                 description = '',
+                 level       = CURRENT_ZONE,
+                 search      = ''):
+        '''Commands are
 
+            - menu
+            - key
+            - Type (only 'P' for now)
+            - Expression
+        '''
+        super(GlobalPropertyTrait,self).__init__(*commands,
+                                                 numeric = numeric,
+                                                 min     = min,
+                                                 max     = max,
+                                                 wrap    = wrap)
+        self.setType(name        = name,
+                     constraints = self._constraints,
+                     commands    = self._commands,
+                     description = description,
+                     level       = level.replace('/',r'\/'),
+                     search      = search)
+        self.setState()
+
+Trait.known_traits.append(GlobalPropertyTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/prototype.py
+
 # --------------------------------------------------------------------
-def add_panel(root,elem,
-              entryName,
-              fixed = False,
-              nColumns = 1,
-              vert     = False):
-    return add_node(root,elem,WIDGET+'PanelWidget',
-                    entryName = entryName,
-                    fixed     = fixed,
-                    nColumns  = nColumns,
-                    vert      = vert)
+class PrototypeTrait(Trait):
+    ID = 'prototype'
+    def __init__(self,name=''):
+        '''Create a prototype trait (VASSAL.counter.UsePrototype)'''
+        super(PrototypeTrait,self).__init__()
+        self.setType(name = name)
+        self.setState(ignored = '')
 
+
+Trait.known_traits.append(PrototypeTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/place.py
+
 # --------------------------------------------------------------------
-def add_mapwidget(root,elem,entryName=''):
-    return add_node(root,elem,WIDGET+'MapWidget',entryName=entryName)
+class PlaceTrait(Trait):
+    ID      = 'placemark'
+    STACK_TOP = 0
+    STACK_BOTTOM = 1
+    ABOVE = 2
+    BELOW = 3
 
+    # How the LaTeX exporter organises the units.  Format with
+    # 0: the group
+    # 1: the piece name 
+    # SKEL_PATH = (PieceWindow.TAG +r':Counters\/'        +
+    #              TabWidget.TAG   +r':Counters\/'        +
+    #              PanelWidget.TAG +':{0}'         +r'\/'+
+    #              ListWidget.TAG  +':{0} counters'+r'\/'+
+    #              PieceSlot.TAG   +':{1}')
+    @classmethod
+    @property
+    def SKEL_PATH(cls):
+
+        return (PieceWindow.TAG +r':Counters\/'        +
+                TabWidget.TAG   +r':Counters\/'        +
+                PanelWidget.TAG +':{0}'         +r'\/'+
+                ListWidget.TAG  +':{0} counters'+r'\/'+
+                PieceSlot.TAG   +':{1}')
+    
+    def __init__(self,
+                 command         = '', # Context menu name
+                 key             = '', # Context menu key
+                 markerSpec      = '', # Full path in module
+                 markerText      = 'null', # Hard coded message
+                 xOffset         = 0,
+                 yOffset         = 0,
+                 matchRotation   = True,
+                 afterKey        = '',
+                 description     = '',
+                 gpid            = '', # Set in JAVA, but with warning
+                 placement       = ABOVE,
+                 above           = False):
+        '''Create a place marker trait (VASSAL.counter.PlaceMarker)'''
+        super(PlaceTrait,self).__init__()
+        self.setType(command         = command,          # Context menu name
+                     key             = key,              # Context menu key
+                     markerSpec      = markerSpec,
+                     markerText      = markerText,
+                     xOffset         = xOffset,
+                     yOffset         = yOffset,
+                     matchRotation   = matchRotation,
+                     afterKey        = afterKey,
+                     description     = description,
+                     gpid            = gpid,
+                     placement       = placement,
+                     above           = above)
+        self.setState()
+
+Trait.known_traits.append(PlaceTrait)
+
 # --------------------------------------------------------------------
-def add_chartwindow(root,elem,
-                    name,
-                    hotkey      = key('A',ALT),
-                    description = '',
-                    text        = '',
-                    tooltip     = 'Show/hide Charts',
-                    icon        = '/images/chart.gif'):
-    return add_node(root,elem,MODULE+'ChartWindow',
-                    name        = name,
-                    hotkey      = hotkey,
-                    description = description,
-                    text        = text,
-                    tooltip     = tooltip,
-                    icon        = icon)
+class ReplaceTrait(PlaceTrait):
+    ID = 'replace'
+    def __init__(self,
+                 command         = '', # Context menu name
+                 key             = '', # Context menu key
+                 markerSpec      = '', # Full path in module
+                 markerText      = 'null', # Hard message
+                 xOffset         = 0,
+                 yOffset         = 0,
+                 matchRotation   = True,
+                 afterKey        = '',
+                 description     = '',
+                 gpid            = '', # Set in JAVA
+                 placement       = PlaceTrait.ABOVE,
+                 above           = False):
+        super(ReplaceTrait,self).__init__(command         = command, 
+                                          key             = key,  
+                                          markerSpec      = markerSpec,
+                                          markerText      = markerText,
+                                          xOffset         = xOffset,
+                                          yOffset         = yOffset,
+                                          matchRotation   = matchRotation,
+                                          afterKey        = afterKey,
+                                          description     = description,
+                                          gpid            = gpid,
+                                          placement       = placement,
+                                          above           = above)
+    
 
+Trait.known_traits.append(ReplaceTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/report.py
+
 # --------------------------------------------------------------------
-def add_chart(root,elem,
-              chartName,
-              fileName,
-              description = ''):
-    return add_node(root,elem,WIDGET+'Chart',
-                    chartName   = chartName,
-                    description = description,
-                    fileName    = fileName)
+class ReportTrait (Trait):
+    ID = 'report'
+    def __init__(self,
+                 *keys,
+                 nosuppress = True,
+                 description = '',
+                 report      = '$location$: $newPieceName$ $menuCommand$ *',
+                 cyclekeys   = '',
+                 cyclereps   = ''):
+        '''Create a report trait (VASSAL.counters.ReportActon)'''
+        super(ReportTrait,self).__init__()
+        esckeys = ','.join([k.replace(',',r'\,') for k in keys])
+        esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
+        escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
+        
+        self.setType(keys         = esckeys,
+                     report       = report,
+                     cycleKeys    = esccycl,
+                     cycleReports = escreps,
+                     description  = description,
+                     nosuppress   = nosuppress)
+        self.setState(cycle = -1)
 
+Trait.known_traits.append(ReportTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/calculatedproperty.py
+
 # --------------------------------------------------------------------
-def add_dice(root,elem,
-             addToTotal           = 0,
-             canDisable           = False,
-             hotkey               = key('6',ALT),
-             icon                 = '/images/die.gif',
-             keepCount            = 1,
-             keepDice             = False,
-             keepOption           = '>',
-             lockAdd              = False,
-             lockDice             = False,
-             lockPlus             = False,
-             lockSides            = False,
-             nDice                = 1,
-             nSides               = 6,
-             name                 = '1d6',
-             plus                 = 0,
-             prompt               = False,
-             propertyGate         = '',
-             reportFormat         = '** $name$ = $result$ *** &lt;$PlayerName$&gt;',
-             reportTotal          = False,
-             sortDice             = False,
-             text                 = '1d6',
-             tooltip              = 'Roll a 1d6'):
-    return add_node(root,elem,MODULE+'DiceButton',
-                    addToTotal           = addToTotal,
-                    canDisable           = canDisable,
-                    hotkey               = hotkey,
-                    icon                 = icon,
-                    keepCount            = keepCount,
-                    keepDice             = keepDice,
-                    keepOption           = keepOption,
-                    lockAdd              = lockAdd,
-                    lockDice             = lockDice,
-                    lockPlus             = lockPlus,
-                    lockSides            = lockSides,
-                    nDice                = nDice,
-                    nSides               = nSides,
-                    name                 = name,
-                    plus                 = plus,
-                    prompt               = prompt,
-                    propertyGate         = propertyGate,
-                    reportFormat         = reportFormat,
-                    reportTotal          = reportTotal,
-                    sortDice             = sortDice,
-                    text                 = text,
-                    tooltip              = tooltip)
+class CalculatedTrait(Trait):
+    ID = 'calcProp'
+    def __init__(self,name='',expression='',description=''):
+        '''Define a trait that calculates a property'''
+        super(CalculatedTrait,self).__init__()
+        self.setType(name        = name,
+                     expression  = expression,
+                     description = description)
+        self.setState()
 
+
+Trait.known_traits.append(CalculatedTrait)
+
+#
+# EOF
+#
 # ====================================================================
-def enc_def(args,term=False):
-    '''Encode a piece (decorator) definition
+# From traits/restrictcommand.py
 
-    Parameters
-    ----------
-    args : list
-        List of fields
-    term : bool
-        If true, add terminating ';'
+# --------------------------------------------------------------------
+class RestrictCommandsTrait(Trait):
+    ID = 'hideCmd'
+    HIDE = 'Hide'
+    DISABLE = 'Disable'
+    def __init__(self,
+                 name          = '',
+                 hideOrDisable = HIDE,
+                 expression    = '',# Restrict when true
+                 keys          = []):
+        '''Create a layer trait (VASSAL.counter.RestrictCommands)'''
+        super(RestrictCommandsTrait,self).__init__()
+        encKeys = ','.join([k.replace(',',r'\,') for k in keys])
+        self.setType(name          = name,
+                     hideOrDisable = hideOrDisable,
+                     expression    = expression,
+                     keys          = encKeys)
+        self.setState(state='')
+    def setKeys(self,keys):
+        self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
+    
 
-    Returns
-    -------
-    code : str
-        The encoded definition
-    '''
-    return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
-                     for e in args])+(';' if term else '')
+Trait.known_traits.append(RestrictCommandsTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/label.py
+
+class LabelTraitCodes:
+    TOP    = 't'
+    BOTTOM = 'b'
+    CENTER = 'c'
+    LEFT   = 'l'
+    RIGHT  = 'r'
+    PLAIN  = 0
+    BOLD   = 1
+    ITALIC = 2
+    
 # --------------------------------------------------------------------
-def layer_trait(images,
-                newNames     = None,
-                activateName = 'Activate',
-                activateMask = CTRL,
-                activateChar = 'A',
-                increaseName = 'Increase',
-                increaseMask = CTRL,
-                increaseChar = '[',
-                decreaseName = '',
-                decreaseMask = CTRL,
-                decreaseChar  = ']',
-                resetName    = '',
-                resetKey     = '',
-                resetLevel   = 1,
-                under        = False,
-                underXoff    = 0,
-                underYoff    = 0,
-                loop         = True,
-                name         = '',
-                description  = '',
-                randomKey    = '',
-                randomName   = '',
-                follow       = False,
-                expression   = '',
-                first        = 1,
-                version      = 1, # 1:new, 0:old
-                always       = 'true',
-                activateKey  = key('A'),
-                increaseKey  = key('['),
-                decreaseKey  = key(']'),
-                scale        = 1.):
-    '''Create a layer trait (VASSAL.counter.Embellishment)
+class LabelTrait(Trait):
+    ID     = 'label'
+    def __init__(self,
+                 label           = None,
+                 labelKey        = '',
+                 menuCommand     ='Change label',
+                 fontSize        = 10,
+                 background      = 'none',
+                 foreground      = '255,255,255',
+                 vertical        = LabelTraitCodes.TOP,
+                 verticalOff     = 0,
+                 horizontal      = LabelTraitCodes.CENTER,
+                 horizontalOff   = 0,
+                 verticalJust    = LabelTraitCodes.BOTTOM,
+                 horizontalJust  = LabelTraitCodes.CENTER,
+                 nameFormat      = '$pieceName$ ($label$)',
+                 fontFamily      = 'Dialog',
+                 fontStyle       = LabelTraitCodes.PLAIN,
+                 rotate          = 0,
+                 propertyName    = 'TextLabel',
+                 description     = '',
+                 alwaysUseFormat = False):
+        '''Create a label trait (can be edited property)'''
+        super(LabelTrait,self).__init__()
+        self.setType(labelKey		= labelKey,
+                     menuCommand	= menuCommand,
+                     fontSize		= fontSize,
+                     background		= background,
+                     foreground		= foreground,
+                     vertical		= vertical,
+                     verticalOff	= verticalOff,
+                     horizontal		= horizontal,
+                     horizontalOff	= horizontalOff,
+                     verticalJust	= verticalJust,
+                     horizontalJust	= horizontalJust,
+                     nameFormat		= nameFormat,
+                     fontFamily		= fontFamily,
+                     fontStyle		= fontStyle,
+                     rotate		= rotate,
+                     propertyName	= propertyName,
+                     description	= description,
+                     alwaysUseFormat	= alwaysUseFormat)
+        self.setState(label = (nameFormat if label is None else label))
 
-    Parameters
-    ----------
-    main : dict
-        Main image dictionary
-    flipped : dict
-        Flipped image dictionary
 
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state
-    '''
-    if newNames is None:
-        newNames = ['']*len(images)
-    args = ['emb2',                # Code
-            activateName, activateMask, activateChar,    # ACTIVATE ; MASK ; KEY
-            increaseName, increaseMask, increaseChar,
-            decreaseName, decreaseMask, decreaseChar,
-            resetName,    resetKey,     resetLevel,
-            under,        underXoff,    underYoff,
-            ','.join(images),
-            ','.join(newNames),
-            loop,
-            name,
-            randomKey, randomName,
-            follow,    expression,
-            first,
-            version,
-            always,
-            activateKey,
-            increaseKey,
-            decreaseKey,
-            description,
-            scale ]
-    stat = '1' # CURRENT LEVEL
-    return enc_def(args), stat
+Trait.known_traits.append(LabelTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/layer.py
+
 # --------------------------------------------------------------------
-def prototype_trait(name):
-    '''Create a prototype trait (VASSAL.counter.UsePrototype)
+class LayerTrait(Trait):
+    ID = 'emb2'
+    def __init__(self,
+                 images       = [''],
+                 newNames     = None,
+                 activateName = 'Activate',
+                 activateMask = CTRL,
+                 activateChar = 'A',
+                 increaseName = 'Increase',
+                 increaseMask = CTRL,
+                 increaseChar = '[',
+                 decreaseName = '',
+                 decreaseMask = CTRL,
+                 decreaseChar  = ']',
+                 resetName    = '',
+                 resetKey     = '',
+                 resetLevel   = 1,
+                 under        = False,
+                 underXoff    = 0,
+                 underYoff    = 0,
+                 loop         = True,
+                 name         = '',
+                 description  = '',
+                 randomKey    = '',
+                 randomName   = '',
+                 follow       = False,
+                 expression   = '',
+                 first        = 1,
+                 version      = 1, # 1:new, 0:old
+                 always       = True,
+                 activateKey  = key('A'),
+                 increaseKey  = key('['),
+                 decreaseKey  = key(']'),
+                 scale        = 1.):
+        '''Create a layer trait (VASSAL.counter.Embellishment)'''
+        super(LayerTrait,self).__init__()
+        if newNames is None and images is not None:
+            newNames = ['']*len(images)
+        self.setType(
+            activateName        = activateName,
+            activateMask        = activateMask,
+            activateChar        = activateChar,
+            increaseName        = increaseName,
+            increaseMask        = increaseMask,
+            increaseChar        = increaseChar,
+            decreaseName        = decreaseName,
+            decreaseMask        = decreaseMask,
+            decreaseChar        = decreaseChar,
+            resetName           = resetName,
+            resetKey            = resetKey,
+            resetLevel          = resetLevel,
+            under               = under,
+            underXoff           = underXoff,
+            underYoff           = underYoff,
+            images              = ','.join(images),
+            newNames            = ','.join(newNames),
+            loop                = loop,
+            name                = name,
+            randomKey           = randomKey,
+            randomName          = randomName,
+            follow              = follow,
+            expression          = expression,
+            first               = first,
+            version             = version,
+            always              = always,
+            activateKey         = activateKey,
+            increaseKey         = increaseKey,
+            decreaseKey         = decreaseKey,
+            description         = description,
+            scale               = scale)
+        self.setState(level=1)
 
-    Parameters
-    ----------
-    family : str
-        Side in game
+Trait.known_traits.append(LayerTrait)
 
-    Return
-    ------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['prototype', name ]
-    stat = '' # State is nothing 
-    return enc_def(args), stat
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalcommand.py
 
 # --------------------------------------------------------------------
-def mark_trait(key,value):
-    '''Create a marker trait (VASSAL.counter.Marker)
+class GlobalCommandTrait(Trait):
+    ID = 'globalkey'
+    def __init__(self,
+                 commandName   = '',
+                 key           = '', # Command received
+                 globalKey     = '', # Command to send to targets
+                 properties    = '', # Filter target on this expression
+                 ranged        = False,
+                 range         = 1,
+                 reportSingle  = True,
+                 fixedRange    = True,
+                 rangeProperty = '',
+                 description   = '',
+                 deskSelect    = '-1',
+                 target        = ''):
+        '''Create a global key command in piece
+        (VASSAL.counters.CounterGlobalKeyCommand)'''
+        self.setType(commandName   = commandName,
+                     key           = key,
+                     globalKey     = globalKey,
+                     properties    = properties,
+                     ranged        = ranged,
+                     range         = range,
+                     reportSingle  = reportSingle,
+                     fixedRange    = fixedRange,
+                     rangeProperty = rangeProperty,
+                     description   = description,
+                     deskSelect    = deskSelect,
+                     target        = target)
+        self.setState()
+        
+Trait.known_traits.append(GlobalCommandTrait)
 
-    Parameters
-    ----------
-    family : str
-        Side in game
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalhotkey.py
 
-    Return
-    ------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['mark', key ]
-    stat = value # State is nothing 
-    return enc_def(args), stat
+# --------------------------------------------------------------------
+class GlobalHotkeyTrait(Trait):
+    ID = 'globalhotkey'
+    def __init__(self,
+                 name          = '', # Command received
+                 key           = '', # Command key received
+                 globalHotkey  = '', # Key to send
+                 description   = ''):
+        '''Create a global key command in piece
+        (VASSAL.counters.GlobalHotkey)'''
+        self.setType(name          = name,
+                     key           = key,
+                     globalHotkey  = globalHotkey,
+                     description   = description)
+        self.setState()
+        
+Trait.known_traits.append(GlobalHotkeyTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/nostack.py
+
 # --------------------------------------------------------------------
-def report_trait(*keys,
-                 nosuppress = True,
+class NoStackTrait(Trait):
+    ID = 'immob'
+    NORMAL_SELECT         = ''
+    SHIFT_SELECT          = 'i'
+    CTRL_SELECT           = 't'
+    ALT_SELECT            = 'c'
+    NEVER_SELECT          = 'n'
+    NORMAL_BAND_SELECT    = ''
+    ALT_BAND_SELECT       = 'A'
+    ALT_SHIFT_BAND_SELECT = 'B'
+    NEVER_BAND_SELECT     = 'Z'
+    NORMAL_MOVE           = 'N'
+    SELECT_MOVE           = 'I'
+    NEVER_MOVE            = 'V'
+    NORMAL_STACK          = 'L'
+    NEVER_STACK           = 'R'
+    IGNORE_GRID           = 'g'
+    def __init__(self,
+                 select      = NORMAL_SELECT,
+                 bandSelect  = NORMAL_BAND_SELECT,
+                 move        = NORMAL_MOVE,
+                 canStack    = False,
+                 ignoreGrid  = False,
+                 description = ''):
+
+        selectionOptions = (select +
+                            (self.IGNORE_GRID if ignoreGrid else '') +
+                            bandSelect)
+        movementOptions  = move
+        stackingOptions  = self.NORMAL_STACK if canStack else self.NEVER_STACK
+                 
+        '''Create a mark trait (static property)'''
+        super(NoStackTrait,self).__init__()
+        
+        self.setType(selectionOptions = selectionOptions,
+                     movementOptions  = movementOptions,
+                     stackingOptions  = stackingOptions,
+                     description      = description)
+        self.setState()
+
+
+Trait.known_traits.append(NoStackTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/deselect.py
+
+# --------------------------------------------------------------------
+class DeselectTrait(Trait):
+    ID = 'deselect'
+    THIS = 'D' # Deselect only this piece
+    ALL  = 'A' # Deselect all pieces 
+    ONLY = 'S' # Select this piece only
+    def __init__(self,
+                 command     = '',
+                 key         = '',
                  description = '',
-                 report      = '$location$: $newPieceName$ $menuCommand$ *',
-                 cyclekeys   = '',
-                 cyclereps   = ''):
-    '''Create a report trait (VASSAL.counters.ReportActon)
+                 unstack     = False,
+                 deselect    = THIS):
+        '''Create a deselect trait'''
+        super(DeselectTrait,self).__init__()
+        self.setType(command     = command,
+                     key         = key,
+                     description = description,
+                     unstack     = unstack,
+                     deselect    = deselect)
+        self.setState()
 
-    Parameters
-    ----------
-    *keys : tuple of str
-    	The keys to report actions for
-    
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    esckeys = ','.join([k.replace(',',r'\,') for k in keys])
-    esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
-    escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
-    args = ['report',    # CODE
-            esckeys,     # ESCAPED KEYS (commas escaped)
-            report,      # REPORT
-            esccycl,     # CYCLE KEYS (commas escaped)
-            escreps,     # CYCKE REPORTS (commas escaped)
-            description, # DESCRIPTION
-            nosuppress]  # NOSUPPRESS
-    stat = '-1'          # CYCLE STATE
-    return enc_def(args), stat
 
+Trait.known_traits.append(DeselectTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/restrictaccess.py
+
 # --------------------------------------------------------------------
-def moved_trait(image='moved.gif',
-                xoff = 36,
-                yoff = -38,
-                name = 'Mark Moved',
-                key  = key('M')):
-    '''Create a moved trait (VASSAL.counters.MovementMarkable)
+class RestrictAccessTrait(Trait):
+    ID = 'restrict'
+    def __init__(self,
+                 sides         = [],
+                 byPlayer      = False,
+                 noMovement    = True,
+                 description   = '',
+                 owner         = '',):
+        '''Create a layer trait (VASSAL.counter.Restricted)'''
+        super(RestrictAccessTrait,self).__init__()
+        encSides = ','.join(sides)
+        self.setType(sides         = encSides,
+                     byPlayer      = byPlayer,
+                     noMovement    = noMovement,
+                     description   = description)
+        self.setState(owner=owner)
 
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['markmoved',    # CODE
-            image,          # IMAGE
-            xoff, yoff,     # XOFF ; YOFF
-            name,           # MENU
-            key,            # KEY, MODIFIER
-            '']             # ?
-    stat = False          # MOVED?
-    return enc_def(args), stat
+Trait.known_traits.append(RestrictAccessTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/rotate.py
+
 # --------------------------------------------------------------------
-def trail_trait(key             = key('T'),
-                name            = 'Movement Trail',
-                localVisible    = False,
-                globalVisible   = False,
-                radius          = 10,
-                fillColor       = rgb(255,255,255),
-                lineColor       = rgb(0,0,0),
-                activeOpacity   = 100,
-                inactiveOpacity = 50,
-                edgesBuffer     = 20,
-                displayBuffer   = 30,
-                lineWidth       = 1,
-                turnOn          = '',
-                turnOff         = '',
-                reset           = '',
-                description     = ''):
-    ''' Create a movement trail trait ( VASSAL.counters.Footprint)
-    
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state 
-    '''
-    args = ['footprint',       # CODE
-            key,               # ENABLE KEY
-            name,              # MENU 
-            localVisible,      # LOCAL VISABLE
-            globalVisible,     # GLOBAL VISABLE
-            radius,            # RADIUS
-            fillColor,         # FILL COLOR
-            lineColor,         # LINE COLOR 
-            activeOpacity,     # ACTIVE OPACITY
-            inactiveOpacity,   # INACTIVE OPACITY
-            edgesBuffer,       # EDGES BUFFER
-            displayBuffer,     # DISPLAY BUFFER
-            lineWidth,         # LINE WIDTH 
-            turnOn,            # TURN ON KEY
-            turnOff,           # TURN OFF KEY
-            reset,             # RESET KEY
-            description]       # DESC
-    stat = [False,             # GLOBAL VIS
-            '',                # MAP
-            0]                 # POINTS (followed by [; [X,Y]*]
-    return enc_def(args), enc_def(stat)
+class RotateTrait(Trait):
+    ID = 'rotate'
+    def __init__(self,
+                 nangles          = 6,
+                 rotateCWKey      = key(']'),
+                 rotateCCWKey     = key('['),
+                 rotateCW         = 'Rotate CW',
+                 rotateCCW        = 'Rotate CCW',
+                 rotateRndKey     = '',
+                 rotateRnd        = '',
+                 name             = 'Rotate',
+                 description      = 'Rotate piece',
+                 rotateDirectKey  = '',
+                 rotateDirect     = '',
+                 directExpression = '',
+                 directIsFacing   = True,
+                 angle            = 0):
+        '''Create a Rotate trait'''
+        super(RotateTrait,self).__init__()
+        if nangles == 1:
+            self.setType(nangles          = nangles,
+                         rotateKey        = rotateCWKey,
+                         rotate           = rotateCW,
+                         rotateRndKey     = rotateRndKey,
+                         rotateRnd        = rotateRnd,
+                         name             = name,
+                         description      = description,
+                         rotateDirectKey  = rotateDirectKey,
+                         rotateDirect     = rotateDirect,
+                         directExpression = directExpression,
+                         directIsFacing   = directIsFacing)
+        else:
+            self.setType(nangles          = nangles,
+                         rotateCWKey      = rotateCWKey,
+                         rotateCCWKey     = rotateCCWKey,
+                         rotateCW         = rotateCW,
+                         rotateCCW        = rotateCCW,
+                         rotateRndKey     = rotateRndKey,
+                         rotateRnd        = rotateRnd,
+                         name             = name,
+                         description      = description,
+                         rotateDirectKey  = rotateDirectKey,
+                         rotateDirect     = rotateDirect,
+                         directExpression = directExpression,
+                         directIsFacing   = directIsFacing)
+            
+        self.setState(angle = int(angle) if nangles > 1 else float(angle))
 
+Trait.known_traits.append(RotateTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/stack.py
+
 # --------------------------------------------------------------------
-def delete_trait(name = 'Delete', key = key('D')):
-    '''Create a delete trait (VASSAL.counters.Delete)
+class StackTrait(Trait):
+    ID = 'stack'
+    def __init__(self,
+                 board     = '',
+                 x         = '',  
+                 y         = '',  
+                 pieceIds  = [],
+                 layer     = -1): 
+        '''Create a stack trait in a save file'''
+        self.setType()       # NAME
+        # print('Piece IDs:',pieceIds)
+        self.setState(board      = board,
+                      x          = x,
+                      y          = y,
+                      pieceIds   = ';'.join([str(p) for p in pieceIds]),
+                      layer      = f'@@{layer}')
+        
+Trait.known_traits.append(StackTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/mark.py
+
+# --------------------------------------------------------------------
+class MarkTrait(Trait):
+    ID = 'mark'
+    def __init__(self,name='',value=''):
+        '''Create a mark trait (static property)'''
+        super(MarkTrait,self).__init__()
+        self.setType(name = name)
+        self.setState(value = value)
+
+
+Trait.known_traits.append(MarkTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/mask.py
+
+# --------------------------------------------------------------------
+# Inset
+# obs;88,130;ag_hide_1.png;Reveal;I;?;sides:Argentine;Peek;;true;;
+# obs;88,130;ag_hide_1.png;Reveal;I;?;side:Argentine;;;true;;
+#
+# Peek
+# obs;88,130;ag_hide_1.png;Reveal;P89,130;?;sides:Argentine;Peek;;true;;
+#
+# Image
+#
+class MaskTrait(Trait):
+    ID = 'obs'
+    INSET = 'I'
+    BACKGROUND = 'B'
+    PEEK = 'P'
+    IMAGE = 'G'
+    INSET2 = '2'
+    PLAYER = 'player:'
+    SIDE = 'side:'
+    SIDES = 'sides:'
+    def __init__(self,
+                 keyCommand   = '',
+                 imageName    = '',
+                 hideCommand  = '',
+                 displayStyle = '',
+                 peekKey      = '',
+                 ownerImage   = '',
+                 maskedName   = '?',
+                 access       = '',#?
+                 peekCommand  = '',
+                 description  = '',
+                 autoPeek     = True,
+                 dealKey      = '',
+                 dealExpr     = ''):
+        '''Create a mark trait (static property)'''
+        super(MaskTrait,self).__init__()
+        disp = displayStyle
+        if displayStyle == self.PEEK:
+            disp += peekKey
+        elif displayStyle == self.IMAGE:
+            disp += ownerImage
+            
+        acc = self.PLAYER
+        if isinstance(access,list):
+            acc = self.SIDES + ':'.join(access)
+        elif access.startswith('player'):
+            acc = self.PLAYER
+        elif access.startswith('side'):
+            acc = self.SIDE
+                
+        self.setType(keyCommand   = keyCommand,
+                     imageImage   = imageName,
+                     hideCommand  = hideCommand,
+                     displayStyle = disp,
+                     maskedName   = maskedName,
+                     access       = acc, # ?
+                     peekCommand  = peekCommand,
+                     description  = description,
+                     autoPeek     = autoPeek,
+                     dealKey      = dealKey,
+                     dealExpr     = dealExpr)
+        self.setState(value='null')
+
+    @classmethod
+    def peekDisplay(cls,key):#Encoded key
+        return cls.PEEK + key
     
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['delete', name, key, '']
-    stat = '' # Nothing
-    return enc_def(args), stat
+    @classmethod
+    def peekImage(cls,ownerImage):
+        return cls.IMAGE + ownerImage
 
+    @classmethod
+    def sides(cls,*names):
+        return cls.SIDES+':'.join(names)
+
+Trait.known_traits.append(MaskTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/trail.py
+
 # --------------------------------------------------------------------
-def sendto_trait(mapName,
-                 boardName,
+class TrailTrait(Trait):
+    ID = 'footprint'
+    def __init__(self,
+                 key             = key('T'),
+                 name            = 'Movement Trail',
+                 localVisible    = False,
+                 globalVisible   = False,
+                 radius          = 10,
+                 fillColor       = rgb(255,255,255),
+                 lineColor       = rgb(0,0,0),
+                 activeOpacity   = 100,
+                 inactiveOpacity = 50,
+                 edgesBuffer     = 20,
+                 displayBuffer   = 30,
+                 lineWidth       = 1,
+                 turnOn          = '',
+                 turnOff         = '',
+                 reset           = '',
+                 description     = ''):        
+        ''' Create a movement trail trait ( VASSAL.counters.Footprint)'''
+        super(TrailTrait,self).__init__()
+        self.setType(key               = key,# ENABLE KEY
+                     name              = name,# MENU 
+                     localVisible      = localVisible,# LOCAL VISABLE
+                     globalVisible     = globalVisible,# GLOBAL VISABLE
+                     radius            = radius,# RADIUS
+                     fillColor         = fillColor,# FILL COLOR
+                     lineColor         = lineColor,# LINE COLOR 
+                     activeOpacity     = activeOpacity,# ACTIVE OPACITY
+                     inactiveOpacity   = inactiveOpacity,# INACTIVE OPACITY
+                     edgesBuffer       = edgesBuffer,# EDGES BUFFER
+                     displayBuffer     = displayBuffer,# DISPLAY BUFFER
+                     lineWidth         = int(lineWidth),# LINE WIDTH 
+                     turnOn            = turnOn,# TURN ON KEY
+                     turnOff           = turnOff,# TURN OFF KEY
+                     reset             = reset,# RESET KEY
+                     description       = description)       # DESC
+        self.setState(isGlobal  = False,
+                      map       = '',
+                      points    = 0,     # POINTS (followed by [; [X,Y]*]
+                      init      = False) 
+
+Trait.known_traits.append(TrailTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/delete.py
+
+# --------------------------------------------------------------------
+class DeleteTrait(Trait):
+    ID = 'delete'
+    def __init__(self,
+                 name   = 'Delete',
+                 key = key('D')):
+        '''Create a delete trait (VASSAL.counters.Delete)'''
+        super(DeleteTrait,self).__init__()
+        self.setType(name  = name,
+                     key   = key,
+                     dummy = '')
+        self.setState()
+
+Trait.known_traits.append(DeleteTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/sendto.py
+
+# --------------------------------------------------------------------
+class SendtoTrait(Trait):
+    ID = 'sendto'
+    LOCATION = 'L'
+    ZONE     = 'Z'
+    REGION   = 'R'
+    GRID     = 'G'
+    def __init__(self,
+                 mapName     = '',
+                 boardName   = '',
                  name        = '',
                  key         = key('E'),
                  restoreName = 'Restore',
@@ -1891,1772 +6198,5319 @@
                  xoff        = 1,
                  yoff        = 1,
                  description = '',
-                 destination = 'L',
+                 destination = LOCATION,
                  zone        = '',
                  region      = '',
                  expression  = '',
                  position    = ''):
-    '''Create a send to trait (VASSAL.counter.SendToLocation)
+        '''Create a send to trait (VASSAL.counter.SendToLocation)'''
+        self.setType(name           = name,# NAME
+                     key            = key,# KEY , MODIFIER
+                     mapName        = mapName,# MAP
+                     boardName      = boardName,# BOARD
+                     x              = x,
+                     y              = y,# X ; Y
+                     restoreName    = restoreName,# BACK
+                     restoreKey     = restoreKey,# KEY , MODIFIER
+                     xidx           = xidx,
+                     yidx           = yidx,# XIDX ; YIDX
+                     xoff           = xoff,
+                     yoff           = yoff,# XOFF ; YOFF
+                     description    = description,# DESC
+                     destination    = destination,# DEST type
+                     zone           = zone,# ZONE
+                     region         = region,# REGION
+                     expression     = expression,# EXPRESSION
+                     position       = position)                   # GRIDPOS
+        self.setState(backMap = '', backX = '', backY = '')
 
-    Parameters
-    ----------
-    side : str
-        Sub-map to move to
-    
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['sendto',              # CODE
-            name,           # NAME
-            key,              # KEY , MODIFIER
-            mapName,            # MAP
-            boardName,        # BOARD
-            x, y,              # X ; Y
-            restoreName,             # BACK
-            restoreKey,              # KEY , MODIFIER
-            xidx, yidx,            # XIDX ; YIDX
-            xoff, yoff,                  # XOFF ; YOFF
-            description, # DESC
-            destination,                   # DEST
-            zone,                    # ZONE
-            region,                    # REGION
-            expression,                    # EXPRESSION
-            position ]                   # GRIDPOS
-    stat = ['',                    # BACKMAP 
-            '',                    # X
-            '']                    # Y
-    return enc_def(args), enc_def(stat)
+Trait.known_traits.append(SendtoTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/moved.py
+
 # --------------------------------------------------------------------
-def basic_trait(name,
-                filename,     # Can be empty
-                gpid,         # Can be empty
-                cloneKey='',  # Deprecated
-                deleteKey=''):# Deprecated
-    '''Create a basic unit (VASSAL.counters.BasicPiece)
+class MovedTrait(Trait):
+    ID = 'markmoved'
+    def __init__(self,
+                 image      = 'moved.gif',
+                 xoff       = 36,
+                 yoff       = -38,
+                 name       = 'Mark moved',
+                 key        = key('M'),
+                 dummy      = ''  # Description
+                 # ignoreSame = True
+                 ):
+        '''Create a moved trait (VASSAL.counters.MovementMarkable)'''
+        super(MovedTrait,self).__init__()
+        self.setType(image    = image,
+                     xoff     = xoff,
+                     yoff     = yoff,
+                     name     = name,
+                     key      = key,
+                     dummy    = dummy, # Description
+                     # ignoreSame = ignoreSame
+                     )
+        self.setState(moved = False)
 
-    Parameters
-    ----------
-    main : dict
-        Information
-    gpid : int
-        Piece global identifier
-    image : bool
-        If true, add image
+Trait.known_traits.append(MovedTrait)
 
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''    
-    args = ['piece',    # CODE
-            cloneKey,   # CLONEKEY
-            deleteKey,  # DELETEKEY
-            filename,   # IMAGE  
-            name]       # NAME
-    stat = ['null',         # MAPID (possibly 'null')
-            0,              # X
-            0,              # Y
-            gpid,           # GLOBAL PIECE ID
-            0]              # PROPERTY COUNT (followed by [; KEY; VALUE]+)
-    return enc_def(args), enc_def(stat)
+#
+# EOF
+#
+# ====================================================================
+# From traits/skel.py
 
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/submenu.py
+
 # --------------------------------------------------------------------
-def enc_parts(*parts):
-    '''Encode parts of a full piece definition
+class SubMenuTrait(Trait):
+    ID = 'submenu'
+    def __init__(self,
+                 subMenu     = '',  # Title
+                 keys        = [],  # Keys
+                 description = ''):
+        '''Create a sub menu (VASSAL.counters.SubMenu)'''
+        self.setType(subMenu     = subMenu,   # CLONEKEY
+                     keys        = ','.join([k.replace(',',r'\,')
+                                             for k in keys]),
+                     description = description)
+        self.setState() # PROPERTY COUNT (followed by [; KEY; VALUE]+)
+    def setKeys(self,keys):
+        '''Set the keys'''
+        self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
+        
+Trait.known_traits.append(SubMenuTrait)
 
-    Each trait (VASSAL.counter.Decorator, VASSAL.counter.BasicPiece)
-    definition or state is separated by a litteral TAB character.
-    Beyond the first TAB separator, additional escape characters
-    (BACKSLAH) are added in front of the separator.  This is to that
-    VASSAL.utils.SequenceDecoder does not see consequitive TABs as a
-    single TAB.
+#
+# EOF
+#
+# ====================================================================
+# From traits/basic.py
 
-    Parameters
-    ----------
-    parts : tuple
-        The various trait definitions or states
+# --------------------------------------------------------------------
+class BasicTrait(Trait):
+    ID = 'piece'
+    def __init__(self,
+                 name      = '',
+                 filename  = '',  # Can be empty
+                 gpid      = '',  # Can be empty
+                 cloneKey  = '',  # Deprecated
+                 deleteKey = ''): # Deprecated
+        '''Create a basic unit (VASSAL.counters.BasicPiece)'''
+        self.setType(cloneKey  = cloneKey,   # CLONEKEY
+                     deleteKey = deleteKey,  # DELETEKEY
+                     filename  = filename,   # IMAGE  
+                     name      = name)       # NAME
+        self.setState(map        = 'null', # MAPID (possibly 'null')
+                      x          = 0,
+                      y          = 0,
+                      gpid       = gpid,
+                      properties = 0) # PROPERTY COUNT (followed by [; KEY; VALUE]+)
+        
+Trait.known_traits.append(BasicTrait)
 
-    Returns
-    -------
-    code : str
-        The encoded piece
-    '''
-    ret = ''
-    sep = r'	'
-    for i, p in enumerate(parts):
-        if i != 0:
-            ret += '\\'*(i-1) + sep
-        ret += str(p)
+#
+# EOF
+#
+# ====================================================================
+# From traits/trigger.py
 
-    return ret
-
 # --------------------------------------------------------------------
-def add_piece(typ, state):
-    '''Create and add piece command (VASSAL.command.AddPiece)
+class TriggerTrait(Trait):
+    ID      = 'macro'
+    WHILE   = 'while'
+    UNTIL   = 'until'
+    COUNTED = 'counted' # - Always one "do ... while"
+    def __init__(self,
+                 name            = '',
+                 command         = '', # Context menu name
+                 key             = '', # Context menu key
+                 property        = '', # Enable/Disable
+                 watchKeys       = [],
+                 actionKeys      = [], # What to do
+                 loop            = False,
+                 preLoop         = '', # Key
+                 postLoop        = '', # Key
+                 loopType        = COUNTED, # Loop type
+                 whileExpression = '',
+                 untilExpression = '',
+                 count           = 0,
+                 index           = False,
+                 indexProperty   = '',
+                 indexStart      = '',
+                 indexStep       = ''):
+        '''Create a layer trait (VASSAL.counter.Trigger)'''
+        super(TriggerTrait,self).__init__()
+        encWKeys = Trait.encodeKeys(watchKeys, ',')
+        encAKeys = Trait.encodeKeys(actionKeys,',')
+        self.setType(name            = name,            
+                     command         = command,          # Context menu name
+                     key             = key,              # Context menu key
+                     property        = property,         # Enable/Disable
+                     watchKeys       = encWKeys,       
+                     actionKeys      = encAKeys,         # What to do
+                     loop            = loop,            
+                     preLoop         = preLoop,          # Key
+                     postLoop        = postLoop,         # Key
+                     loopType        = loopType,         # Loop type
+                     whileExpression = whileExpression, 
+                     untilExpression = untilExpression, 
+                     count           = count,           
+                     index           = index,           
+                     indexProperty   = indexProperty,   
+                     indexStart      = indexStart,      
+                     indexStep       = indexStep)       
+        self.setState(state='')
 
-    Each part is separated by a SLASH ('/').  The first part is the
-    command type (PLUS '+') to perform.  Second is the IDENTIFIER,
-    possibly 'null'.  Then comes the fully encoded type of the piece,
-    followed by the state of the piece (also fully encoded).
+    def getActionKeys(self):
+        return Trait.decodeKeys(self['actionKeys'],',')
+    
+    def getWatchKeys(self):
+        return Trait.decodeKeys(self['watchKeys'],',')
+    
+    def setActionKeys(self,keys):
+        self['actionKeys'] = Trait.encodeKeys(keys,',')
+        
+    def setWatchKeys(self,keys):
+        self['watchKeys'] = Trait.encodeKeys(keys,',')
+        
+        
 
-    Parameters
-    ----------
-    typ : str
-        Encoded piece definition
-    state : str
-        Encoded state definition
+Trait.known_traits.append(TriggerTrait)
 
-    Returns
-    -------
-    cmd : str
-        The add command encoded
-    '''
-    args = ['+',      # Type
-            'null',   # IDENTIFER  
-            typ,      # DEFINITION
-            state]    # STATE 
-    return '/'.join(args)
+#
+# EOF
+#
+# ====================================================================
+# From traits/nonrect.py
 
 # --------------------------------------------------------------------
-def add_proto(typ,state):
-    '''Create an add prototype command.  This is the same as 'add_piece'
+class NonRectangleTrait(Trait):
+    ID      = 'nonRect2'
+    CLOSE   = 'c'
+    MOVETO  = 'm'
+    LINETO  = 'l'
+    CUBICTO = 'l'
+    QUADTO  = 'l'
+    def __init__(self,
+                 scale    = 1.,
+                 filename = '',
+                 path     = [],
+                 image    = None):
+        '''Create a NonRectangle trait (static property)'''
+        super(NonRectangleTrait,self).__init__()
+        l = []
+        if len(filename) > 0:
+            l.append(f'n{filename}')
 
-    Parameters
-    ----------
-    typ : str
-        Encoded piece definition
-    state : str
-        Encoded state definition
+        if len(path) <= 0:
+            path = self.getShape(image)
 
-    Returns
-    -------
-    cmd : str
-        The add command encoded
-    '''    
-    return add_piece(typ,state)
-                      
+        if len(path) > 0:
+            # print(path)
+            l += [f'{p[0]},{int(p[1])},{int(p[2])}' if len(p) > 2 else p
+                  for p in path]
+    
+        self.setType(scale = scale,
+                     code  = ','.join(l))
+        self.setState()
 
+    @classmethod
+    def getShape(cls,image):
+        if image is None:
+            return []
+
+        from PIL import Image
+        from io import BytesIO
+
+        code = []
+        with Image.open(BytesIO(image)) as img:
+            alp = img.getchannel('A') # Alpha channel
+            # Find least and largest non-transparent pixel in each row
+            rows  = []
+            w     = alp.width
+            h     = alp.height
+            bb    = alp.getbbox()
+            for r in range(bb[1],bb[3]):
+                ll, rr = bb[2], bb[0]
+                for c in range(bb[0],bb[2]):
+                    if alp.getpixel((c,r)) != 0:
+                        ll = min(c,ll)
+                        rr = max(c,rr)
+                rows += [[r-h//2,ll-w//2,rr-w//2]]
+                    
+            # Now produce the code - we start with the top line
+            code = [(cls.MOVETO,rows[0][1],rows[0][0]-1),
+                    (cls.LINETO,rows[0][2],rows[0][0]-1)]
+            
+            # Now loop  down right side of image
+            for c in rows:
+                last = code[-1]
+                if last[1] != c[2]:
+                    code += [(cls.LINETO, c[2], last[2])]
+                code += [(cls.LINETO, c[2], c[0])]
+                
+            # Now loop up left side of image
+            for c in rows[::-1]:
+                last = code[-1]
+                if last[1] != c[1]:
+                    code += [(cls.LINETO,c[1],last[2])]
+                code += [(cls.LINETO,c[1],c[0])]
+
+            # Terminate with close
+            code += [(cls.CLOSE)]
+
+        return code
+
+
+Trait.known_traits.append(NonRectangleTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/click.py
+
 # --------------------------------------------------------------------
-def add_proto_code(decs):
-    typs = [d[0] for d in decs]
-    stts = [d[1] for d in decs]
-    typ  = enc_parts(*typs)
-    stt  = enc_parts(*stts)
+class ClickTrait(Trait):
+    ID = 'button'
+    def __init__(self,
+                 key         = '',
+                 x           = 0,
+                 y           = 0,
+                 width       = 0,
+                 height      = 0,
+                 description = '',
+                 context     = False,
+                 whole       = True,
+                 version     = 1,
+                 points      = []):
+        '''Create a click trait (static property)'''
+        super(ClickTrait,self).__init__()
+        self.setType(key          = key,
+                     x            = x,
+                     y            = y,
+                     width        = width,
+                     height       = height,
+                     description  = description,
+                     context      = context,
+                     whole        = whole,
+                     version      = version,
+                     npoints      = len(points),
+                     points       = ';'.join([f'{p[0]};{p[1]}'
+                                              for p in points]))            
+        self.setState()
 
-    return add_proto(typ,stt)
 
-# ====================================================================
+Trait.known_traits.append(ClickTrait)
+
 #
-# Below specific to the export app
+# EOF
 #
 # ====================================================================
-def get_bb(buffer):
-    '''Get bounding box of image
+# From game.py
 
-    Parameters
-    ----------
-    buffer : bytes
-         The image bytes
+# --------------------------------------------------------------------
+class Game(Element):
+    TAG = Element.BUILD+'GameModule'
+    def __init__(self,build,node=None,
+                 name            = '',
+                 version         = '', 
+                 ModuleOther1    = "",
+                 ModuleOther2    = "",
+                 VassalVersion   = "3.6.7",
+                 description     = "",
+                 nextPieceSlotId = 20):
+        '''Create a new Game object
 
-    Returns
-    -------
-    ulx, uly, lrx, lry : tuple
-         The coordinates of the bounding box 
-    '''
-    from PIL import Image
-    from io import BytesIO
+        Parameters
+        ----------
+        build : xml.dom.Document
+            root note
+        node : xml.dom.Node
+            To read from, or None
+        name : str
+            Name of module
+        version : str
+            Version of module
+        ModuleOther1 : str
+            Free form string 
+        ModuleOther2 : str
+            Free form string
+        VassalVersion : str
+            VASSAL version this was created for
+        description : str
+            Speaks volumes
+        nextPieceSlotId : int
+            Starting slot ID.
+        '''
+        super(Game,self).__init__(build, self.TAG,
+                                  node            = node,
+                                  name            = name,
+                                  version         = version,
+                                  ModuleOther1    = ModuleOther1,
+                                  ModuleOther2    = ModuleOther2,
+                                  VassalVersion   = VassalVersion,
+                                  description     = description,
+                                  nextPieceSlotId = nextPieceSlotId)
+    def nextPieceSlotId(self):
+        '''Increment next piece slot ID'''
+        ret = int(self.getAttribute('nextPieceSlotId'))
+        self.setAttribute('nextPieceSlotId',str(ret+1))
+        return ret
+    #
+    def addBasicCommandEncoder(self,**kwargs):
+        '''Add a `BasicCommandEncoder` element to this
 
-    with Image.open(BytesIO(buffer)) as img:
-        bb  = img.getbbox()
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BasicCommandEncoder
+            The added element
+        '''
+        return self.add(BasicCommandEncoder,**kwargs)
+    def addGlobalTranslatableMessages(self,**kwargs):
+        '''Add a `GlobalTranslatableMessages` element to this
 
-    return bb
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : GlobalTranslatableMessages
+            The added element
+        '''
+        return self.add(GlobalTranslatableMessages,**kwargs)
+    def addPlayerRoster(self,**kwargs):
+        '''Add a `PlayerRoster` element to this
 
-# ====================================================================
-def piece_body(family,main,flipped,gpid):
-    '''Create an full add piece command from the side, image, and ID
-    information.  This adds (in reverse order)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PlayerRoster
+            The added element
+        '''
+        return self.add(PlayerRoster,**kwargs)
+    def addChessClock(self,**kwargs):
+        '''Add a `ChessClockControl` element to this
 
-    - BasicPiece
-    - Side prototype
-    - Flip layer (if 'flipped' is not none)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PlayerRoster
+            The added element
+        '''
+        return self.add(ChessClockControl,**kwargs)
+    def addLanguage(self,**kwargs):
+        '''Add a `Language` element to this
 
-    Parameters
-    ----------
-    family : str
-        Side in the game
-    main : dict
-        Main image information
-    flipped : dict
-        Flipped image information, possibly None
-    gpid : int
-        Global piece identifier
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Language
+            The added element
+        '''
+        return self.add(Language,**kwargs)
+    def addChatter(self,**kwargs):
+        '''Add a `Chatter` element to this
 
-    Returns
-    -------
-    cmd : str
-        The command to add the piece
-    '''
-    # Create the layer, prototype, and basic piece traits
-    decs = []
-    if flipped is not None:
-        imgs = [ main['filename'], flipped['filename'] ]
-        decs.extend([ layer_trait(images=imgs,
-                                  newNames=['','Reduced +'],
-                                  activateName = '',
-                                  decreaseName = '',
-                                  increaseName = 'Flip',
-                                  increaseKey  = key('F'),
-                                  decreaseKey  = '',
-                                  name='Step'),
-                      report_trait(key('F')) ])
-    for m in main.get('mains',[]):
-        decs.append(prototype_trait(m + ' prototype'))
-    ech = main.get('echelon',None)
-    cmd = main.get('command',None)
-    if ech is not None: decs.append(prototype_trait(ech + ' prototype'))
-    if cmd is not None: decs.append(prototype_trait(cmd + ' prototype'))
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chatter
+            The added element
+        '''
+        return self.add(Chatter,**kwargs)
+    def addKeyNamer(self,**kwargs):
+        '''Add a `KeyNamer` element to this
 
-    decs.extend([ prototype_trait(family+' prototype'),
-                  basic_trait(main['name'], main['filename'],gpid)
-                 ])
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : KeyNamer
+            The added element
+        '''
+        return self.add(KeyNamer,**kwargs)
+    def addNotes(self,**kwargs):
+        '''Add a `Notes` element to this
 
-    typs = [d[0] for d in decs]
-    stts = [d[1] for d in decs]
-    typ  = enc_parts(*typs)
-    stt  = enc_parts(*stts)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Notes
+            The added element
+        '''
+        return self.add(Notes,**kwargs)
+    def addLanguage(self,**kwargs):
+        '''Add a `Language` element to this
 
-    return add_piece(typ,stt)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Language
+            The added element
+        '''
+        return self.add(Language,**kwargs)
+    def addChatter(self,**kwargs):
+        '''Add a `Chatter` element to this
 
-# --------------------------------------------------------------------
-def proto_body(family):
-    '''Create an full add prototype com from the side information.
-    This adds (in reverse order)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chatter
+            The added element
+        '''
+        return self.add(Chatter,**kwargs)
+    def addKeyNamer(self,**kwargs):
+        '''Add a `KeyNamer` element to this
 
-    - An empty BasicPiece
-    - An eliminate (or sendto) trait
-    - A delete trait
-    - A mark moved trait
-    - A move trail trait 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : KeyNamer
+            The added element
+        '''
+        return self.add(KeyNamer,**kwargs)
+    def addGlobalProperties(self,**kwargs):
+        '''Add a `GlobalProperties` element to this
 
-    Parameters
-    ----------
-    family : str
-        Side in the game
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : GlobalProperties
+            The added element
+        '''
+        return self.add(GlobalProperties,**kwargs)
+    def addGlobalOptions(self,**kwargs):
+        '''Add a `GlobalOptions` element to this
 
-    Returns
-    -------
-    cmd : str
-        The command to add the piece
-    '''
-    # Dummy image 
-    decs = [ report_trait(key('E'),key('R'),key('M')),
-             moved_trait(),
-             trail_trait(),
-             delete_trait(),
-             sendto_trait('DeadMap',family+' pool','Eliminate',key('E'),
-                          'Restore',key('R'),description="Eliminate unit"),
-             mark_trait('Faction',family),
-             basic_trait('','','') ]
-    typs = [d[0] for d in decs]
-    stts = [d[1] for d in decs]
-    typ  = enc_parts(*typs)
-    stt  = enc_parts(*stts)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : GlobalOptions
+            The added element
+        '''
+        return self.add(GlobalOptions,**kwargs)
+    def addTurnTrack(self,**kwargs):
+        '''Add a `TurnTrack` element to this
 
-    return add_proto(typ,stt)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : TurnTrack
+            The added element
+        '''
+        return self.add(TurnTrack,**kwargs)
+    def addDocumentation(self,**kwargs):
+        '''Add a `Documentation` element to this
 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Documentation
+            The added element
+        '''
+        return self.add(Documentation,**kwargs)
+    def addPrototypes(self,**kwargs):
+        '''Add a `Prototypes` element to this
 
-# --------------------------------------------------------------------
-def add_splash(root,doc,categories,title='',verbose=False):
-    '''Add an about page
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Prototypes
+            The added element
+        '''
+        return self.add(Prototypes,**kwargs)
+    def addPieceWindow(self,**kwargs):
+        '''Add a `PieceWindow` element to this
 
-    Parametersg
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    doc : xml.dom.minidom.Element
-        The documentation element
-    categories : dict
-        Catalog of images
-    title : str
-        Title of the game 
-    '''
-    fronts    = categories.get('front',{}).get('all',[])
-    front     = list(fronts.values())[0] if len(fronts) > 0 else None
-    if front is None:
-        return
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PieceWindow
+            The added element
+        '''
+        return self.add(PieceWindow,**kwargs)
+    def addChartWindow(self,**kwargs):
+        '''Add a `ChartWindow` element to this
 
-    if verbose:
-        print(f'Adding about page with {title}')
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ChartWindow
+            The added element
+        '''
+        return self.add(ChartWindow,**kwargs)
+    def addInventory(self,**kwargs):
+        '''Add a `Inventory` element to this
 
-    add_about(root,doc, f'About {title}', fileName = front['filename'])
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Inventory
+            The added element
+        '''
+        return self.add(Inventory,**kwargs)
+    def addMap(self,**kwargs):
+        '''Add a `Map` element to this
 
-# --------------------------------------------------------------------
-def add_rules(root,doc,zipfile,rules,verbose=False):
-    '''Add rules to the module
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Map
+            The added element
+        '''
+        return self.add(Map,**kwargs)
+    def addDiceButton(self,**kwargs):
+        '''Add a `DiceButton` element to this
 
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    doc : xml.dom.minidom.Element
-        The documentation element
-    zipfile : zipfile.ZipFile
-        The module archive 
-    rules : str
-        File name of rules (PDF)
-    '''
-    if rules is None or rules == '':
-        return
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : DiceButton
+            The added element
+        '''
+        return self.add(DiceButton,**kwargs)
+    def addPredefinedSetup(self,**kwargs):
+        '''Add a `PredefinedSetup` element to this
 
-    if verbose:
-        print('Adding rules PDF to module')
-        
-    zipfile.write(rules,'rules.pdf')
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PredefinedSetup
+            The added element
+        '''
+        return self.add(PredefinedSetup,**kwargs)
+    def addGameMassKey(self,**kwargs):
+        '''Add a `GameMassKey` element to this
 
-    add_pdffile(root,doc,title='Show rules',pdfFile='rules.pdf')
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : GameMassKey
+            The added element
+        '''
+        return self.add(GameMassKey,**kwargs)
+    def addStartupMassKey(self,**kwargs):
+        '''Add a `StartupMassKey` element to this
 
-# --------------------------------------------------------------------
-def add_notes(root,doc,zipfile,title,version,sides,pdffile,infofile,rules,
-              verbose=False):
-    '''Add rules to the module
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : StartupMassKey
+            The added element
+        '''
+        return self.add(StartupMassKey,**kwargs)
+    def addMenu(self,**kwargs):
+        '''Add a `Menu` element to this
 
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    doc : xml.dom.minidom.Element
-        The documentation element
-    zipfile : zipfile.ZipFile    
-        The module archive
-    sides : list
-        Sides in the game
-    pdffile : str
-        The images PDF
-    infofile : str
-        The images information    
-    rules : str
-        File name of rules (PDF)
-    '''
-    factions = "".join([f'<li>{s}</li>' for s in sides])
-    notes = f'''
-    <html>
-    <body>
-    <h1>Draft VASSAL module of {title} (Version {version})</h1>
-    <p>
-     This is a draft VASSAL module of {title} generated from
-    </p>
-    <ul>
-     <li>images in {pdffile}, and</li>
-     <li>information in {infofile}</li>
-    </ul>
-    <p>
-     both generated by LaTeX using the <b>wargame</b> [1] package, and
-     processed by the Python script <code>wgexport.py</code> from that
-     package.
-    </p>
-    <p>
-     The module is <i>not</i> complete and the developer should edit
-     the module as needed in the VASSAL module editor.
-    </p>
-    <p>
-     [1] <code>https://gitlab.com/wargames_tex/wargame_tex</code>
-    </p>
-    <h2>Content of the module</h2>
-    <p>
-     The <code>wgexport.py</code> Python script has identified the
-     factions of the game to be
-    </p>
-    <ul>
-    '''+factions+f'''
-    </ul>
-    <p>
-     and generated prototype pieces for these (along with possible
-     non-faction pieces.  The script also generated a map ("dead map")
-     that contains boards for each faction and is meant to store
-     eliminated units ("<faction> pool").  These pools have a
-     generic shape and colour, and the developer should take care to
-     correct these as needed.
-    </p>
-    <p>
-     Boards of the game present in the {pdffile} and {infofile} files
-     have been generated.  Each board has a single zone in it that
-     encompasses the full board, and a hex grid is added to that
-     zone. The module developer should adjust the zone and embedded
-     hex grid according to the actual map, using the VASSAL module
-     editor.  The hex grid (as well as coordinates) is drawn by
-     default so as to make this obvious to the developer.  The hex
-     sizes should be correct, and mainly the offset needs to be
-     adjusted.
-    </p>
-    <p>
-     Images marked as OOBs have also been added as maps to the
-     module. These also have a single zone with a rectangular grid
-     imposed in it.  This grid, and the grid coordinates are drawn by
-     default, so it is easier to see and to remember to adjust it. The
-     module developer should adjust this, possibly adding zones, so
-     that it matches up with the graphics.  The developer can add
-     units to the OOB via 'At-start' stacks in the VASSAL editor.
-    </p>
-    <p>
-     All pieces identified by {infofile} and present in {pdffile} have
-     been generated.  Double sided pieces (identified by images which
-     also has a suffix-"flipped" image) are created as such.
-    </p>
-    <p>
-     Charts identified by the {infofile} and {pdffile} have been added
-     to the module.  The are added as separate (pop-up) frames, but
-     the module developer can change this in the VASSAL module editor.
-    </p>
-    <p>
-     If the {infofile} and {pdffile} identified a front page, then that
-     has been added to module as the splash screen.
-    </p>
-    <p>
-     If a PDF rules file ({rules}) was passed to the Python script,
-     then that has been included in the module and is accessible from
-     the "Help" menu.
-    </p>
-    <h2>After finalising the module</h2>
-    <p>
-     After the developer has finalised the module (added more pieces,
-     fixed board zones a and grids, add in more commands), then this
-     Help menu entry should be deleted.
-    </p>
-    </body>
-    </html>
-    '''
-    if version.lower() == 'draft':
-        if verbose:
-            print(f' Adding draft notes {version}')
-        zipfile.writestr('help/notes.html',notes)
-        add_helpfile(root,doc,title='Draft Module notes',
-                     fileName='help/notes.html')
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Menu
+            The added element
+        '''
+        return self.add(Menu,**kwargs)
+    def addSymbolicDice(self,**kwargs):
+        '''Add a `SymbolicDice` element to this
 
-    keys = f'''
-    <html>
-    <body>
-    <h1>{title} (Version {version}) Key bindings</h1>
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : SymbolicDice
+            The added element
+        '''
+        return self.add(SymbolicDice,**kwargs)
     
-    <table>
-     <tr>
-      <th>Key   </th>
-      <th>Where </th>
-      <th>Effect</th>
-     </tr>
-     <tr>
-      <td>Alt-A</td>
-      <td>-</td>
-      <td>Show the charts panel</td>
-     </tr>
-     <tr>
-      <td>Alt-B</td>
-      <td>-</td>
-      <td>Show the OOBs</td>
-     </tr>
-     <tr>
-      <td>Alt-C</td>
-      <td>-</td>
-      <td>Show the counters panel</td>
-     </tr>
-     <tr>
-      <td>Alt-E</td>
-      <td>-</td>
-      <td>Show the eliminated units</td>
-     </tr>
-     <tr>
-      <td>Alt-I</td>
-      <td>-</td>
-      <td>Show/refresh inventory window</td>
-     </tr>
-     <tr>
-      <td>Alt-M</td>
-      <td>-</td>
-      <td>Show map</td>
-     </tr>
-     <tr>
-      <td>Alt-T</td>
-      <td>-</td>
-      <td>Increase turn track</td>
-     </tr>
-     <tr>
-      <td>Alt-Shift-T</td>
-      <td>-</td>
-      <td>Decrease turn track</td>
-     </tr>
-     <tr>
-      <td>Alt-6</td>
-      <td>-</td>
-      <td>Roll the dice</td>
-     </tr>
-     <tr>
-      <td>Ctrl-D</td>
-      <td>Board,Counter</td>
-      <td>Delete selected counters</td>
-     </tr>
-     <tr>
-      <td>Ctrl-E</td>
-      <td>Board,Counter</td>
-      <td>Eliminate selected counters</td>
-     </tr>
-     <tr>
-      <td>Ctrl-F</td>
-      <td>Board,Counter</td>
-      <td>Flip selected counters</td>
-     </tr>
-     <tr>
-      <td>Ctrl-M</td>
-      <td>Board,Counter</td>
-      <td>Toggle "moved" markers on selected counters</td>
-     </tr>
-     <tr>
-      <td>Ctrl-O</td>
-      <td>Board</td>
-      <td>Hide/show counters</td>
-     </tr>
-     <tr>
-      <td>Ctrl-R</td>
-      <td>Board,Counter</td>
-      <td>Restore unit (from dead pool)</td>
-     </tr>
-     <tr>
-      <td>Ctrl-T</td>
-      <td>Board,Counter</td>
-      <td>Toggle move trail</td>
-     </tr>
-     <tr>
-      <td>Ctrl-+</td>
-      <td>Board</td>
-      <td>Zoom in</td>
-     </tr>
-     <tr>
-      <td>Ctrl--</td>
-      <td>Board</td>
-      <td>Zoom out</td>
-     </tr>
-     <tr>
-      <td>Ctrl-=</td>
-      <td>Board</td>
-      <td>Select zoom</td>
-     </tr>
-     <tr>
-      <td>Ctrl-Shift-O</td>
-      <td>Board</td>
-      <td>Show overview map</td>
-     </tr>
-     <tr>
-      <td>←,→,↑↓</td>
-      <td>Board</td>
-      <td>Scroll board left, right, up, down (slowly)</td>
-     </tr>
-     <tr>
-      <td>PnUp,PnDn</td>
-      <td>Board</td>
-      <td>Scroll board up/down (fast)</td>
-     </tr>
-     <tr>
-      <td>Ctrl-PnUp,Ctrl-PnDn</td>
-      <td>Board</td>
-      <td>Scroll board left/right (fast)</td>
-     </tr>
-     <tr>
-      <td>Mouse-scroll up/down</td>
-      <td>Board</td>
-      <td>Scroll board up//down</td>
-     </tr>
-     <tr>
-      <td>Shift-Mouse-scroll up/down</td>
-      <td>Board</td>
-      <td>Scroll board right/leftown</td>
-     </tr>
-     <tr>
-      <td>Ctrl-Mouse-scroll up/down</td>
-      <td>Board</td>
-      <td>Zoom board out/in</td>
-     </tr>
-     <tr>
-      <td>Mouse-2</td>
-      <td>Board</td>
-      <td>Centre on mouse</td>
-     </tr>
-    </table>
-    </body>
-    </html>    
-    '''
-    if verbose:
-        print(f'Add key bindings help file')
-    zipfile.writestr('help/keys.html',keys)
-    add_helpfile(root,doc,title='Key bindings',
-                 fileName='help/keys.html')
     
-    return notes
+    # ----------------------------------------------------------------
+    def getGlobalProperties(self,single=True):
+        '''Get all or a sole `GlobalPropertie` element(s) from this
 
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `GlobalPropertie` child, otherwise fail.
+            If `False` return all `GlobalPropertie` children in this element
+        Returns
+        -------
+        children : list
+            List of `GlobalPropertie` children (even if `single=True`)
+        '''
+        return self.getAllElements(GlobalProperties,single)
+    def getBasicCommandEncoder(self,single=True):
+        '''Get all or a sole `Ba` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Ba` child, otherwise fail.
+            If `False` return all `Ba` children in this element
+        Returns
+        -------
+        children : list
+            List of `Ba` children (even if `single=True`)
+        '''
+        return self.getAllElements(BasicCommandEncoder,single)
+    def getGlobalTranslatableMessages(self,single=True):
+        '''Get all or a sole `GlobalTranslatableMessage` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `GlobalTranslatableMessage` child, otherwise fail.
+            If `False` return all `GlobalTranslatableMessage` children in this element
+        Returns
+        -------
+        children : list
+            List of `GlobalTranslatableMessage` children (even if `single=True`)
+        '''
+        return self.getAllElements(GlobalTranslatableMessages,single)
+    def getLanguages(self,single=False):
+        '''Get all or a sole `Language` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Language` child, otherwise fail.
+            If `False` return all `Language` children in this element
+        Returns
+        -------
+        children : list
+            List of `Language` children (even if `single=True`)
+        '''
+        return self.getAllElements(Language,single)
+    def getChessClocks(self,asdict=False):
+        '''Get all or a sole `Language` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Language` child, otherwise fail.
+            If `False` return all `Language` children in this element
+        Returns
+        -------
+        children : list
+            List of `Language` children (even if `single=True`)
+        '''
+        return self.getElementsByKey(ChessClockControl,'name',asdict)
+    def getChatter(self,single=True):
+        '''Get all or a sole `Chatter` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Chatter` child, otherwise fail.
+            If `False` return all `Chatter` children in this element
+        Returns
+        -------
+        children : list
+            List of `Chatter` children (even if `single=True`)
+        '''
+        return self.getAllElements(Chatter,single)
+    def getKeyNamer(self,single=True):
+        '''Get all or a sole `KeyNamer` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `KeyNamer` child, otherwise fail.
+            If `False` return all `KeyNamer` children in this element
+        Returns
+        -------
+        children : list
+            List of `KeyNamer` children (even if `single=True`)
+        '''
+        return self.getAllElements(KeyNamer,single)
+    def getDocumentation(self,single=True):
+        '''Get all or a sole `Documentation` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Documentation` child, otherwise fail.
+            If `False` return all `Documentation` children in this element
+        Returns
+        -------
+        children : list
+            List of `Documentation` children (even if `single=True`)
+        '''
+        return self.getAllElements(Documentation,single)
+    def getPrototypes(self,single=True):
+        '''Get all or a sole `Prototypes` (i.e., the containers of
+        prototypes, not a list of actual prototypes) element(s) from
+        this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Prototypes` child, otherwise fail.
+            If `False` return all `Prototypes` children in this element
+        Returns
+        -------
+        children : list
+            List of `Prototype` children (even if `single=True`)
+
+        '''
+        return self.getAllElements(Prototypes,single)
+    def getPlayerRoster(self,single=True):
+        '''Get all or a sole `PlayerRo` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `PlayerRo` child, otherwise fail.
+            If `False` return all `PlayerRo` children in this element
+        Returns
+        -------
+        children : list
+            List of `PlayerRo` children (even if `single=True`)
+        '''
+        return self.getAllElements(PlayerRoster,single)
+    def getGlobalOptions(self,single=True):
+        '''Get all or a sole `GlobalOption` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `GlobalOption` child, otherwise fail.
+            If `False` return all `GlobalOption` children in this element
+        Returns
+        -------
+        children : list
+            List of `GlobalOption` children (even if `single=True`)
+        '''
+        return self.getAllElements(GlobalOptions,single)
+    def getInventories(self,asdict=True):
+        '''Get all Inventorie element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Inventorie` elements.  If `False`, return a list of all Inventorie` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Inventorie` children
+        '''
+        return self.getElementsByKey(Inventory,'name',single)
+    def getPieceWindows(self,asdict=True):
+        '''Get all PieceWindow element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `PieceWindow` elements.  If `False`, return a list of all PieceWindow` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `PieceWindow` children
+        '''
+        return self.getElementsByKey(PieceWindow,'name',asdict)
+    def getChartWindows(self,asdict=True):
+        '''Get all ChartWindow element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `ChartWindow` elements.  If `False`, return a list of all ChartWindow` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `ChartWindow` children
+        '''
+        return self.getElementsByKey(ChartWindow,'name',asdict)
+    def getDiceButtons(self,asdict=True):
+        '''Get all DiceButton element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `DiceButton` elements.  If `False`, return a list of all DiceButton` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `DiceButton` children
+        '''
+        return self.getElementsByKey(DiceButton,'name',asdict)
+    def getPredefinedSetups(self,asdict=True):
+        '''Get all PredefinedSetup element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `PredefinedSetup` elements.  If `False`, return a list of all PredefinedSetup` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `PredefinedSetup` children
+        '''
+        return self.getElementsByKey(PredefinedSetup,'name',asdict)
+    def getNotes(self,single=True):
+        '''Get all or a sole `Note` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Note` child, otherwise fail.
+            If `False` return all `Note` children in this element
+        Returns
+        -------
+        children : list
+            List of `Note` children (even if `single=True`)
+        '''
+        return self.getAllElements(Notes,single)
+    def getTurnTracks(self,asdict=True):
+        '''Get all TurnTrack element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `TurnTrack` elements.  If `False`, return a list of all TurnTrack` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `TurnTrack` children
+        '''
+        return self.getElementsByKey(TurnTrack,'name',asdict)
+    def getPieces(self,asdict=False):
+        '''Get all Piece element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Piece` elements.  If `False`, return a list of all Piece` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Piece` children
+        '''
+        return self.getElementsByKey(PieceSlot,'entryName',asdict)
+    def getSpecificPieces(self,*names,asdict=False):
+        '''Get all SpecificPiece element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `SpecificPiece` elements.  If `False`, return a list of all SpecificPiece` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `SpecificPiece` children
+        '''
+        return self.getSpecificElements(PieceSlot,'entryName',
+                                        *names,asdict=asdict)
+    def getMap(self,asdict=False):
+        return self.getElementsByKey(Map,'mapName',asdict)
+    def getWidgetMaps(self,asdict=True):
+        '''Get all WidgetMap element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `WidgetMap` elements.  If `False`, return a list of all WidgetMap` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `WidgetMap` children
+        '''
+        return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict)
+    def getMaps(self,asdict=True):
+        '''Get all Map element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Map` elements.  If `False`, return a list of all Map` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Map` children
+        '''
+        maps = self.getMap(asdict=asdict)
+        wmaps = self.getWidgetMaps(asdict=asdict)
+        if asdict:
+            maps.update(wmaps)
+        else:
+            maps.extend(wmaps)
+        return maps
+    def getBoards(self,asdict=True):
+        '''Get all Board element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board`
+            elements.  If `False`, return a list of all Board`
+            children.
+
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+
+        '''
+        return self.getElementsByKey(Board,'name',asdict)
+    def getGameMassKeys(self,asdict=True):
+        '''Get all GameMassKey element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board`
+            elements.  If `False`, return a list of all Board`
+            children.
+
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+
+        '''
+        return self.getElementsByKey(GameMassKey,'name',asdict)
+    def getStartupMassKeys(self,asdict=True):
+        '''Get all StartupMassKey element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board`
+            elements.  If `False`, return a list of all Board`
+            children.
+
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+
+        '''
+        return self.getElementsByKey(StartupMassKey,'name',asdict)
+    def getMenus(self,asdict=True):
+        '''Get all Menu element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board`
+            elements.  If `False`, return a list of all Board`
+            children.
+
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+
+        '''
+        return self.getElementsByKey(Menu,'text',asdict)
+    def getSymbolicDices(self,asdict=True):
+        '''Get all Menu element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `SymbolicDice`
+            elements.  If `False`, return a list of all `SymbolicDice`
+            children.
+
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `SymbolicDice` children
+
+        '''
+        return self.getElementsByKey(SymbolicDice,'name',asdict)
+    def getAtStarts(self,single=False):
+        '''Get all or a sole `AtStart` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `AtStart` child, otherwise fail.
+            If `False` return all `AtStart` children in this element
+        Returns
+        -------
+        children : list
+            List of `AtStart` children (even if `single=True`)
+        '''
+        return self.getAllElements(AtStart,single)
+
+
 # --------------------------------------------------------------------
-def add_tut(root,doc,zipfile,tutorial,verbose=False):
-    if tutorial is None:
-        return None
+class BasicCommandEncoder(GameElement):
+    TAG = Element.MODULE+'BasicCommandEncoder'
+    def __init__(self,doc,node=None):
+        super(BasicCommandEncoder,self).__init__(doc,self.TAG,node=node)
 
-    if verbose:
-        print(f'Adding tutorial {tutorial}')
-    zipfile.write(tutorial,'tutorial.vlog')
-    
-    return add_tutorial(root,doc,name='Tutorial',logfile='tutorial.vlog')
+#
+# EOF
+#
+# ====================================================================
+# From buildfile.py
 
 # --------------------------------------------------------------------
-def add_basics_to_map(root,map,**attr):
-    '''Add a map to the document
+class BuildFile(Element):
+    def __init__(self,root=None):
+        '''Construct from a DOM object, if given, otherwise make new'''
+        from xml.dom.minidom import Document
+        super(BuildFile,self).__init__(None,'',None)
+        
+        self._root = root
+        if self._root is None:
+            self._root = Document()
 
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    parent : xml.dom.minidom.Element
-        The parent element
-    button : str
-        Button text
-    name : str
-        Name of the map
-    stacking : bool
-        Whether to enable stacking
+        self._node = self._root
 
-    Returns
-    -------
-    map : xml.dom.minidom.Element
-        The map element
-    bpicker : xml.dom.minidom.Element
-        The board picker in map 
-    '''
-    stacking = add_stacking(root,map)
-    add_camera               (root,map)
-    add_forwardtochatter     (root,map)
-    add_menudisplayer        (root,map)
-    add_mapcenterer          (root,map)
-    add_stackexpander        (root,map)
-    add_piecemover           (root,map)
-    add_keybufferer          (root,map)
-    add_selectionhighlighters(root,map) # TBD
-    add_highlightlastmoved   (root,map)
-    add_zoomer               (root,map)
+    def addGame(self,**kwargs):
+        '''Add a `Game` element to this
 
-    return stacking
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Game
+            The added element
+        '''
+        return Game(self,**kwargs)
 
-# --------------------------------------------------------------------
-def add_elim(root,game,sides,categories,verbose=False):
-    '''Add an eliminated map
+    def getGame(self):
+        '''Get the `Game`''' 
+        return Game(self,
+                    node=self._root.\
+                    getElementsByTagName('VASSAL.build.GameModule')[0])
 
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    game : xml.dom.minidom.Element
-        The parent element
-    sides : list of str
-        List of sides in the game
-    '''
-    if verbose:
-        print(' Adding eliminated map and boards')
+    def encode(self):
+        '''Encode into XML'''
+        return self._root.toprettyxml(indent=' ',
+                                      encoding="UTF-8",
+                                      standalone=False)
 
-    if sides is None or len(sides) == 0:
-        return
+
+#
+# EOF
+#
+# ====================================================================
+# From moduledata.py
+
+# --------------------------------------------------------------------
+class Data(Element):
+    TAG = 'data'
+    def __init__(self,doc,node=None,version='1'):
+        super(Data,self).__init__(doc,self.TAG,node=node,version=version)
         
-    map = add_map(root,game,'DeadMap',
-                  buttonName    = '', #'Eliminated units',
-                  icon          = '/images/playerAway.gif',
-                  markMoved     = 'Never',
-                  launch        = True,
-                  allowMultiple = True,
-                  hotkey=key('E',ALT))
-    bpicker = add_boardpicker(root,map,
-                              slotHeight = 400,
-                              slotWidth  = 400)
-    add_setup(root,bpicker,'DeadMap',2,[s+' pool' for s in sides])
-    add_masskey(root,map,'Restore',
-                key('R'),
-                key('R'),
-                icon         = '/images/Undo16.gif',
-                tooltip      = "Restore selected units")
-    
-    for i,s in enumerate(sides):
-        if verbose:
-            print(f'  Adding {s} pool')
+    def addVersion(self,**kwargs):
+        '''Add a `Version` element to this
 
-        color        = [0,0,0,64]
-        color[i % 3] = 255
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Version
+            The added element
+        '''
+        return self.add(Version,**kwargs)
+    def addVASSALVersion(self,**kwargs):
+        '''Add a `VASSALVersion` element to this
 
-        w     = 400
-        h     = 400
-        c     = ','.join([str(c) for c in color])
-        dimg  = categories.get('pool',{}).get('all',{}).get(s,None)
-        img   = ''
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : VASSALVersion
+            The added element
+        '''
+        return self.add(VASSALVersion,**kwargs)
+    def addName(self,**kwargs):
+        '''Add a `Name` element to this
 
-        if dimg:
-            bb  = get_bb(dimg['img'])
-            w   = bb[2] - bb[0]
-            h   = bb[3] - bb[1]
-            c   = ''
-            img = dimg['filename']
-            if verbose:
-                print(f'   Using image provided by the user: {img}')
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Name
+            The added element
+        '''
+        return self.add(Name,**kwargs)
+    def addDescription(self,**kwargs):
+        '''Add a `Description` element to this
 
-        add_board(root,bpicker,f'{s} pool',
-                  image = img,
-                  color = c,
-                  width = w,
-                  height = h)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Description
+            The added element
+        '''
+        return self.add(Description,**kwargs)
+    def addDateSaved(self,**kwargs):
+        '''Add a `DateSaved` element to this
 
-    add_basics_to_map(root,map)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : DateSaved
+            The added element
+        '''
+        return self.add(DateSaved,**kwargs)
+    def getVersion(self,single=True):
+        '''Get all or a sole `Version` element(s) from this
 
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Version` child, otherwise fail.
+            If `False` return all `Version` children in this element
+        Returns
+        -------
+        children : list
+            List of `Version` children (even if `single=True`)
+        '''
+        return self.getAllElements(Version,single=single)
+    def getVASSALVersion(self,single=True):
+        '''Get all or a sole `VASSALVersion` element(s) from this
+
+        Parameters
+        ----------
+        single : bool        
+            If `True`, there can be only one `VASSALVersion` child,
+            otherwise fail.  If `False` return all `VASSALVersion`
+            children in this element
+        Returns
+        -------
+        children : list
+            List of `VASSALVersion` children (even if `single=True`)
+
+        '''
+        return self.getAllElements(VASSALVersion,single=single)
+    def getName(self,single=True):
+        return self.getAllElements(Name,single=single)
+    def getDescription(self,single=True):
+        '''Get all or a sole `Description` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Description` child,
+            otherwise fail.  If `False` return all `De` children in
+            this element
+        Returns
+        -------
+        children : list
+            List of `De` children (even if `single=True`)
+
+        '''
+        return self.getAllElements(Description,single=single)
+    def getDateSaved(self,single=True):
+        '''Get all or a sole `DateSaved` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `DateSaved` child, otherwise fail.
+            If `False` return all `DateSaved` children in this element
+        Returns
+        -------
+        children : list
+            List of `DateSaved` children (even if `single=True`)
+        '''
+        return self.getAllElements(DateSaved,single=single)
+    
 # --------------------------------------------------------------------
-def add_hex_grid(root,parent,color=rgb(255,0,0),visible=True,grid=True):
-    '''Add a hex grid, either to a zone or to the multizone container'''
+class DataElement(Element):
+    def __init__(self,data,tag,node=None,**kwargs):
+        super(DataElement,self).__init__(data,tag,node=node,**kwargs)
 
-    hexes = add_hexgrid(root,parent,color=color,visible=visible,
-                        edgesLegal=True)
-    if not grid:
-        return hexes
+    def getData(self):
+        return self.getParent(Data)
 
-    add_hexnumbering(root,hexes,
-                     color   =  color,
-                     hType   = 'A',
-                     hOff    = -1,
-                     vType   = 'N',
-                     vOff    = -1,
-                     visible = visible)
+# --------------------------------------------------------------------
+class Version(DataElement):
+    TAG = 'version'
+    def __init__(self,data,node=None,version=''):
+        super(Version,self).__init__(data,self.TAG,node=node)
+        if node is None:
+            self.addText(version)
 
-    return hexes
+# --------------------------------------------------------------------
+class VASSALVersion(DataElement):
+    TAG = 'VassalVersion'
+    def __init__(self,data,node=None,version='3.6.7'):
+        super(VASSALVersion,self).__init__(data,self.TAG,node=node)
+        if node is None:
+            self.addText(version)
 
 # --------------------------------------------------------------------
-def get_picture_info(picture,name,width,height,verbose):
+class Name(DataElement):
+    TAG = 'name'
+    def __init__(self,data,node=None,name=''):
+        super(Name,self).__init__(data,self.TAG,node=node)
+        if node is None:
+            self.addText(name)
+            
+# --------------------------------------------------------------------
+class Description(DataElement):
+    TAG = 'description'
+    def __init__(self,data,node=None,description=''):
+        super(Description,self).__init__(data,self.TAG,node=node)
+        if node is None:
+            self.addText(description)
+
+# --------------------------------------------------------------------
+class DateSaved(DataElement):
+    TAG = 'dateSaved'
+    def __init__(self,data,node=None,milisecondsSinceEpoch=-1):
+        super(DateSaved,self).__init__(data,self.TAG,node=node)
+        if node is None:
+            from time import time
+            s = f'{int(time()*1000)}' if milisecondsSinceEpoch < 0 else \
+                str(milisecondsSinceEpoch)
+            self.addText(s)
+            
+# --------------------------------------------------------------------
+class ModuleData(Element):
+
+    def __init__(self,root=None):
+        '''Construct from a DOM object, if given, otherwise make new'''
+        from xml.dom.minidom import Document
+        super(ModuleData,self).__init__(None,'',None)
+        
+        self._root = root
+        if self._root is None:
+            self._root = Document()
+
+        self._node = self._root
+
+    def addData(self,**kwargs):
+        '''Add a `Data` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Data
+            The added element
+        '''
+        return Data(self,**kwargs)
+
+    def getData(self):
+        return Data(self,
+                    node=self._root.getElementsByTagName(Data.TAG)[0])
+
+    def encode(self):
+        return self._root.toprettyxml(indent=' ',
+                                      encoding="UTF-8",
+                                      standalone=False)
+
+#
+# EOF
+#
+# ====================================================================
+# From save.py
+
+# ====================================================================
+class SaveIO:
+    '''Wrapper around a save file 
+    
+    Save file is
+    
+        "!VCSK" KEY content
+    
+    Key is two bytes drawn as a random number in 0-255.  Content is
+    two bytes per character.  Content characters are encoded with the
+    random key.
+    
+    Save file (.vsav) content is
+    
+        "begin_save" ESC
+        "\" ESC
+        [commands]* ESC
+        "PLAYER" name password side ESC
+        [map+"BoardPicker" name x y ESC]+
+        "SETUP_STACK" ESC
+        "TURN"+name state ESC
+        "end_save"
+    
+    Commands are
+    
+        "+/" id "/" body "\"
+    
+    where body are
+    
+        "stack" "/" mapName ; x ; y ; ids "\"
+        piece_type "/" piece_state   (x and y set here too) "\"
+    
+    x and y are pixel coordinates (sigh!).  This means we have to know
+    
+    - the pixel location of a hex
+    - the hex coordinates of that hex
+    - whether rows and columns are descending
+    - if even hexes are higher or not
+    
+    The two first items _must_ be user supplied (I think).  When we
+    add stacks or pieces, we must then get the numerical hex
+    coordinates - not what the user specified in the VASSAL editor or
+    the like.  Of course, this means opening the module before writing
+    the patch.py script.
+    
+    It seems like every piece is added in a stack.
+    
+    The id is a numerical value.  Rather big (e.g., 1268518000806). It
+    is the current number of miliseconds since epoch, with offset to
+    disambiguate.
+    
+    The ID is the current time, taken from a milisecond clock,
+    possibly adjusted up if there is a clash.  This is all managed by
+    the GameState class.
+
     '''
-    Returns
-    -------
-    hex_width, hex_height : float, float
-        Scale hex width
-    scx, scy : float, float, float, float
-        Scale to image and picture (x,y)
-    rot90 : bool
-        True if rotated +/-90 degrees
-    tran : callable
-        Translation function
-    '''
-    if picture is None:
-        print(f'WARNING: No Tikz picture information.'
-              f"Are you sure you used the `[zoned]' option for the "
-              f"tikzpicture environment of {name}?")
-        f = lambda x,y: (x,y)
-        return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
+    VCS_HEADER = b'!VCSK'
+    VK_ESC     = chr(27)
+    DEC_MAP    = {
+        # 0-9
+        0x30: 0x30,
+        0x31: 0x30,
+        0x32: 0x30,
+        0x33: 0x30,
+        0x34: 0x30,
+        0x35: 0x30,
+        0x36: 0x30,
+        0x37: 0x30,
+        0x38: 0x30,
+        0x39: 0x30,
+        # A-F
+        0x41: 0x37,
+        0x42: 0x37,
+        0x43: 0x37,
+        0x44: 0x37,
+        0x45: 0x37,
+        0x46: 0x37,
+        # a-f
+        0x61: 0x57,
+        0x62: 0x57,
+        0x63: 0x57,
+        0x64: 0x57,
+        0x65: 0x57,
+        0x66: 0x57
+    }
+    ENC_MAP = [b'0',b'1',b'2',b'3',b'4',b'5',b'6',b'7',b'8',b'9',
+               b'a',b'b',b'c',b'd',b'e',b'f']
+
+    # ----------------------------------------------------------------
+    @classmethod
+    def decHex(cls,b):
+        '''Decode a single char into a number
+
+        If the encoded number is b, then the decoded number is
+        
+            b - off
     
-    # Get picture bounding box
-    tll = picture['lower left']
-    tur = picture['upper right']
-    # Get picture transformation
-    pa  = picture['xx']
-    pb  = picture['xy']
-    pc  = picture['yx']
-    pd  = picture['yy']
-    # Get picture offset (always 0,0?)
-    pdx = picture['dx']
-    pdy = picture['dy']
-    # Define picture global transformation
-    pr  = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
-    # Globally transform (rotate) picture bounding box 
-    pll  = pr(*tll)
-    pur  = pr(*tur)
-    # Calculate widht, height, and scaling factors 
-    pw   = pur[0] - pll[0]
-    ph   = pur[1] - pll[1]
-    scw  = width / pw
-    sch  = height / ph
-    # Extract picture scales and rotation
-    # Courtesy of
-    # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
-    from math import sqrt, atan2, degrees, isclose
-    psx   = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
-    psy   = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
-    prt   = degrees(atan2(pc,pd))
-    if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
-        raise RuntimeException('Rotations of Tikz pictures other than '
-                               '0 or +/-90,+/- 180, or +/-270 not supported. '
-                               'found {prt}')
-    rot90      = isclose(abs(prt),90)
-    if any([isclose(prt,a) for a in [90,270,180,-180]]):
-        print(f'WARNING: rotations by {prt} not fully supported')
+        where off is an offset that depends on b
+    
+           off = 0x30   if 0x30 <= b <= 0x39
+                 0x37   if 0x41 <= b <= 0x46
+                 0x57   if 0x61 <= b <= 0x66
+        '''
+        return b - cls.DEC_MAP[b]
+    # --------------------------------------------------------------------
+    @classmethod
+    def readByte(cls,inp,key):
+        '''Read a single byte of information from input stream
+    
+        Two characters (c1 and c2) are read from input stream, and the
+        decoded byte is then
+    
+            ((dechex(c1) << 4 | dechex(c2)) ^ key) & 0xFF
         
-    hex_width  = psx * HEX_WIDTH
-    hex_height = psy * HEX_HEIGHT
-    if verbose:
-        print(f'   Picture transformations: {pa},{pb},{pc},{pd}\n'
-              f'     Scale (x,y):        {psx},{psy}\n'
-              f'     Rotation (degrees): {prt}')
+        Parameters
+        ----------
+        inp : stream
+            Input to read from
+        key : int
+            Key to decode the input
+    
+        Returns
+        -------
+        b : int
+            The read byte
+        '''
+        try:
+            pair = inp.read(2)
+        except Exception as e:
+            from sys import stderr
+            print(e,file=stderr)
+            return None
+    
+        if len(pair) < 2:
+            return None
+    
+        return ((cls.decHex(pair[0]) << 4 | cls.decHex(pair[1])) ^ key) & 0xFF
+    # --------------------------------------------------------------------
+    @classmethod
+    def readSave(cls,file,alsometa=False):
+        '''Read data from save file.  The data is read into lines
+        returned as a list.
 
-    # When translating the Tikz coordinates, it is important to note
-    # that the Tikz y-axis point upwards, while the picture y-axis
-    # point downwards.  This means that the upper right corner is at
-    # (width,0) and the lower left corner is at (0,height).
-    def tranx(x,off=-pll[0]):
-        # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
-        return int(scw * (x + off)+.5)
-    def trany(y,off=-pur[1]):
-        # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
-        return -int(sch * (y + off)+.5)
-    tran  = lambda x,y : (tranx(x), trany(y))
+        '''
+        from zipfile import ZipFile
         
-    return hex_width, hex_height, scw * psx, sch * psy, rot90, tran
-
-# --------------------------------------------------------------------
-def get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
-                  labels,coords,targs,nargs):
-    targs['dx'] = hex_width
-    targs['dy'] = hex_height
-    if labels is not None:
-        labmap = {
-            'auto': {
-                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
-            'auto=numbers' : {
-                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
-            'auto=alpha column': {
-                'hLeading': 0, 'vLeading': 0, 'hType': 'A', 'vType': 'N',
-                'hOff': -1 },
-            'auto=alpha 2 column': {# Not supported
-                'hLeading': 1, 'vLeading': 1, 'hType': 'A', 'vType': 'N' },
-            'auto=inv y x plus 1': {
-                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
-                'hOff': 0, 'vDescend': True }, 
-            'auto=x and y plus 1': {
-                'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
-                'hOff': 0, 'vOff': 0  }
-        }
-        for l in labels.split(','): 
-            nargs.update(labmap.get(l,{}))
+        # We open the save file as a zip file 
+        with ZipFile(file,'r') as z:
+            # open the save file in the archive
+            save = z.open('savedGame','r')
             
-    # Field/Direction | Normal | Rotated |
-    # ----------------|--------|---------|
-    # hOff            | column | column  |
-    # vOff            | row    | row     |
-    # hDescend        | column | row     | True, incr to left 
-    # vDescend        | row    | column  | True, incr upward
-    # First='H'       | column | column  |
+            # First, we check the header
+            head = save.read(len(cls.VCS_HEADER))
+            assert head == cls.VCS_HEADER, \
+                f'Read header {head} is not {cls.VCS_HEADER}'
     
-    # In wargame offset=0 means that the first coordinates are 0,0.
-    # But offset 0,0 in VASSAL means start at 1,1
-    horiz         = 'column'
-    vert          = 'row'
-    nargs['vOff'] = nargs.get('vOff',0)-coords[vert] ['offset']-1
-    nargs['hOff'] = nargs.get('hOff',0)-coords[horiz]['offset']-1
+            # Then, read the key
+            pair = save.read(2)
+            key  = (cls.decHex(pair[0]) << 4 | cls.decHex(pair[1]))
+    
+            # Now read content, one byte at a time 
+            content = ''
+            while True:
+                byte = cls.readByte(save,key)
+                if byte is None:
+                    break
+    
+                # Convert byte to character 
+                content += chr(byte)
+    
+            lines = content.split(cls.VK_ESC)
 
-    # In VASSAL, vDesend=False means rows increase downward,
-    # while in wargame, factor=-1 means rows increase downward
-    horiz               = 'row'    if rot90 else 'column'
-    vert                = 'column' if rot90 else 'row'
-    nargs['hDescend']   = coords[horiz]['factor'] !=  1
-    nargs['vDescend']   = coords[vert] ['factor'] != -1 # flipped y
-    targs['edgesLegal'] = True
+            if alsometa:
+                savedata = z.read(VSav.SAVE_DATA)
+                moduledata = z.read(VMod.MODULE_DATA)
 
-    # Check for inversed columns 
-    inv_col = coords['column']['factor'] == -1
-    ell     = hex_width if inv_col else 0
+        if not alsometa:
+            return key, lines
 
-    # Some special stuff in case of 90 degree rotation
-    if rot90:
-        targs.update({'sideways': True})
-        nargs['hOff']     -= 1
-        nargs['hDescend'] = not nargs['hDescend']
-        if inv_col:
-            nargs['hOff'] += 3
+        return key,lines,savedata,moduledata
 
-    margin = i.get('board frame',{}).get('margin',0)        
-    mx     = scx * margin
-    my     = scy * margin
-    if inv_col:  mx = 0 # Bug in wargame?
+    # --------------------------------------------------------------------
+    @classmethod
+    def writeByte(cls,out,byte,key):
+        '''Write a single byte
+
+        Parameters
+        ----------
+        out : IOStream
+            Stream to write to
+        byte : char
+            Single byte to write
+        key : int
+            Key to encode with (defaults to 0xAA - alternating 0's and 1's)
+        '''
+        b    = ord(byte) ^ key
+        pair = cls.ENC_MAP[(b & 0xF0) >> 4], cls.ENC_MAP[b & 0x0F]
+        out.write(pair[0])
+        out.write(pair[1])
+
+    # --------------------------------------------------------------------
+    @classmethod
+    def writeInZip(cls,z,key,lines,filename='savedGame'):
+        '''Write a save file in a zip file (VMod)'''
+        # open the save file in the archive
+        with z.open(filename,'w') as save:
+            # Write header
+            save.write(cls.VCS_HEADER)
+    
+            # Split key
+            pair = cls.ENC_MAP[(key & 0xF0) >> 4], cls.ENC_MAP[(key & 0x0F)]
+            save.write(pair[0])
+            save.write(pair[1])
+    
+            # Form content
+            content = cls.VK_ESC.join(lines)
+    
+            # Write each character as two
+            for c in content:
+                cls.writeByte(save, c, key)
         
-    # Grid offset is defined relative to the full image,
-    # not the zone:
-    #
-    #   X,Y offset: The horizontal and vertical position of
-    #   the center of the first hex of the grid.
-    #
-    # That is, (x,y)=(0,0) is in the top-left corner of the
-    # image, while in TikZ it is in the bottom left corner.
-    # This means we shift by the boudning box offsets plus
-    # some stuff to align with the zone.
-    #
-    # In case of inversed columns, we need to add some extra stuff.
-    x0, y0 = llx-hex_width/3 + ell + mx, ury + my
+    # --------------------------------------------------------------------
+    @classmethod
+    def writeSave(cls,file,key,lines,savedata=None,moduledata=None):
+        '''Write a save file'''
+        from zipfile import ZipFile, ZIP_DEFLATED
+        
+        # We open the save file as a zip file 
+        with ZipFile(file,'w',ZIP_DEFLATED) as z:
+            cls.writeInZip(z,key,lines,filename='savedGame')
 
-    # Rotated?
-    if rot90:
-        x0, y0 = ury+2*hex_width/3 + ell + mx, llx + my
+            if savedata is not None:
+                z.writestr(VSav.SAVE_DATA,savedata)
+                z.writestr(VMod.MODULE_DATA,moduledata)
         
-    # Below should be double-checked.
-    
-    if not rot90:
-        if coords.get('column',{}).get('top short','') == 'isodd' \
-           and coords.get('column',{}).get('offset',0) == -1:
-            y0 -= hex_width/2
-        elif coords.get('column',{}).get('top short','') == 'iseven' \
-             and coords.get('column',{}).get('offset',-1) == 0:
-            y0 -= hex_width/2
+# ====================================================================
+#
+# VSave file
+#
+class SaveFile:
+    def __init__(self,game,firstid=None):
+        '''Creates a save file to add positions to'''
+        from time import time
+        self._game     = game
+        self._counters = {}
+        self._stacks   = {}
+        self._pieces   = self._game.getPieces(asdict=True)
+        self._nextId   = (int(time()*1000) - 360000
+                          if firstid is None else firstid)
+        
+    def add(self,grid,**kwargs):
+        '''Add pieces to the save.
 
-    targs.update({'x0': int(x0),'y0': int(y0)})
+        Parameters
+        ----------
+        grid : BaseGrid
+            Grid to add pieces to 
+        kwargs : dict
+            Either a map from piece name to hex position,
+            Or a map from hex position to list of pieces
+        '''
+        for k,v in kwargs.items():
+            # print('Add to save',k,v)
+            self._add(grid,k,v)
 
-# --------------------------------------------------------------------
-def get_rectparams(i,llx,ury,width,height,rot90,targs,nargs):
-    targs['dx']       = width
-    targs['dy']       = height
-    targs['x0']       = int(llx - width/2)
-    targs['y0']       = int(ury + height/2)
-    targs['color']    = rgb(0,255,0)
-    nargs['color']    = rgb(0,255,0)
-    nargs['vDescend'] = True
-    nargs['vOff']     = -3
-    nargs.update({'sep':',','vLeading':0,'hLeading':0})
-    
-# --------------------------------------------------------------------
-def add_zones(root,zoned,name,board,width,height,gpid,
-              labels=None,coords=None,picinfo=None,
-              verbose=False,visible=True):
-    grids = []
-    picture = None
-    if verbose:
-        print(f'  Adding zones to {name} zoned={"zoned" in board}')
+    def _add(self,grid,k,v):
+        '''Add to the save'''
+        # print(f'Adding {k} -> {v}')
+        loc       = None
+        piece     = self._pieces.get(k,None)
+        pieces    = []
+        boardName = grid.getBoard()['name']
+        # print(f'Board name: {boardName}')
+        if piece is not None:
+            # print(f'Key is piece: {k}->{piece}')
+            pieces.append(piece)
+            loc = v
+        else:
+            # Key is not a piece name, so a location
+            loc = k
+            # Convert value to iterable 
+            try:
+                iter(v)
+            except:
+                v = list(v)
+            
+            for vv in v:
+                if isinstance(vv,PieceSlot):
+                    pieces.append(vv)
+                    continue
+                if isinstance(vv,str):
+                    piece = self._pieces.get(vv,None)
+                    if piece is None:
+                        continue
+                    pieces.append(piece)
 
-    for k, v in board.items():
-        if k == 'labels': labels = v
-        if k == 'coords': coords = v
-        if k == 'zoned':  picture = v
-        if 'zone' not in k or k == 'zoned':
-            continue
+        # print(f'Loc: {loc}')
+        if len(pieces) < 1:
+            return
+        
+        if (boardName,loc) not in self._stacks:
+            # print(f'Adding stack {boardName},{loc}')
+            coord = grid.getLocation(loc)
+            if coord is None:
+                print(f'did not get coordinates from {loc}')
+                return
+            self._stacks[(boardName,loc)] = {
+                'x': coord[0],
+                'y': coord[1],
+                'pids': [] }
+                
+        place = self._stacks[(boardName,loc)]
+        for piece in pieces:
+            name   = piece['entryName']
+            count  = self._counters.get(name,None)
+                            
+            if count is None:
+                count = {'pid':  self._nextId,
+                         'piece': piece,
+                         'board': boardName,
+                         'x':     place['x'],
+                         'y':     place['y'],
+                         }
+                self._counters[name] = count
+                self._nextId += 1
 
-        gridtype = (add_hexgrid
-                    if 'hex' in k.lower()
-                    else add_squaregrid)
-        gridnum  = (add_hexnumbering
-                    if 'hex' in k.lower()
-                    else add_squarenumbering)
-        grids.append([k,v,gridtype,gridnum])
+                
+            # print(f'Adding to stack {boardName},{loc}: {count[0]}')
+            place['pids'].append(count['pid'])
 
-    if len(grids) < 1: return gpid
-    if verbose:
-        print(f'    Got zones: {[k[0] for k in grids]}')
-    # Either get picture information from argument, or deduce from the
-    # found 'zoned' key.
-    if picinfo is None:
-        picinfo = get_picture_info(picture,name,width,height,verbose)
+    def getLines(self):
+        '''Get the final lines of code'''
+        key   = 0xAA # fixed key
         
-    hex_width, hex_height, scx, scy, rot90, tran = picinfo 
+        lines = ['begin_save',
+                 '',
+                 '\\']
+
+        self._pieceLines(lines)
+        self._otherLines(lines)
         
-    
-    for g in grids:
-        n, i, typ, num = g
-        if verbose:
-            print(f'   Adding zone {n}')
-        if 'scope' in n:
-            llx,lly = tran(*i['global lower left'])
-            urx,ury = tran(*i['global upper right'])
-            path    = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
-            nm      = n.replace('zone scope ','')
-        elif 'path' in n:
-            path    = [tran(*ii) for ii in i['path']]
-            llx     = min([px for px,py in path])
-            ury     = max([py for px,py in path])
-            nm      = n.replace('zone path ','')
+        lines.append('end_save')
+        return lines
 
-        # Checkf if we have "point" type elements in this object and
-        # add them to dict.
-        points = [ v for k,v in i.items()
-                   if (k.startswith('point') and
-                       isinstance(v,dict) and \
-                       v.get('type','') == 'point')]
+    def _pieceLines(self,lines):
+        '''Add piece lines to save file
 
-        pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
-        if verbose:
-            print(f'    Zone path ({llx},{ury}): {pathstr}')
-        zone    = add_zone(root,zoned,
-                           name=nm,
-                           locationFormat = ("$name$"
-                                             if 'pool' in n.lower() else
-                                             "$gridLocation$"),
-                           useParentGrid=False,
-                           path=pathstr)
+        Parameters
+        ----------
+        lines : list
+            The lines to add
+        '''
+        # print(self._counters)
+        for counter in self._counters.values():
+            iden   = counter['pid']
+            piece  = counter['piece']
+            traits = piece.getTraits()
+            traits = Trait.flatten(traits,self._game)
+            # Get last - trait (basic piece), and modify coords
+            basic  = traits[-1]
+            basic['map'] = counter['board']
+            basic['x']   = counter['x']
+            basic['y']   = counter['y']
+            # Set old location if possible
+            parent = piece.getParent(DummyElement,checkTag=False)
+            if parent is not None and parent._node.nodeName == AtStart.TAG:
+                oldLoc   = parent['location']
+                oldBoard = parent['owningBoard']
+                oldMap   = self._game.getBoards()[oldBoard].getMap()['mapName']
+                oldX     = parent['x']
+                oldY     = parent['y']
+                oldZone  = None
+                zones    = self._game.getBoards()[oldBoard].getZones()
+                for zone in zones.values():
+                    grid = zone.getGrids()[0]
+                    if grid is None: continue
+                    
+                    coord = grid.getLocation(oldLoc)
+                    if coord is None: continue
 
-        # Do not add grids to pools 
-        if 'pool' in n.lower():
-            continue
+                    oldZone = zone['name']
+                    oldX    = coord[0]
+                    oldY    = coord[1]
+                    break
 
-        targs  = {'color':rgb(255,0,0),'visible':visible}
-        nargs  = {'color':rgb(255,0,0),'visible':visible}
-        if 'hex' in n.lower():
-            get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
-                          labels,coords,targs,nargs)
-        else:
-            width = hex_width / HEX_WIDTH * RECT_WIDTH
-            height = hex_height / HEX_HEIGHT * RECT_HEIGHT
-            get_rectparams(i,llx,ury,width,height,rot90,targs,nargs)
+                if oldZone is not None:
+                    basic['properties'] = \
+                        f'6;OldZone;{oldZone};OldLocationName;{oldLoc};'+\
+                        f'OldX;{oldX};OldY;{oldY};'+\
+                        f'OldBoard;{oldBoard};OldMap;{oldMap}'
+                else:
+                    basic['properties'] = \
+                        f'5;OldLocationName;{oldLoc};'+\
+                        f'OldX;{oldX};OldY;{oldY};'+\
+                        f'6;OldBoard;{oldBoard};OldMap;{oldMap}'
+
+                for trait in traits:
+                    if trait.ID == TrailTrait.ID:
+                        trait['map']    = oldMap
+                        trait['points'] = f'1;{oldX},{oldY}'
+                        trait['init']   = True
+                    
+            # Wrapper 
+            wrap   = DummyWithTraits(self._game,traits=[])
+            wrap.setTraits(*traits,iden=str(iden))
+            lines.append(wrap._node.childNodes[0].nodeValue+'\\')
+
+        layer = -1
+        for key,dat in self._stacks.items():
+            pids = dat.get('pids',None)
+            x    = dat['x']
+            y    = dat['y']
+            if pids is None or len(pids) < 1:
+                print(f'No pieces at {key[0]},{key[1]}')
+                continue
             
-        # print(targs,nargs)
-        if 'turn' in n.lower(): nargs['sep'] = 'T'
-        if 'oob' in n.lower():  nargs['sep'] = 'O'
+            iden         =  self._nextId
+            self._nextId += 1
+            stack        =  StackTrait(board=key[0],x=x,y=y,pieceIds=pids,layer=layer)
+            layer        = 1
+            wrap         =  DummyWithTraits(self._game,traits=[])
+            wrap.setTraits(stack,iden=iden)
+            lines.append(wrap._node.childNodes[0].nodeValue+'\\')
+            
+    def _otherLines(self,lines):
+        '''Add other lines to save'''
+        lines.append('UNMASK\tnull')
+        for r in self._game.getPlayerRoster():
+            lines.extend(r.encode())
+        for n in self._game.getNotes(single=False):
+            lines.extend(n.encode())
+        setupStack = False
+        for m in self._game.getMaps(asdict=False):
+            for bp in m.getBoardPicker(single=False):
+                lines.extend(bp.encode())
+            if not setupStack:
+                atstart = m.getAtStarts(single=False)
+                if atstart and len(atstart) > 0:
+                    lines.append('SETUP_STACK')
+                    setupStack = True
+                
+        # for tk,tt in self._game.getTurnTracks(asdict=True):
+        #     lines.extend(tt.encode())
 
-        if verbose:
-            print(f'    Adding grid')
+            
+# --------------------------------------------------------------------
+class SaveData(ModuleData):
+    def __init__(self,root=None):
+        '''Convinience wrapper'''
+        super(SaveData,self).__init__(root=root)
+# ====================================================================
+# From vsav.py
 
-        if len(points) <= 0:
-            grid = typ(root,zone,**targs)
-            num(root,grid,**nargs)
-        else:
-            if verbose:
-                print(f'     Use a region grid')
-            grid = add_regiongrid(root,zone,snapto=True,visible=visible)
-            for p in points:
-                pn = p["name"]
-                pc = p["coords"]
-                if verbose:
-                    print(f'      Add region {pn} {p}')
+# --------------------------------------------------------------------
+class VSav:
+    SAVE_DATA = 'savedata'
+    
+    def __init__(self,build,vmod):
+        '''Create a VASSAL save file programmatically
 
-                n, gpid = add_region(root,grid,pn,*tran(*pc),gpid,
-                                     alsoPiece=True,verbose=verbose)
-                
+        Parameters
+        ----------
+        build : xml.dom.Document
+            `buildFile.xml` as XML
+        vmod : VMod
+            Module file
+        '''
+        from time import time 
+        self._vmod  = vmod
+        self._game  = build.getGame()
+        self._start = int(time()*1000)
+        
 
-        # Once we've dealt with this grid, we should see if we have
-        # any embedded zones we should deal with.
-        add_zones(root,zoned,name,i,width,height,gpid,
-                  labels=labels,coords=coords,picinfo=picinfo,
-                  verbose=verbose)
+    def createSaveData(self,description=None):
+        '''Create `savedgame`'''
+        desc           = (self._game['description']
+                          if description is None else description)
+        self._saveData = SaveData(root=None)
+        data           = self._saveData.addData()
+        data.addVersion      (version    =self._game['version'])
+        data.addVASSALVersion(version    =self._game['VassalVersion'])
+        data.addDescription  (description=desc)
+        data.addDateSaved    (milisecondsSinceEpoch=self._start)
+        return self._saveData
+
+    def createModuleData(self):
+        '''Create `moduleData`'''
+        self._moduleData = ModuleData()
+        data = self._moduleData.addData()
+        data.addVersion      (version    =self._game['version'])
+        data.addVASSALVersion(version    =self._game['VassalVersion'])
+        data.addName         (name       =self._game['name'])
+        data.addDescription  (description=self._game['description'])
+        data.addDateSaved    (milisecondsSinceEpoch=self._start)
+        return self._moduleData
         
-        # Adding a global grid
-        #targs['color'] = rgb(0,255,0)
-        #nargs['color'] = rgb(0,255,0)
-        #grid = typ(root,zoned,**targs)
-        #num(root,grid,**nargs)
+    def addSaveFile(self):
+        '''Add a save file to the module
 
-    return gpid
+        Returns
+        -------
+        vsav : SaveFile
+            Save file to add content to        
+        '''
+        self._saveFile = SaveFile(game=self._game,firstid=self._start)
+        return self._saveFile
 
+    def run(self,savename='Save.vsav',description=None):
+        '''Run this to generate the save file
+
+        Parameters
+        ----------
+        savename : str
+            Name of save file to write
+        description : str
+            Short description of the save file
+        '''
+        from zipfile import ZipFile, ZIP_DEFLATED
+        
+        self.createSaveData(description=description)
+        self.createModuleData()
+        
+        with self._vmod.getInternalFile(savename,'w') as vsav:
+            with ZipFile(vsav,'w',ZIP_DEFLATED) as zvsav:
+                # The key is set to 0xAA (alternating ones and zeros)
+                SaveIO.writeInZip(zvsav,0xAA,self._saveFile.getLines())
+            
+                zvsav.writestr(VMod.MODULE_DATA, self._moduleData.encode())
+                zvsav.writestr(VSav.SAVE_DATA,   self._saveData.encode())
+            
+#
+# EOF
+#
+# ====================================================================
+# From vmod.py
+# ====================================================================
+#
+# Wrapper around a module 
+#
+class VMod:
+    BUILD_FILE = 'buildFile.xml'
+    MODULE_DATA = 'moduledata'
     
-# --------------------------------------------------------------------
-def add_inv(root,game,sides,**attr):
-    '''Add inventory to module'''
-    filt = '{' + '||'.join([f'Faction=="{s}"' for s in sides])+'}'
-    grp  = 'Faction,Command,Echelon,Type'
-    inv = add_inventory(root,game,
-                        include             = filt,
-                        groupBy             = grp,
-                        sortFormat          = '$PieceName$',
-                        tooltip             = 'Show inventory of all pieces',
-                        zoomOn              = True,
-                        **attr)
-    return inv
-                        
-                        
-# --------------------------------------------------------------------
-def add_map_board(root,game,name,board,gpid,verbose=False,visible=False):
-    '''A add a single board to the module
-    '''
+    def __init__(self,filename,mode):
+        '''Interface to VASSAL Module (a Zip file)'''
+        self._mode = mode
+        self._vmod = self._open(filename,mode)
 
-    map = add_map(root,game, name)
-    bpicker = add_boardpicker(root,map)
+    def __enter__(self):
+        '''Enter context'''
+        return self
+
+    def __exit__(self,*e):
+        '''Exit context'''
+        self._vmod.close()
+        return None
+
+    def _open(self,filename,mode):
+        '''Open a file in VMod'''
+        from zipfile import ZipFile, ZIP_DEFLATED
+
+        return ZipFile(filename,mode,compression=ZIP_DEFLATED)
+        
+    def removeFiles(self,*filenames):
+        '''Open a temporary zip file, and copy content from there to
+        that file, excluding filenames mentioned in the arguments.
+        Then close current file, rename the temporary file to this,
+        and reopen in 'append' mode.  The deleted files are returned
+        as a dictionary.
+
+        Parameters
+        ----------
+        filenames : tuple
+            List of files to remove from the VMOD
+
+        Returns
+        -------
+        files : dict
+            Dictionary from filename to content of the removed files.
+
+        Note, the VMOD is re-opened in append mode after this
+        '''
+        from tempfile import mkdtemp
+        from zipfile import ZipFile
+        from shutil import move, rmtree 
+        from os import path
+
+        tempdir = mkdtemp()
+        ret     = {}
+
+        try:
+            tempname = path.join(tempdir, 'new.zip')
+            with self._open(tempname, 'w') as tmp:
+
+                for item in self._vmod.infolist():
+                    data = self._vmod.read(item.filename)
+
+                    if item.filename not in filenames:
+                        tmp.writestr(item, data)
+                    else:
+                        ret[item.filename] = data
+
+            name = self._vmod.filename
+            self._vmod.close()
+            move(tempname, name)
+
+            self._mode = 'a'
+            self._vmod = self._open(name,'a')
+        finally:
+            rmtree(tempdir)
+
+        # Return the removed files 
+        return ret
+
+    def fileName(self):
+        '''Get name of VMod file'''
+        return self._vmod.filename
     
-    add_counterviewer      (root,map)
-    add_hidepieces         (root,map)
-    add_globalmap          (root,map)
-    add_basics_to_map      (root,map)
-    add_masskey            (root,map,'Flip',     key('F'),key('F'),
-                            icon = '/images/Undo16.gif',
-                            tooltip='Flip selected units')
-    add_masskey            (root,map,'Eliminate',key('E'),key('E'),
-                            icon = '/images/playerAway.gif',
-                            tooltip='Eliminate selected units')
+    def addFiles(self,**files):
+        '''Add a set of files  to this
 
-    # Get the image so we may make a rectangle
-    if verbose:
-        print(f' Adding board {board["name"]} -> {board["filename"]}')
-    ulx,uly,lrx,lry = get_bb(board['img'])
-    eboard = add_board(root,bpicker,name,image=board['filename'])
-    zoned  = add_zoned(root,eboard)
-    #hexes = add_hexgrid(root,zoned)
-    add_zonehighlighter(root,zoned)
+        Parameters
+        ----------
+        files : dict
+            Dictionary that maps file name to content.
+        '''
+        for filename,data in files.items():
+            self.addFile(filename,data)
 
-    if not 'zones' in board:
-        full = add_zone(root,zoned,
-                        name='full',
-                        useParentGrid=False,
-                        path=(f'{ulx},{uly};' +
-                              f'{lrx},{uly};' +
-                              f'{lrx},{lry};' +
-                              f'{ulx},{lry}'))
+    def addFile(self,filename,content):
+        '''Add a file to this
 
-        # We do not have board info, so add a grid to the whole thing
-        add_hex_grid(root,full,rgb(255,0,0),True,True,visible=visible)
-        return
+        Parameters
+        ----------
+        filename : str
+            File name in module
+        content : str
+            File content
+        Returns
+        -------
+        element : File
+            The added element
+        '''
+        self._vmod.writestr(filename,content)
 
-    # If we get here, we have board info!
-    w = abs(ulx-lrx)
-    h = abs(uly-lry)
-    return add_zones(root,zoned,name,board['zones'],w,h,gpid,
-                     verbose=verbose,visible=visible)
+    def addExternalFile(self,filename,target=None):
+        '''Add an external file element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ExternalFile
+            The added element
+        '''
+        if target is None: target = filename
+        self._vmod.write(filename,target)
+        
+    def getFileNames(self):
+        '''Get all filenames in module'''
+        return self._vmod.namelist()
     
-# --------------------------------------------------------------------
-def add_boards(root,game,categories,sides,gpid,verbose=False,visible=False):
-    '''Add all boards to the module, including eliminated map
+    def getFiles(self,*filenames):
+        '''Return named files as a dictionary.
 
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    game : xml.dom.minidom.Element
-        The parent element
-    categories : dict
-        Catalogue of images 
-    sides : list of  str
-        List of sides in the game
-        Name of the map
-    '''
-    if verbose:
-        print('Adding maps and boards')
+        Parameters
+        ----------
+        filenames : tuple
+            The files to get 
+        Returns
+        -------
+        files : dict
+            Mapping of file name to file content
+        '''
+        fn  = self.getFileNames()
+        ret = {}
+        for f in filenames:
+            if f not in fn:
+                continue
 
-    # Loop over defined boards
-    for bn, b in categories.get('board',{}).get('all',{}).items():
-        gpid = add_map_board(root,game,bn,b,gpid,verbose,visible)
+            ret[f] = self._vmod.read(f)
+
+        return ret
+
+    def getDOM(self,filename):
+        '''Get content of a file decoded as XML DOM
+
+        Parameters
+        ----------
+        filename : str
+            Name of file in module 
+        '''
+        from xml.dom.minidom import parseString
+
+        r = self.getFiles(filename)
+        if filename not in r:
+            raise RuntimeException(f'No {filename} found!')
+
+        return parseString(r[filename])
         
-    add_elim(root,game,sides,categories,verbose)
+    def getBuildFile(self):
+        '''Get the buildFile.xml decoded as a DOM tree'''
+        return self.getDOM(VMod.BUILD_FILE)
 
-    return gpid
+    def getModuleData(self):
+        '''Get the moduledata decoded as a DOM tree'''
+        return self.getDOM(VMod.MODULE_DATA)
 
-# --------------------------------------------------------------------
-def add_oob(root,widget,name,oob,gpid,hotkey='',verbose=False,visible=False):
-    '''A add a single board to the module
-    '''
-    # Short hand 
-    cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)
+    def getInternalFile(self,filename,mode):
+        return self._vmod.open(filename,mode)
 
-    if verbose:
-        print(f' Adding oob {name}')
+    def addVSav(self,build):
+        '''Add a `VSav` element to this
 
-    map     = add_widgetmap(root,widget, name,markMoved='Never',hotkey='')
-    bpicker = add_boardpicker(root,map)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : VSav
+            The added element
+        '''
+        return VSav(build=build,vmod=self)
 
-    add_counterviewer      (root,map)
-    add_basics_to_map      (root,map)
+
+#
+# EOF
+#
+# ====================================================================
+# From exporter.py
+
+class Exporter:
+    def __init__(self):
+        '''Base class for exporters'''
+        pass
+
+
+    def setup(self):
+        '''Should be defined to set-up for processing, for example
+        generating images and such.  This is executed in a context
+        where the VMod file has been opened for writing via
+        `self._vmod`. Thus, files can be added to the module at this
+        stage.
+        '''         
+        pass
+
+    def createBuildFile(self,ignores='(.*markers?|all|commons|[ ]+)'):
+        '''Should be defined to make the `buildFile.xml` document
+
+        Parameters
+        ----------
+        ignores : str
+            Regular expression to match ignored categories for factions
+            determination. Python's re.fullmatch is applied to this
+            regular exression against chit categories.  If the pattern
+            is matched, then the chit is not considered to belong to a
+            faction.
+
+        '''
+        pass
+
+    def createModuleData(self):
+        '''Should be defined to make the `moduledata` document'''
+        pass
     
+    def run(self,vmodname,patch=None):
+        '''Run the exporter to generate the module
+        '''
+        with VMod(vmodname,'w') as vmod:
+            self._vmod = vmod
+            self.setup()
+            self.createBuildFile() 
+            self.createModuleData()
+            self.runPatch(patch)
+            self._vmod.addFiles(**{VMod.BUILD_FILE  :
+                                   self._build.encode(),
+                                   VMod.MODULE_DATA :
+                                   self._moduleData.encode()})
+        Verbose().message('Created VMOD')
+        
 
-    # Get the image so we may make a rectangle
-    if verbose:
-        print(f'  Adding OOB {oob["name"]} -> {oob["filename"]}')
+    def runPatch(self,patch):
+        '''Run user specified patch script.  The script should define
+
+            ```
+            def patch(buildFile,moduleData,vmod,verbose):
+            ```
+
+        where `buildFile` is the `buildFile.xml` and `moduleData` are
+        the XML documents as `xml.dom.Document` objects, `vmod` is a
+        `VMod` instance, and `verbose` is a `bool` selecting verbose
+        mode or not.
+        '''
+        if patch is None or patch == '':
+            return
         
-    ulx,uly,lrx,lry = get_bb(oob['img'])
-    board = add_board(root,bpicker,name,image=oob['filename'])
-    zoned = add_zoned(root,board)
-    add_zonehighlighter(root,zoned)
+        from importlib.util import spec_from_file_location, module_from_spec
+        from pathlib import Path
+        from sys import modules
 
-    if not 'zones' in oob:
-        zone = add_zone(root,zoned,
-                        name='full',
-                        useParentGrid=False,
-                        path=(f'{ulx},{uly};' +
-                              f'{lrx},{uly};' +
-                              f'{lrx},{lry};' +
-                              f'{ulx},{lry}'))
-        # We do not have board info, so add a grid to the whole thing
-        grid = add_squaregrid(root,zone,visible=visible)
-        add_squarenumbering(root,grid,visible=visible)
-        return
+        p = Path(patch)
+        with VerboseGuard(f'Will patch module with {p.stem}.patch function') \
+             as v:
 
-    # If we get here, we have board info!
-    w = abs(ulx-lrx)
-    h = abs(uly-lry)
-    return add_zones(root,zoned,name,oob['zones'],w,h,gpid,
-                     verbose=verbose,visible=visible)
+            spec   = spec_from_file_location(p.stem, p.absolute())
+            module = module_from_spec(spec)
+            spec.loader.exec_module(module)
+            modules[p.stem] = module
+            
+            # Patch must accept xml.dom.document,xml.dom.document,ZipFile
+            module.patch(self._build,
+                         self._moduleData,
+                         self._vmod,
+                         Verbose().verbose)
     
-# --------------------------------------------------------------------
-def add_oobs(root,game,categories,gpid,verbose=False,visible=False):
-    '''Add all OObs to the module
+# ====================================================================
+# From latexexporter.py
 
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    game : xml.dom.minidom.Element
-        The parent element
-    categories : dict
-        Catalogue of images 
-    '''
-    # Short hand
-    oobc = categories.get('oob',{}).get('all',{}).items()
-    if len(oobc) < 1:
-        return gpid
-    
-    oobs = add_chartwindow(root,game, 'OOBs',
-                           hotkey      = key('B',ALT),
-                           description = 'OOBs',
-                           text        = '',
-                           icon        = '/images/inventory.gif',
-                           tooltip     = 'Show/hide OOBs')
-    tabs = add_tabs(root,oobs,'OOBs')
+# ====================================================================
+#
+# Exporter class 
+#
+class LaTeXExporter(Exporter):
+    def __init__(self,
+                 vmodname      = 'Draft.vmod',
+                 pdfname       = 'export.pdf',
+                 infoname      = 'export.json',
+                 title         = 'Draft',
+                 version       = 'draft',
+                 description   = '',     
+                 rules         = None,
+                 tutorial      = None,
+                 patch         = None,
+                 visible       = True,
+                 vassalVersion = '3.6.7',
+                 nonato        = False,
+                 nochit        = False,
+                 counterScale  = 1):
+        '''Exports a PDF and associated JSON files to a VASSAL module.
 
-    if verbose:
-        print('Adding OOBs')
+        Parameters
+        ----------
+        vmodname : str
+            Name of module file to write
+        pdfname : str
+            Name of PDF file to read images from
+        infoname : str
+            Name of JSON file to read meta data from
+        title : str
+            Name of module
+        version : str
+            Version of midule
+        description : str
+            Short description of the module
+        rules : str
+            Optional name PDF file to attach as rules
+        tutorial : str
+            Optional name of a VASSAL log file to use as tutorial
+        patch : str
+            Optional name of Python script to post process the module
+        visible : bool
+            Make grids visible
+        vassalVersion : str
+            VASSAL version to encode this module for
+        '''
+        self._vmodname        = vmodname
+        self._pdfname         = pdfname
+        self._infoname        = infoname
+        self._title           = title
+        self._version         = version
+        self._description     = description
+        self._rules           = rules
+        self._tutorial        = tutorial
+        self._patch           = patch
+        self._visible         = visible or version.lower() == 'draft'
+        self._vassalVersion   = vassalVersion
+        self._nonato          = nonato
+        self._nochit          = nochit
+        self._counterScale    = counterScale
+        self._battleMark      = 'wgBattleMarker'
+        self._battleMarks     = []
+        self._oddsMarks       = []
+        self._resultMarks     = []
+        self._markBattle      = key(NONE,0)+',wgMarkBattle'
+        self._clearBattle     = key(NONE,0)+',wgClearBattle'
+        self._clearAllBattle  = key(NONE,0)+',wgClearAllBattle'
+        self._zeroBattle      = key(NONE,0)+',wgZeroBattle'
+        self._incrBattle      = key(NONE,0)+',wgIncrBattle'
+        self._setBattle       = key(NONE,0)+',wgSetBattle'
+        self._getBattle       = key(NONE,0)+',wgGetBattle'
+        self._markOdds        = key(NONE,0)+',wgMarkOdds'
+        self._markResult      = key(NONE,0)+',wgMarkResult'
+        self._clearMoved      = key(NONE,0)+',wgClearMoved'
+        self._zeroBattleAF    = key(NONE,0)+',wgZeroBattleAF'
+        self._zeroBattleDF    = key(NONE,0)+',wgZeroBattleDF'
+        self._zeroBattleFrac  = key(NONE,0)+',wgZeroBattleFrac'
+        self._zeroBattleOdds  = key(NONE,0)+',wgZeroBattleOdds'
+        self._zeroBattleShft  = key(NONE,0)+',wgZeroBattleShift'
+        self._zeroBattleIdx   = key(NONE,0)+',wgZeroBattleIdx'
+        self._calcBattleAF    = key(NONE,0)+',wgCalcBattleAF'
+        self._calcBattleDF    = key(NONE,0)+',wgCalcBattleDF'
+        self._calcBattleFrac  = key(NONE,0)+',wgCalcBattleFrac'
+        self._calcBattleOdds  = key(NONE,0)+',wgCalcBattleOdds'
+        self._calcBattleShft  = key(NONE,0)+',wgCalcBattleShift'
+        self._calcBattleIdx   = key(NONE,0)+',wgCalcBattleIdx'
+        self._calcBattleRes   = key(NONE,0)+',wgCalcBattleResult'
+        self._clearBattlePhs  = key(NONE,0)+',wgClearBattlePhs'
+        self._resolveBattle   = key(NONE,0)+',wgResolveBattle'
+        self._rollDice        = key(NONE,0)+',wgRollDice'
+        self._diceInitKey     = key(NONE,0)+',wgInitDice'
+        self._clearKey        = key('C')
+        self._clearAllKey     = key('C',CTRL_SHIFT)
+        self._deleteKey       = key('D')
+        self._eliminateKey    = key('E')
+        self._flipKey         = key('F')
+        self._trailKey        = key('M')
+        self._restoreKey      = key('R') 
+        self._markKey         = key('X')
+        self._resolveKey      = key('Y')
+        self._rotateCCWKey    = key('[')
+        self._rotateCWKey     = key(']')
+        self._chartsKey       = key('A',ALT)
+        self._oobKey          = key('B',ALT)
+        self._countersKey     = key('C',ALT)
+        self._deadKey         = key('E',ALT)
+        self._diceKey         = key('6',ALT)
+        self._battleCounter   = 'wgBattleCounter'
+        self._currentBattle   = 'wgCurrentBattle'
+        self._currentAttacker = 'wgCurrentAttacker'
+        self._battleNo        = 'wgBattleNo'
+        self._battleAF        = 'wgBattleAF'
+        self._battleDF        = 'wgBattleDF'
+        self._battleFrac      = 'wgBattleFrac'
+        self._battleIdx       = 'wgBattleIdx'
+        self._battleOdds      = 'wgBattleOdds'
+        self._battleOddsM     = 'wgBattleOddsMarker'
+        self._battleShift     = 'wgBattleShift'
+        self._battleCtrl      = 'wgBattleCtrl'
+        self._battleCalc      = 'wgBattleCalc'
+        self._battleUnit      = 'wgBattleUnit'
+        self._battleResult    = 'wgBattleResult'
+        self._autoOdds        = 'wgAutoOdds'
+        self._autoResults     = 'wgAutoResults'
+        self._noClearMoves    = 'wgNoClearMoves'
+        self._noClearBattles  = 'wgNoClearBattles'
+        self._debug           = 'wgDebug'
+        self._verbose         = 'wgVerbose'
+        self._hidden          = None
+        self._hiddenName      = 'wg hidden unit'
+        self._dice            = {}
+        self._diceInit        = None
+        
+        with VerboseGuard('Overall settings') as v:
+            v(f'Module file name:  {self._vmodname}')
+            v(f'PDF file name:     {self._pdfname}')
+            v(f'JSON file name:    {self._infoname}')
+            v(f'Game title:        {self._title}')
+            v(f'Game version:      {self._version}')
+            v(f'Description:       {self._description}')
+            v(f'Rules PDF file:    {self._rules}')
+            v(f'Tutorial log:      {self._tutorial}')
+            v(f'Patch scripts:     {self._patch}')
+            v(f'Visible grids:     {self._visible}')
+            v(f'Scale of counters: {self._counterScale}')
+        
+    def setup(self):
+        # Start the processing 
+        self._info       = self.convertPages()
+        self._categories, \
+            self._mains, \
+            self._echelons, \
+            self._commands = self.writeImages(self._counterScale)
 
-    # Loop over defined boards
-    for on, o in oobc:
-        widget = add_mapwidget(root,tabs, entryName=on)
-        gpid = add_oob(root,widget,on,o,gpid,
-                       verbose = verbose,
-                       hotkey  = key('B',ALT),
-                       visible = visible)
 
-    return gpid
+    def run(self):
+        super(LaTeXExporter,self).run(self._vmodname,self._patch)
+        
+            
 
-# --------------------------------------------------------------------
-def add_markproto(root,protos,key,value):
-    decs = [ mark_trait(key,value.capitalize()),
-             basic_trait('','','') ]
-    typs = [d[0] for d in decs]
-    stts = [d[1] for d in decs]
-    typ  = enc_parts(*typs)
-    stt  = enc_parts(*stts)
-    body = add_proto(typ,stt)
-    add_prototype(root,protos,value+' prototype',body)
-                  
-# --------------------------------------------------------------------
-def add_counters(root,game,categories,
-                 unittypes,echelons,commands,verbose=False):
-    '''Add all counters to the module.
+    # ================================================================
+    def createProcess(self,args):
+        '''Spawn a process and pipe output here
 
-    This will add all counters to the module, as well as prototypes
-    for each sub-category.
+        Parameters
+        ----------
+        args : list
+            List of process command line elements
+        Returns
+        -------
+        pipe : subprocess.Pipe
+            Pipe to read from 
+        '''
+        from os import environ
+        from subprocess import Popen, PIPE
 
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    game : xml.dom.minidom.Element
-        The game element
-    categories : dict
-        Catalogue of images 
-    '''
-    # Short hand 
-    # Create prototypes container before adding pieces 
-    protos = add_prototypes(root,game)
-    for utype in unittypes:  add_markproto(root,protos,'Type',utype)
-    for ech   in echelons:   add_markproto(root,protos,'Echelon',ech)
-    for cmd   in commands:   add_markproto(root,protos,'Command',cmd)
+        return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
 
-    # Stating global piece ID
-    gpid  = 20
+    # ----------------------------------------------------------------
+    def addPws(self,opw=None,upw=None):
+        '''Add a `Pws` element to arguments
 
-    # A window to hold the pieces 
-    piecew = add_piecewindow(root,game,'Counters',
-                             hotkey = key('C',ALT))
-    # Tab container for each sub-category
-    piecet = add_tabs(root,piecew,entryName='Counters')
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Pws
+            The added element
+        '''
+        '''Add password options'''
+        args = []
+        if upw is not None:  args.extend(['-upw',upw])
+        if opw is not None:  args.extend(['-opw',opw])
+        return args
+            
+    # ----------------------------------------------------------------
+    def getPdfInfo(self,upw=None,opw=None,timeout=None):
+        '''Get information about the PDF
 
-    if verbose:
-        print('Adding counters and prototypes')
+        Parameters
+        ----------
+        opw : str
+            Owner password (optional)
+        upw : str
+            User password (optional)
+        timeout : int
+            Time out in miliseconds for subprocesses
+
+        Returns
+        -------
+        info : dict
+             Image information 
+        '''
+        args = ['pdfinfo', self._pdfname ]
+        args.extend(self.addPws(opw=opw,upw=upw))
+
+        with VerboseGuard(f'Getting information from PDF {self._pdfname}'):
+            proc = self.createProcess(args)
+            try:
+                out, err = proc.communicate(timeout=timeout)
+            except:
+                proc.kill()
+                proc.communicate()
+                raise RuntimeError(f'Failed to get PDF info: {e}')
+            
+            d = {}
+            for field in out.decode('utf8','ignore').split('\n'):
+                if field == '':
+                    continue
+                subfields  = field.split(':')
+                key, value = subfields[0], ':'.join(subfields[1:])
+                if key != '':
+                    d[key] = (int(value.strip()) if key == 'Pages'
+                              else value.strip())
+            
+            if 'Pages' not in d:
+                raise ValueError(f'Page count not found from {self._pdfname}')
+
+        return d
+
+    # ----------------------------------------------------------------
+    def getImagesInfo(self):
+        '''Read in JSON information, and return as dictionary'''
+        from json import load
+
+        with VerboseGuard(f'Getting information from JSON {self._infoname}'):
+            with open(self._infoname) as file:
+                info = load(file)
+
+        return info
+
+    # ================================================================
+    def convertPage(self,page,opw=None,upw=None,timeout=None):
+        '''Convert a page from PDF into an image (bytes)
+
+        Parameters
+        ----------
+        page : int
+            Page number in the PDF to convert 
+        opw : str
+            Owner password (optional)
+        upw : str
+            User password (optional)
+        timeout : int
+            Time out in miliseconds for subprocesses
+
+        Returns
+        -------
+        info : dict
+             Image information 
+        '''
+        args = ['pdftocairo',
+                '-transp',
+                '-singlefile',
+                '-f', str(page),
+                '-l', str(page),
+                '-png' ]
+        args.extend(self.addPws(opw=opw,upw=upw))
+        args.append(self._pdfname)
+        args.append('-')
+
+        proc = self.createProcess(args)
+
+        try:
+            out, err = proc.communicate(timeout=timeout)
+        except Exception as e:
+            proc.kill()
+            proc.communicate()
+            raise RuntimeError(f'Failed to convert page {page} of '
+                               f'{self._pdfname}: {e}')
+
+        # Just return the bytes
+        return out
         
-    # Loop over sub-categories 
-    for subn, subc in categories.get('counter',{}).items():
-        # Friendly message
-        if verbose:
-            print(f' Adding {subn} counters and prototype')
+        
+    # ----------------------------------------------------------------
+    def ignoreEntry(self,info,ignores=['<<dummy>>','<<eol>>']):
+        '''Check if we should ignore an entry in the JSON file'''
+        return info['category'] in ignores
 
-        # Create a panel for this sub-category 
-        piecep = add_panel(root,piecet,
-                           entryName = subn,
-                           fixed     = False)
-        # Add a list to the panel
-        piecel = add_list(root,piecep,
-                          entryName=f'{subn} counters')
+    # ----------------------------------------------------------------
+    def scaleImage(self,buffer,factor):
+        from PIL import Image
+        from io import BytesIO
+        from math import isclose
 
-        if verbose:
-            print(f'  Prototype: {subn} prototype')
-            # Create prototype for this sub-category 
-        proto  = add_prototype(root,protos,
-                               name        = f'{subn} prototype',
-                               code        = proto_body(subn),
-                               description = f'Prototype for {subn} counters')
+        if isclose(factor,1): return buffer
 
-        # Loop over counters in this sub-category 
-        for cn, c in subc.items():
-            # Flipped counters are not added directly 
-            if cn.endswith('flipped'): continue
+        # print(f'Scaling image by factor {factor}')
+        with Image.open(BytesIO(buffer)) as img:
+            w, h = img.width, img.height
+            cpy  = img.resize((int(factor*w),int(factor*h)))
 
-            if verbose:
-                print(f'  counter: {cn}')
-                
-            # Get image bounding box 
-            bb = get_bb(c['img'])
+            with BytesIO() as out:
+                cpy.save(out,format='PNG')
+                return out.getvalue()
 
-            # Get flipped image, if any 
-            cf = subc.get(cn + ' flipped', None)
+        
+    # ----------------------------------------------------------------
+    def convertPages(self,opw=None,upw=None,timeout=None):
+        '''Reads in JSON and pages from PDF and stores information
+        dictionary, which is returned
 
-            # Create a slot for this piece 
-            sl = add_pieceslot(root,piecel,
-                               entryName = cn,
-                               gpid      = gpid,
-                               code      = piece_body(subn,c,cf,gpid),
-                               height    = bb[3]-bb[1],
-                               width     = bb[2]-bb[0])
+        Parameters
+        ----------
+        opw : str
+            Owner password (optional)
+        upw : str
+            User password (optional)
+        timeout : int
+            Time out in miliseconds for subprocesses
 
-            # Store the global piece identifier  - do we need this?
-            c['gpid'] = gpid
+        Returns
+        -------
+        info : dict
+             Image information 
+        '''
+        oargs    = {'opw':opw,'upw':upw }
+        docinfo  = self.getPdfInfo()
+        imgsinfo = self.getImagesInfo()
 
-            # Increment global piece identifier 
-            gpid += 1
+        if len(imgsinfo) - 1 != docinfo['Pages']:
+            raise RuntimeError(f'Number of pages in {pdfname} '
+                               f'(doc["Pages"]) not matched in JSON '
+                               f'{self._infoname} -> {len(imagesinfo)}')
 
-    # Return next free piece identifier 
-    return gpid
+        with VerboseGuard(f'Converting {docinfo["Pages"]} '
+                          f'pages in {self._pdfname}') as v:
+            for i,info in enumerate(imgsinfo):
+                if self.ignoreEntry(info): continue
 
-# --------------------------------------------------------------------
-def add_charts(root,game,categories,verbose=False):
-    '''Add all boards to the module
+                if i == 0: v(end='')
+                v(f'[{info["number"]}]',end=' ',flush=True)
+                info['img'] = self.convertPage(info['number'],**oargs)
 
-    This will select all 'chart' elements from the catalogue and add
-    them as individual (popup) panels to the module.
+            v('done')
+
+        return imgsinfo
+
+    # ----------------------------------------------------------------
+    def getBB(self,buffer):
+        '''Get bounding box of image
     
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    game : xml.dom.minidom.Element
-        The game element
-    categories : dict
-        Catalogue of images
+        Parameters
+        ----------
+        buffer : bytes
+             The image bytes
+    
+        Returns
+        -------
+        ulx, uly, lrx, lry : tuple
+             The coordinates of the bounding box 
+        '''
+        from PIL import Image
+        from io import BytesIO
+    
+        with Image.open(BytesIO(buffer)) as img:
+            bb  = img.getbbox()
+    
+        return bb
 
-    '''
-    chartc = categories.get('chart',{}).get('all',{}).items()
-    if len(chartc) < 1:
-        return
+    # ----------------------------------------------------------------
+    def getWH(self,buffer):
+        '''Get bounding box of image
     
-    # Short hand 
-    cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)
-    cw = lambda parent,tag,**attr : create_elem(root,parent,vw(tag),**attr)
+        Parameters
+        ----------
+        buffer : bytes
+             The image bytes
+    
+        Returns
+        -------
+        ulx, uly, lrx, lry : tuple
+             The coordinates of the bounding box 
+        '''
+        from PIL import Image
+        from io import BytesIO
+    
+        with Image.open(BytesIO(buffer)) as img:
+            w, h  = img.width, img.height
+    
+        return w,h
+    
+    # ----------------------------------------------------------------
+    def getOutline(self,buffer):
+        '''Get bounding box of image
+    
+        Parameters
+        ----------
+        buffer : bytes
+             The image bytes
+    
+        Returns
+        -------
+        ulx, uly, lrx, lry : tuple
+             The coordinates of the bounding box 
+        '''
+        from PIL import Image
+        from io import BytesIO
 
-    if verbose:
-        print('Adding charts')
+        # print(buffer)
+        with Image.open(BytesIO(buffer)) as img:
+            bb  = img.getbbox()
 
-    charts = add_chartwindow(root,game, 'Charts',
-                             hotkey      = key('A',ALT),
-                             description = 'Charts',
-                             tooltip     = 'Show/hide Charts')
-    tabs = add_tabs(root,charts,'Charts')
+            for r in range(bb[0],bb[2]):
+                for c in range(bb[1],bb[3]):
+                    pass #print(img.getpixel((c,r)))
     
+        return None
     
-    # Loop over defined charts
-    for cn, c in chartc:
-        if verbose:
-            print(f' Chart {cn} ({c["filename"]})')
-        add_chart(root,tabs,
-                  chartName = cn,
-                  description = cn,
-                  fileName = c['filename'])
+            
+    # ================================================================
+    def writeImages(self,counterScale=1):
+        '''From the information gathered about the images (including
+        their bitmap representation, generate image files in the
+        module
 
-# --------------------------------------------------------------------
-def add_diceroll(root,game,categories,verbose=False):
-    '''Add a dice button (1d6)
-    
-    Parameters
-    ----------
-    root : xml.dom.minidom.Document
-        Root of the document
-    game : xml.dom.minidom.Element
-        The game element
-    categories : dict
-        Catalogue of images
-    '''
-    if verbose:
-        print('Adding dice')
+        '''
+        categories = {}
+        unittypes  = []
+        echelons   = []
+        commands   = []
+        
+        with VerboseGuard(f'Writing images in VMod '
+                          f'{self._vmod.fileName()}',end=' ') as v:
+            for info in self._info:
+                if self.ignoreEntry(info): continue
+            
+                typ = info.get('category','counter')
+                sub = info.get('subcategory','all')
+            
+                info['filename'] = info['name'].replace(' ','_') + '.png'
+                imgfn            = 'images/'+info['filename']
+                if imgfn not in self._vmod.getFileNames():
+                    if typ == 'counter':
+                        # print(f'Possibly scale file {imgfn}')
+                        info['img'] = self.scaleImage(info['img'],
+                                                      counterScale)
+                    # self.message(f'Writing image {imgfn}')
+                    self._vmod.addFile(imgfn,info['img'])
+            
+                if sub == '':
+                    info['subcategory'] = 'all'
+                    sub                 = 'all'
+            
+                # Add into catalogue 
+                if typ not in categories:
+                    v('')
+                    v(f'Adding category "{typ}"')
+                    v('',end=' ')
+                    categories[typ] = {}
+                cat = categories[typ]
+            
+                if sub not in cat:
+                    v('')
+                    v(f'Adding sub-category "{sub}"')
+                    v('',end=' ')
+                    cat[sub] = {}
+                tgt = cat[sub]
 
-    add_dice(root,game,
-             hotkey      = key('6',ALT),
-             nDice       = 1,
-             nSides      = 6,
-             text        = '1d6',
-             tooltip     = 'Roll a 1d6',
-             name        = '1d6')
+                v(f'[{info["name"]}]',end=' ',flush=True,noindent=True)
+                #self.message(f'Adding "{info["name"]}" to catalogue')
+                tgt[info['name']] = info
 
-    
-# ====================================================================
-def create_build(info,
-                 categories,
-                 unittypes,
-                 echelons,
-                 commands,
-                 zipfile,
-                 title,
-                 version,
-                 desc     = '',
-                 rules    = None,
-                 tutorial = None,
-                 pdffile  = '',
-                 infofile = '',
-                 verbose  = False,
-                 visible  = False):
-    '''Create the buildFile.xml document
+                if self._nonato: continue
+                
+                # Get NATO App6c information, if any
+                natoapp6c = info.get('natoapp6c',None)
+                if natoapp6c is not None:
+                    from re import sub
+                    def clean(s):
+                        return sub('.*=','',
+                                   (sub(r'\[[^]]+\]','',s.strip())
+                                    .replace('{','')
+                                    .replace('}','')
+                                    .replace('/',' '))).strip()
+                    mains   = clean(natoapp6c.get('main',   ''))
+                    lower   = clean(natoapp6c.get('lower',  ''))
+                    upper   = clean(natoapp6c.get('upper',  ''))
+                    echelon = clean(natoapp6c.get('echelon',''))
+                    command = clean(natoapp6c.get('command',''))
 
-    Parameters
-    ----------
-    info : dict
-         Information dicationary
-    categories : dict
-         Catalogue of images
-    unittypes : set
-    	Set of used unit types
-    zipfile : zipfile.ZipFile
-         Module archive
-    title : str
-         Title of module
-    version : str
-         Version number of module
-    desc : str
-         Short description of the game
-    rules : str
-         Filename of PDF rules document (if any)
+                    
+                    if mains is not None:
+                        if len(lower) > 0: mains += ' '+lower
+                        if len(upper) > 0: mains += ' '+upper
+                        mains = sub(r'\[[^]]+\]','',mains)\
+                            .replace('{','').replace('}','')#.split(',')
+                        unittypes.append(mains.replace(',',' '))
+                        unittypes.extend([s.strip().replace(',',' ')
+                                          for s in mains.split(',')])
+                        #if len(mains) > 1:
+                        #    unittypes.append('+'.join(mains))
+                        info['mains'] = mains
+                        
+                    if len(echelon) > 0:
+                        echelons.append(echelon)
+                        info['echelon'] = echelon
+            
+                    if len(command) > 0:
+                        commands.append(command)
+                        info['command'] = command
+            
+            
+            # Finished loop over infos. Make unit types, echelons,
+            # commands unique  
+            v('done')
 
-    Returns
-    -------
-    xml : str
-        The encoded document
-    '''
-    from xml.dom.minidom import Document
+        return categories, set(unittypes), set(echelons), set(commands)
 
-    if verbose:
-        print(f'Creating buildFile.xml: {title},{version}')
+    # ================================================================
+    def createModuleData(self):
+        '''Create the `moduleData` file in the module
+        '''
+        with VerboseGuard(f'Creating module data'):
+            self._moduleData = ModuleData()
+            data = self._moduleData.addData()
+            data.addVersion      (version=self._version)
+            data.addVASSALVersion(version=self._vassalVersion)
+            data.addName         (name=self._title)
+            data.addDescription  (description=self._description)
+            data.addDateSaved    ()
+        
+    # ================================================================
+    def createBuildFile(self,
+                        ignores = '(.*markers?|all|commons|.*hidden|[ ]+)'):
+        '''Create the `buildFile.xml` file in the module.
 
-    # Create document 
-    root = Document()
+        Parameters
+        ----------
+        ignores : str
+            Regular expression to match ignored categories for factions
+            determination. Python's re.fullmatch is applied to this
+            regular exression against chit categories.  If the pattern
+            is matched, then the chit is not considered to belong to a
+            faction.
 
-    # Add game element 
-    game = add_game(root,root,
-                    name = title,
-                    version = version,
-                    description = desc)
+        '''
+        from re import fullmatch, IGNORECASE
+        with VerboseGuard(f'Creating build file') as v:
+            self._build = BuildFile() # 'buildFile.xml')
+            self._game  = self._build.addGame(name        = self._title,
+                                              version     = self._version,
+                                              description = self._description)
+            doc = self.addDocumentation()
+            self._game.addBasicCommandEncoder()
+            
+            # Extract the sides
+            self._sides = [ k
+                            for k in self._categories.get('counter',{}).keys()
+                            if fullmatch(ignores, k, IGNORECASE) is None]
+            v(f'Got sides: {", ".join(self._sides)}')
+            
+            v(f'Adding Global options')
+            go = self._game.addGlobalOptions(
+                autoReport         = GlobalOptions.PROMPT,
+                centerOnMove       = GlobalOptions.PROMPT,
+                nonOwnerUnmaskable = GlobalOptions.PROMPT,
+                playerIdFormat     = '$playerName$')
+            go.addOption(name='undoHotKey',value=key('Z'))
+            go.addOption(name='undoIcon',  value='/images/Undo16.gif')
+            # go.addOptoin(name='stepHotKey',value='')
+            go.addBoolPreference(name    = self._verbose,
+                                 default = True,
+                                 desc    = 'Be verbose',
+                                 tab     = self._title)
+            go.addBoolPreference(name    = self._debug,
+                                 default = False,
+                                 desc    = 'Show debug chat messages',
+                                 tab     = self._title)
+            go.addBoolPreference(name    = self._autoOdds,
+                                 default = False,
+                                 desc    = 'Calculate Odds on battle declaration',
+                                 tab     = self._title)
+            go.addBoolPreference(name    = self._autoResults,
+                                 default = False,
+                                 desc    = 'Resolve battle results automatically',
+                                 tab     = self._title)
+            go.addBoolPreference(name    = self._noClearMoves,
+                                 default = False,
+                                 desc    = ('Do not remove moved markers '
+                                            'on phase change'),
+                                 tab     = self._title)
+            go.addBoolPreference(name    = self._noClearBattles,
+                                 default = False,
+                                 desc    = ('Do not remove battle markers '
+                                            'on phase change'),
+                                 tab     = self._title)
+                                            
+            v(f'Adding player roster')
+            roster = self._game.addPlayerRoster()
+            for side in self._sides:
+                roster.addSide(side)
+            
+            v(f'Adding global properties')
+            glob = self._game.addGlobalProperties()
+            glob.addProperty(name='TurnTracker.defaultDocked',
+                             initialValue=True)
 
-    # Extract the sides of the game from the counter sub-categories
-    # Should be a little more clever about the case here
-    dsides   = {k.lower() : k for k in categories.get('counter',{}).keys()}
-    sides    = None
-    for skip in ['all','common', 'marker', 'markers']:
-        if skip in dsides: del dsides[skip]
-        sides = list(dsides.values())
-    if verbose:
-        print('Identified factions: ',sides)
+            self._battleMarks   = self._categories\
+                                      .get('counter',{})\
+                                      .get('BattleMarkers',[])
+            if len(self._battleMarks) > 0:
+                v(f'We have battle markers')
+
+                glob.addProperty(name         = self._battleCounter,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 min          = 0,
+                                 max          = len(self._battleMarks),
+                                 wrap         = True,
+                                 description  = 'Counter of battles')
+                glob.addProperty(name         = self._currentBattle,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 min          = 0,
+                                 max          = len(self._battleMarks),
+                                 wrap         = True,
+                                 description  = 'Current battle number')
+                glob.addProperty(name         = self._currentAttacker,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 min          = 0,
+                                 max          = 1,
+                                 wrap         = True,
+                                 description  = 'Current unit is attacker')
+                glob.addProperty(name         = self._battleAF,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 description  = 'Current battle AF')
+                glob.addProperty(name         = self._battleDF,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 description  = 'Current battle DF')
+                glob.addProperty(name         = self._battleFrac,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 description  = 'Current battle fraction')
+                glob.addProperty(name         = self._battleShift,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 description  = 'Current battle odds shift')
+                glob.addProperty(name         = self._battleOdds,
+                                 initialValue = '',
+                                 isNumeric    = False,
+                                 description  = 'Current battle odds')
+                glob.addProperty(name         = self._battleResult,
+                                 initialValue = '',
+                                 isNumeric    = False,
+                                 description  = 'Current battle results')
+                glob.addProperty(name         = self._battleIdx,
+                                 initialValue = 0,
+                                 isNumeric    = True,
+                                 description  = 'Current battle odds index')
+            
+            self._oddsMarks   = self._categories\
+                                      .get('counter',{})\
+                                      .get('OddsMarkers',[])
+            if len(self._oddsMarks) > 0:
+                v(f'We have odds markers')
+                
+            self._resultMarks   = self._categories\
+                                      .get('counter',{})\
+                                      .get('ResultMarkers',[])
+            if len(self._resultMarks) > 0:
+                v(f'We have result markers')
+              
+            self.addNotes()
+            v(f'Adding turn track')
+            turns = self._game.addTurnTrack(name='Turn',
+                                            counter={
+                                                'property': 'Turn',
+                                                'phases': {
+                                                    'property': 'Phase',
+                                                    'names': self._sides } })
+            turns.addHotkey(hotkey = self._clearMoved+'Phase',
+                            name   = 'Clear moved markers')
+            if len(self._battleMarks) > 0:
+                turns.addHotkey(hotkey = self._clearBattlePhs,
+                                name   = 'Clear battle markers')
+
+            self._dice   = self._categories\
+                               .get('die-roll',{})
+            if len(self._dice) > 0:
+                v(f'We have symbolic dice')
+                self._diceInit = []
+                # from pprint import pprint 
+                # pprint(self._dice,depth=3)
+                for die, faces in self._dice.items():
+                    ico  = self.getIcon(die+'-die-icon','')
+                    # print(f'Die {die} icon="{ico}"')
+                               
+                    dmin = +100000
+                    dmax = -100000
+                    symb = self._game.addSymbolicDice(
+                        name         = die+'Dice',
+                        text         = die if ico == '' else '',
+                        icon         = ico,
+                        tooltip      = f'{die} die roll',
+                        format       = (f'{{"{die} die roll: "+result1'
+                                        # f'+" <img src=\'{die}-"+result1'
+                                        # f'+".png\' width=24 height=24>"'
+                                        f'}}'),
+                        resultWindow = True);
+                    sdie = symb.addDie(name = die);
+                    for face, fdata in faces.items():
+                        fn   = fdata['filename']
+                        val  = int(fn.replace('.png','').replace(die+'-',''))
+                        dmin = min(dmin,val)
+                        dmax = min(dmax,val)
+                        sdie.addFace(icon  = fn,
+                                     text  = str(val),
+                                     value = val);
+
+                    self._diceInit.extend([
+                        GlobalPropertyTrait(
+                            ['',self._diceInitKey,
+                             GlobalPropertyTrait.DIRECT,
+                             f'{{{dmin}}}'],
+                            name        = die+'Dice_result',
+                            numeric     = True,
+                            min         = dmin,
+                            max         = dmax,
+                            description = f'Initialize {die}Dice'),
+                        ReportTrait(
+                            self._diceInitKey,
+                            report=(f'{{{self._debug}?("Initialize '
+                                    f'{die}Dice_result to {dmin}"):""}}'))
+                    ])
+                    
+
+                # Add start-up key
+                self._game.addStartupMassKey(
+                    name        = 'Initialise dice results',
+                    hotkey      = self._diceInitKey,
+                    target      = '',
+                    filter      = f'{{BasicName=="{self._hiddenName}"}}',
+                    whenToApply = StartupMassKey.EVERY_LAUNCH,
+                    reportFormat=f'{{{self._debug}?("Init Dice results"):""}}')
+                
+                                
+                                
+
+                
+            
+            self.addKeybindings(doc)
+            self.addCounters()
+            self.addInventory()
+            self.addBoards()
+            self.addDeadMap()
+            self.addOOBs()
+            self.addCharts()
+            self.addDie()
+
+    # ----------------------------------------------------------------
+    def addDocumentation(self):
+        '''Add documentation to the module.  This includes rules,
+        key-bindings, and about elements.
+        '''
+        with VerboseGuard('Adding documentation') as v:
+            doc = self._game.addDocumentation()
+            if self._rules is not None:
+                self._vmod.addExternalFile(self._rules,'rules.pdf')
+                doc.addBrowserPDFFile(title   = 'Show rules',
+                                      pdfFile = 'rules.pdf')
+            
+            if self._tutorial is not None:
+                self._vmod.addExternalFile(self._tutorial,'tutorial.vlog')
+                doc.addTutorial(name            = 'Tutorial',
+                                logfile         = 'tutorial.vlog',
+                                launchOnStartup = True)
+            
+            
+            fronts    = self._categories.get('front',{}).get('all',[])
+            front     = list(fronts.values())[0] if len(fronts) > 0 else None
+            if front is not None:
+                v(f'Adding about page')
+                doc.addAboutScreen(title=f'About {self._title}',
+                                   fileName = front['filename'])
+
+            return doc
+
+    # ----------------------------------------------------------------
+    def addKeybindings(self,doc):
+        keys = [
+            ['Alt-A',	'-',	'Show the charts panel'],
+            ['Alt-B',	'-',	'Show the OOBs'],
+            ['Alt-C',	'-',	'Show the counters panel'],
+            ['Alt-E',	'-',	'Show the eliminated units'],
+            ['Alt-I',	'-',	'Show/refresh inventory window'],
+            ['Alt-M',	'-',	'Show map'],
+            ['Alt-T',	'-',	'Increase turn track'],
+            ['Alt-Shift-T', '-',	'Decrease turn track'],
+            ['Alt-6',	'-',	'Roll the dice'],
+            ['Ctrl-D',	'Board,Counter','Delete counters'],
+            ['Ctrl-E',	'Board,Counter','Eliminate counters'],
+            ['Ctrl-F',	'Board,Counter','Flip counters'],
+            ['Ctrl-M',	'Board,Counter','Toggle "moved" markers'],
+            ['Ctrl-O',	'Board',	'Hide/show counters'],
+            ['Ctrl-R',	'Board,Counter','Restore unit'],
+            ['Ctrl-T',	'Board,Counter','Toggle move trail'],
+            ['Ctrl-Z',	'Board',        'Undo last move'],
+            ['Ctrl-+',	'Board',	'Zoom in'],
+            ['Ctrl--',	'Board',	'Zoom out'],
+            ['Ctrl-=',	'Board',	'Select zoom'],
+            ['Ctrl-Shift-O',	'Board','Show overview map'],
+            ['←,→,↑↓','Board',
+             'Scroll board left, right, up, down (slowly)'],
+            ['PnUp,PnDn','Board',	     'Scroll board up/down (fast)'],
+            ['Ctrl-PnUp,Ctrl-PnDn','Board',  'Scroll board left/right (fast)'],
+            ['Mouse-scroll up/down',	'Board',  'Scroll board up//down'],
+            ['Shift-Mouse-scroll up/down','Board','Scroll board right/leftown'],
+            ['Ctrl-Mouse-scroll up/down','Board','Zoom board out/in'],
+            ['Mouse-2',	'Board',	'Centre on mouse']]
+        if self._battleMarks:
+            for a,l in zip(['Ctrl-D','Ctrl-Shift-O', 'Ctrl-+', 'Ctrl-+'],
+                           [['Ctrl-C',      'Counter', 'Clear battle'],
+                            ['Ctrl-Shift-C','Board',   'Clear all battle'],
+                            ['Ctrl-X',      'Board,Counter','Mark battle'],
+                            ['Ctrl-Y',      'Board,Counter','Resolve battle'],
+                            ]):
+                ks   = [k[0] for k in keys]
+                didx = ks.index(a)
+                keys.insert(didx,l)
+            
+        self._vmod.addFile('help/keys.html',
+                           Documentation.createKeyHelp(
+                               keys,
+                               title=self._title,
+                               version=self._version))
+        doc.addHelpFile(title='Key bindings',fileName='help/keys.html')
         
-    # Use the standard command encoder
-    add_basiccommandencoder(root,game)
+    # ----------------------------------------------------------------
+    def addNatoPrototypes(self,prototypes):
+        # Add unit categories as prototypes 
+        for n,c in zip(['Type','Echelon','Command'],
+                       [self._mains, self._echelons, self._commands]):
+            sc = set([cc.strip() for cc in c])
+            with VerboseGuard(f'Adding prototypes for "{n}"') as vv:
+                for i,cc in enumerate(sc):
+                    cc = cc.strip()
+                    if len(cc) <= 0: continue
+                    vv(f'[{cc}] ',end='',flush=True,noindent=True)
+                    p = prototypes.addPrototype(name        = f'{cc} prototype',
+                                                description = '',
+                                                traits      = [MarkTrait(n,cc),
+                                                               BasicTrait()])
+                vv('')
+        
+    # ----------------------------------------------------------------
+    def addBattleControlPrototype(self,prototypes):
+        # Control of battles.
+        #
+        # This has traits to
+        #
+        # - Zero battle counter
+        # - Increment battle counter
+        # - Set current battle number
+        # - Mark battle
+        # - Calculate odds
+        #
+        # When wgMarkBattle is issued to this piece, then
+        #
+        # - Increment battle counter
+        # - Set global current battle
+        # - Trampoline to GCK markBattle
+        #   - For all selected pieces, issue markBattle
+        #     - All wgBattleUnit pieces then
+        #       - Get current battle # and store
+        #       - Add marker on top of it self
+        # - Issue calculateOddsAuto
+        #   - If auto odds on, go to calcOddsStart,
+        #     - Trampoline to GCK calcOddsAuto
+        #       - Which sends calcOddsStart to all markers
+        #         - For each mark
+        #           - Set current battle to current global
+        #           - Trampoline calcOdds via GKC
+        #             - Send calcBattleOdds to wgBattleCalc
+        #               - Zero odds
+        #               - Calculate fraction
+        #                 - Zero fraction
+        #                 - Calculate total AF
+        #                   - Zero AF
+        #                   - via trampoline to GKC
+        #                 - Calculate total DF
+        #                   - Zero DF
+        #                   - via trampoline to GKC
+        #                 - Real fraction calculation
+        #                   - From calculate fraction 
+        #                   - Access via calculate trait
+        #               - Calculate shift
+        #                 - Zero shift
+        #                 - Trampoline to GKC
+        #                 - Access via calculate trait
+        #               - Calculate index
+        #                 - Via calculated OddsIndex
+        #               - Calculate odds real
+        #                 - Via calculated Index to odds 
+        #           - Do markOddsAuto which selects between odds
+        #             - Do markOddsReal+OddsIndex
+        #               - Set global battle #
+        #               - Place marker
+        #                 - Take global battle #
+        #            - De-select all other marks to prevent
+        #              further propagation
+        #              
+        if len(self._battleMarks) <= 0:
+            return False
 
-    # Add document parts
-    doc = add_documentation(root,game)
-    add_splash(root, doc, categories, title,verbose)
-    add_rules(root, doc, zipfile, rules, verbose)
-    add_tut(root, doc, zipfile, tutorial, verbose)
-    notes = add_notes(root,doc,zipfile,title,version,
-                      sides,pdffile,infofile,rules,verbose)
+        n = len(self._battleMarks)
+        # --- Battle counter control - reset and increment -----------
+        traits = [
+            GlobalPropertyTrait(
+                ['',self._zeroBattle,GlobalPropertyTrait.DIRECT,'{0}'],
+                ['',self._incrBattle,GlobalPropertyTrait.DIRECT,
+                 f'{{({self._battleCounter}%{n})+1}}'],
+                name        = self._battleCounter,
+                numeric     = True,
+                min         = 0,
+                max         = n,
+                wrap        = True,
+                description = 'Zero battle counter',
+            ),
+            GlobalPropertyTrait(
+                ['',self._setBattle,GlobalPropertyTrait.DIRECT,
+                 f'{{{self._battleCounter}}}'],
+                name        = self._currentBattle,
+                numeric     = True,
+                min         = 0,
+                max         = n,
+                wrap        = True,
+                description = 'Zero battle counter',
+            ),
+            ReportTrait(self._zeroBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": zero battle counter: "'
+                                f'+{self._battleCounter}):""}}')),
+            ReportTrait(self._incrBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": '
+                                f'increment battle counter: "'
+                                f'+{self._battleCounter}):""}}')),
+            ReportTrait(self._setBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": set current battle: "+'
+                                f'{self._battleCounter}+" -> "+'
+                                f'{self._currentBattle}):""}}')),
+            GlobalHotkeyTrait(name         = '',
+                              key          = self._markBattle+'Trampoline',
+                              globalHotkey = self._markBattle,
+                              description  = 'Mark selected for battle'),
+            ReportTrait(self._markBattle+'Trampoline',
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": forward mark battle: "+'
+                                f'{self._battleCounter}):""}}')),
+            GlobalHotkeyTrait(name         = '',
+                              key          = self._calcBattleOdds+'Start',
+                              globalHotkey = self._calcBattleOdds+'Auto',
+                              description  = 'Trampoline to global'),
+            ReportTrait(self._calcBattleOdds+'Start',
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": forward odds: "+'
+                                f'{self._battleCounter}):""}}')),
+            DeselectTrait(command    = '',
+                          key        = self._calcBattleOdds+'Deselect',
+                          deselect   = DeselectTrait.ALL),
+            ReportTrait(self._calcBattleOdds+'Deselect',
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": select only this: "+'
+                                f'{self._battleCounter}):""}}')),
+            TriggerTrait(command    = '',
+                         key        = self._calcBattleOdds+'Auto',
+                         actionKeys = [self._calcBattleOdds+'Start'],
+                         property   = f'{{{self._autoOdds}==true}}'),
+            ReportTrait(self._calcBattleOdds+'Auto',
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": forward odds: "+'
+                                f'{self._battleCounter}):""}}')),
+            TriggerTrait(command    = '',
+                         key        = self._markBattle,
+                         actionKeys = [self._incrBattle,
+                                       self._setBattle,
+                                       self._markBattle+'Trampoline',
+                                       self._calcBattleOdds+'Auto']),
+            ReportTrait(self._markBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": mark battle: "+'
+                                f'{self._battleCounter}):""}}')),
+            GlobalHotkeyTrait(name         = '',
+                              key          = self._clearAllBattle+'Trampoline',
+                              globalHotkey = self._clearAllBattle,
+                              description  = 'Mark selected for battle'),
+            TriggerTrait(command    = '',
+                         key        = self._clearAllBattle,
+                         actionKeys = [self._clearAllBattle+'Trampoline',
+                                       self._zeroBattle]),
+            ReportTrait(self._clearBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": clear battle: "+'
+                                f'{self._battleCounter}):""}}')),
+            GlobalHotkeyTrait(name         = '',
+                              key          = self._clearMoved+'Trampoline',
+                              globalHotkey = self._clearMoved,
+                              description  = 'Clear moved markers'),
+            MarkTrait(name=self._battleCtrl,value=True),            
+            BasicTrait()]
+        prototypes.addPrototype(name        = self._battleCtrl,
+                                description = '',
+                                traits      = traits)
+        return True
 
+    # ----------------------------------------------------------------
+    def addBattleCalculatePrototype(self,prototypes):
+        # --- Batttle AF, DF, Odds -----------------------------------
+        # This calculate odds derivation from stated odds. 
+        calcIdx   = 0
+        maxIdx    = len(self._oddsMarks)+1
+        minIdx    = 0
+        idx2Odds  = '""'
+        calcFrac  = 1
+        if len(self._oddsMarks) > 0:
+            odds = [o.replace('odds marker','').strip() for
+                    o in self._oddsMarks]
+            ratios = all([':' in o for o in odds])
+            if ratios: # All is ratios!
+                def calc(s):
+                    num, den = [float(x.strip()) for x in s.split(':')]
+                    return num/den
+                ratios = [[calc(s),s] for s in odds]
+                ind    = [i[0] for i in sorted(enumerate(ratios),
+                                               key=lambda x:x[1][0])]
+                #print(f'=== Rations: {ratios}, Index: {ind[::-1]}')
+                calcIdx  = ':'.join([f'{self._battleFrac}>={ratios[i][0]}?'
+                                     f'({i+1})'
+                                     for i in ind[::-1]]) + ':0'
+                idx2Odds = ':'.join([f'OddsIndex=={i+1}?'
+                                   f'"{ratios[i][1]}"'
+                                   for i in ind[::-1]]) + ':""'
+                calcFrac = (f'{{{self._battleDF}==0?0:'
+                            f'(((double)({self._battleAF}))'
+                            fr'\/{self._battleDF})}}')
+                #print(calcIdx,idx2Odds)
+            else:
+                try:
+                    nums     = [[int(o),o] for o in odds]
+                    calcFrac = f'{{{self._battleAF}-{self._battleDF}}}'
+                    ind      = [i[0] for i in sorted(enumerate(nums),
+                                                     key=lambda x:x[1])]
+                    calcIdx  = ':'.join([f'{self._battleFrac}>={nums[i][0]}?'
+                                         f'({i+1})'
+                                         for i in ind[::-1]])+':0'
+                    idx2Odds = ':'.join([f'OddsIndex=={i+1}?"{nums[i][1]}"'
+                                         for i in ind[::-1]]) + ':""'
+                    vidx2Odds = '\t'+idx2Odds.replace(':',':\n\t')
+                    #print(f'Index to odds: {vidx2Odds}')
+                except:
+                    pass 
+                
+        traits = [
+            CalculatedTrait(# This should be changed to game rules
+                name        = 'OddsShift',
+                expression  = f'{{{self._battleShift}}}',
+                description = 'Calculated internal oddsshift'),
+            CalculatedTrait(# This should be changed to game rules
+                name        = 'OddsIndexRaw',
+                expression  = f'{{{calcIdx}}}',
+                description = 'Calculated internal odds index'),
+            CalculatedTrait(# This should be changed to game rules
+                name        = 'OddsIndexLimited',
+                expression  = (f'{{OddsIndexRaw>{maxIdx}?{maxIdx}:'
+                               f'OddsIndexRaw<{minIdx}?{minIdx}:'
+                               f'OddsIndexRaw}}'),
+                description = 'Calculated internal limited odds index'),
+            CalculatedTrait(# This should be changed to game rules
+                name        = 'OddsIndex',
+                expression  = (f'{{OddsIndexLimited+OddsShift}}'),
+                description = 'Calculated internal odds index (with shift)'),
+            CalculatedTrait(# This should be changed to game rules
+                name        = 'BattleFraction',
+                expression  = calcFrac,
+                description = 'Calculated fraction off battle'),
+            GlobalPropertyTrait(
+                ['',self._zeroBattleShft,GlobalPropertyTrait.DIRECT,'{0}'],
+                name        = self._battleShift,
+                numeric     = True,
+                description = 'Zero battle odds shift',
+            ),
+            GlobalPropertyTrait(
+                ['',self._zeroBattleAF,GlobalPropertyTrait.DIRECT,'{0}'],
+                name        = self._battleAF,
+                numeric     = True,
+                description = 'Zero battle AF',
+            ),
+            GlobalPropertyTrait(
+                ['',self._zeroBattleDF,GlobalPropertyTrait.DIRECT,'{0}'],
+                name        = self._battleDF,
+                numeric     = True,
+                description = 'Zero battle AF',
+            ),
+            # {wgBattleDF==0?0:(double(wgBattleAF)/wgBattleDF)}
+            GlobalPropertyTrait(
+                ['',self._zeroBattleFrac,GlobalPropertyTrait.DIRECT,'{0}'],
+                ['',self._calcBattleFrac+'Real',GlobalPropertyTrait.DIRECT,
+                 '{BattleFraction}'],
+                name        = self._battleFrac,
+                description = 'Calculate battle fraction',
+            ),
+            GlobalPropertyTrait(
+                ['',self._zeroBattleIdx,GlobalPropertyTrait.DIRECT,'{0}'],
+                ['',self._calcBattleIdx,GlobalPropertyTrait.DIRECT,
+                 '{OddsIndex}'],
+                name        = self._battleIdx,
+                description = 'Calculate battle odds index',
+            ),
+            GlobalPropertyTrait(
+                ['',self._zeroBattleOdds,GlobalPropertyTrait.DIRECT,'{""}'],
+                ['',self._calcBattleOdds+'Real',GlobalPropertyTrait.DIRECT,
+                 f'{{{idx2Odds}}}'],
+                name        = self._battleOdds,
+                description = 'Calculate battle odds',
+            ),
+            GlobalHotkeyTrait(name         = '',# Forward to units
+                              key          = self._calcBattleAF+'Trampoline',
+                              globalHotkey = self._calcBattleAF,
+                              description  = 'Calculate total AF'),
+            GlobalHotkeyTrait(name         = '',# Forward to units
+                              key          = self._calcBattleDF+'Trampoline',
+                              globalHotkey = self._calcBattleDF,
+                              description  = 'Calculate total DF'),
+            GlobalHotkeyTrait(name         = '',# Forward to units
+                              key          = self._calcBattleShft+'Trampoline',
+                              globalHotkey = self._calcBattleShft,
+                              description  = 'Calculate total shift'),
+            TriggerTrait(command        = '',
+                         key            = self._calcBattleAF,
+                         actionKeys     = [self._zeroBattleAF,
+                                           self._calcBattleAF+'Trampoline']),
+            TriggerTrait(command        = '',
+                         key            = self._calcBattleDF,
+                         actionKeys     = [self._zeroBattleDF,
+                                           self._calcBattleDF+'Trampoline']),
+            TriggerTrait(command        = '',
+                         key            = self._calcBattleShft,
+                         actionKeys     = [self._zeroBattleShft,
+                                           self._calcBattleShft+'Trampoline']),
+            TriggerTrait(command        = '',
+                         key            = self._calcBattleFrac,
+                         actionKeys     = [self._zeroBattleFrac,
+                                           self._calcBattleAF,
+                                           self._calcBattleDF,
+                                           self._calcBattleFrac+'Real']),
+            TriggerTrait(command        = '',
+                         key            = self._calcBattleOdds,
+                         actionKeys     = [self._zeroBattleOdds,
+                                           self._calcBattleFrac,
+                                           self._calcBattleShft,
+                                           self._calcBattleIdx,
+                                           self._calcBattleOdds+'Real']),
+            ReportTrait(self._zeroBattleAF,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Reset AF: "+'
+                                f'{self._battleAF}):""}}')),
+            ReportTrait(self._zeroBattleDF,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Reset DF: "+'
+                                f'{self._battleDF}):""}}')),
+            ReportTrait(self._zeroBattleFrac,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Reset fraction: "+'
+                                f'{self._battleFrac}):""}}')),
+            ReportTrait(self._zeroBattleOdds,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Reset odds: "+'
+                                f'{self._battleOdds}):""}}')),
+            ReportTrait(self._calcBattleAF,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Total AF: "+'
+                                f'{self._battleAF}):""}}')),
+            ReportTrait(self._calcBattleDF,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Total DF: "+'
+                                f'{self._battleDF}):""}}')),
+            ReportTrait(self._calcBattleShft,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Battle odds shift: "+'
+                                f'{self._battleShift}):""}}')),
+            ReportTrait(self._calcBattleFrac,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Battle fraction: "+'
+                                f'{self._battleFrac}):""}}')),
+            ReportTrait(self._calcBattleOdds,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Battle odds: "+'
+                                f'{self._battleOdds}+" ("+'
+                                f'{self._battleIdx}+")"):""}}')),
+            ReportTrait(self._calcBattleFrac+'Real',
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Battle fraction: "+'
+                                f'{self._battleFrac}+'
+                                f'" AF="+{self._battleAF}+'
+                                f'" DF="+{self._battleDF}'
+                                f'):""}}')),
+            ReportTrait(self._calcBattleOdds+'Real',
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+": Battle odds: "+'
+                                f'{self._battleOdds}+'
+                                f'" ("+{self._battleIdx}+","+OddsShift+","+'
+                                f'" raw="+OddsIndexRaw+","+'
+                                f'" limited="+OddsIndexLimited+","+'
+                                f'" -> "+OddsIndex+","+'
+                                f'{self._battleShift}+")"+'
+                                f'" Fraction="+{self._battleFrac}+'
+                                f'" AF="+{self._battleAF}+'
+                                f'" DF="+{self._battleDF}'
+                                f'):""}}')),
+            ReportTrait(self._calcBattleOdds+'Real',
+                        report=(f'{{{self._verbose}?'
+                                f'("! Battle # "'
+                                f'+{self._battleNo}'
+                                f'+" AF="+{self._battleAF}'
+                                f'+" DF="+{self._battleDF}'
+                                f'+" => "+{self._battleOdds}'
+                                # f'+" <img src=\'odds_marker_"'
+                                # f'+{self._battleOdds}+".png\' "'
+                                # f'+" width=24 height=24>"'
+                                f'):""}}')),
+            MarkTrait(name=self._battleCalc,value=True),            
+            BasicTrait()]
+        prototypes.addPrototype(name        = self._battleCalc,
+                                description = '',
+                                traits      = traits)
+        
+    # ----------------------------------------------------------------
+    def addBattleUnitPrototype(self,prototypes):
+        # --- Battle units that set battle markers -------------------
+        # 
+        # - Trait to add battle number 1 to max
+        #
+        # - Trigger trait for each of these using the global property
+        #   for the current battle
+        #
+        traits = [
+            DynamicPropertyTrait(['',self._getBattle,
+                                  DynamicPropertyTrait.DIRECT,
+                                  f'{{{self._currentBattle}}}'],
+                                 ['',self._clearBattle,
+                                  DynamicPropertyTrait.DIRECT,
+                                  f'{{-1}}'],
+                                 name        = self._battleNo,
+                                 numeric     = True,
+                                 value       = f'{{-1}}',
+                                 description = 'Set battle number'),
+            GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT,
+                                 '{IsAttacker}'],
+                                name        = self._currentAttacker,
+                                numeric     = True,
+                                description = 'Set attacker'),
+            ReportTrait(self._getBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+" current battle # "+'
+                                f'{self._currentBattle}+" -> "+'
+                                f'{self._battleNo}):""}}')),
+            ReportTrait(self._clearBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+" Clear this global="+'
+                                f'{self._currentBattle}+" this="+'
+                                f'{self._battleNo}):""}}')),
+        ]
+        place  = []
+        trig   = []
+        rept   = []
+        for i, bm in enumerate(self._battleMarks):
+            kn   = self._markBattle+str(i+1)
+            path = PlaceTrait.SKEL_PATH.format('BattleMarkers',bm)
 
-    # Create roster or player sides
-    roster   = add_roster(root,game)
-    for s in sides:
-        add_entry(root,roster,s)
+            place.append(
+                PlaceTrait(command       = '',#f'Add battle marker {i}',
+                           key           = kn,
+                           markerSpec    = path,
+                           markerText    = 'null',
+                           xOffset       = -8,
+                           yOffset       = -16,
+                           matchRotation = False,
+                           afterKey      = self._getBattle,
+                           gpid          = self._game.nextPieceSlotId(),
+                           description   = f'Add battle marker {i+1}',
+                           placement     = PlaceTrait.ABOVE,
+                           above         = False))
+            trig.append(
+                TriggerTrait(command     = '',#Mark battle',
+                             key         = self._markBattle,
+                             actionKeys  = [self._getBattle,
+                                            self._setBattle,kn],
+                             property    = f'{{{self._currentBattle}=={i+1}}}'))
+            rept.append(
+                ReportTrait(kn,
+                            report=(f'{{{self._debug}?'
+                                    f'("~ "+BasicName+" placing marker ({i+1})'
+                                    f' ="+ {self._currentBattle}+"'
+                                    f'={kn}"):""}}')))
 
-    # Add global options - mainly defer to user settings
-    add_globaloptions(root,game,
-                      autoReport         = 'Use Preferences Setting',
-                      centerOnMove       = 'Use Preferences Setting',
-                      nonOwnerUnmaskable = 'Use Preferences Setting',
-                      playerIdFormat     = '$playerName$')
-    # Add global properties
-    gp = add_globalproperties(root,game)
-    # Add a turn tracker
-    tt = add_turntrack(root,game,'Turn',counter={
-        'property': 'Turn',
-        'phases': {
-            'property': 'Phase',
-            'names': sides }})
-    add_globalproperty(root,gp,'TurnTracker.defaultDocked',True)
-    
-    # Add all pieces to the module (including prototypes)
-    gpid = add_counters(root,game,categories,
-                        unittypes,echelons,commands,verbose)    
-    # Add an inventory
-    add_inv(root,game,sides)    
-    # Add all boards to the module (including eliminated map)
-    gpid = add_boards(root,game,categories,sides,gpid,
-                      verbose=verbose,visible=visible)
-    # Add all OOBs to the module 
-    gpid = add_oobs(root,game,categories,gpid,
-                    verbose=verbose,visible=visible)
-    # Add charts to the module 
-    add_charts(root,game,categories,verbose=verbose)
-    # Add a dice rool
-    add_diceroll(root,game,categories,verbose=verbose)
+        oth = [
+            GlobalHotkeyTrait(name         = 'Declare battle',
+                              key          = self._markKey,
+                              globalHotkey = self._markKey,
+                              description  = 'Mark for combat'),
+            GlobalPropertyTrait(
+                ['',self._calcBattleAF,GlobalPropertyTrait.DIRECT,
+                f'{{EffectiveAF+{self._battleAF}}}'],
+                name        = self._battleAF,
+                numeric     = True,
+                description = 'Update battle AF'),
+            GlobalPropertyTrait(
+                ['',self._calcBattleDF,GlobalPropertyTrait.DIRECT,
+                 f'{{EffectiveDF+{self._battleDF}}}'],
+                 name        = self._battleDF,
+                numeric     = True,
+                description = 'Update battle AF'),
+            GlobalPropertyTrait(
+                ['',self._calcBattleShft,GlobalPropertyTrait.DIRECT,
+                 f'{{OddsShift}}'],
+                name        = self._battleShift,
+                numeric     = True,
+                description = 'Update battle shift',
+            ),
+            CalculatedTrait(#This could be redefined in module 
+                name        = 'EffectiveAF',
+                expression  = '{CF}',
+                description = 'Current attack factor'),
+            CalculatedTrait(#This could be redefined in module 
+                name        = 'EffectiveDF',
+                expression  = '{DF}',
+                description = 'Current defence factor'),
+            CalculatedTrait(#This could be redefined in module 
+                name        = 'IsAttacker',
+                expression  = '{Phase.contains(Faction)}',
+                description = 'Check if current phase belongs to faction'),
+            CalculatedTrait(#This could be redefined in module 
+                name        = 'OddsShift',
+                expression  = f'{{{self._battleShift}}}',
+                description = 'Check if current phase belongs to faction'),
+            ReportTrait(self._calcBattleAF,
+                        report=(f'{{{self._verbose}?'
+                                f'("! "+BasicName+'
+                                f'" add "+EffectiveAF+'
+                                f'" to total attack factor -> "+'
+                                f'{self._battleAF}'
+                                f'):""}}')),
+            ReportTrait(self._calcBattleDF,
+                        report=(f'{{{self._verbose}?'
+                                f'("! "+BasicName+'
+                                f'" add "+EffectiveDF+'
+                                f'" to total defence factor -> "+'
+                                f'{self._battleDF}'
+                                f'):""}}')),
+            ReportTrait(self._calcBattleShft,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+'
+                                f'" Updating odds shift with "+OddsShift+'
+                                f'" -> "+{self._battleShift}):""}}')),
+        ]
+        traits.extend(
+            place+
+            trig+
+            oth+
+            [MarkTrait(name=self._battleUnit,value=True),
+             BasicTrait()])
+        prototypes.addPrototype(name        = self._battleUnit,
+                                description = '',
+                                traits      = traits)
+    # ----------------------------------------------------------------
+    def addBattleCorePrototype(self,prototypes):
+        # --- Core traits for battle markers (number, odds, results)
+        # - Set the global current battle number
+        # - Get the current global battle number
+        # - Clear this counter
+        # - Trampoline to global command to clear all marks for this battle
+        traits = [
+            # NoStackTrait(select     = NoStackTrait.NORMAL_SELECT,
+            #              move       = NoStackTrait.NORMAL_MOVE,
+            #              canStack   = False,
+            #              ignoreGrid = False),
+            GlobalPropertyTrait(['',self._setBattle,GlobalPropertyTrait.DIRECT,
+                                 f'{{{self._battleNo}}}'],
+                                name        = self._currentBattle,
+                                numeric     = True,
+                                description = 'Set current battle'),
+            GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT,
+                                 '{IsAttacker}'],
+                                name        = self._currentAttacker,
+                                numeric     = True,
+                                description = 'Set attacker'),
+            DynamicPropertyTrait(['',self._getBattle,
+                                  DynamicPropertyTrait.DIRECT,
+                                  f'{{{self._currentBattle}}}'],
+                                 name        = self._battleNo,
+                                 numeric     = True,
+                                 value       = f'{{{self._battleNo}}}',
+                                 description = 'Set battle number'),
+            DynamicPropertyTrait(['',self._getBattle,
+                                  DynamicPropertyTrait.DIRECT,
+                                  f'{{{self._currentAttacker}}}'],
+                                 name        = 'IsAttacker',
+                                 numeric     = True,
+                                 value       = 'false',
+                                 description = 'Set attacker'),
+            DeleteTrait('',self._clearBattle),
+            GlobalHotkeyTrait(name         = '',
+                              key          = self._clearBattle+'Trampo',
+                              globalHotkey = self._clearBattle,
+                              description  = 'Clear selected battle'),
+            TriggerTrait(command    = 'Clear',
+                         key        = self._clearKey,
+                         actionKeys = [self._setBattle,
+                                       self._clearBattle+'Trampo']),
+            ReportTrait(self._setBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+" battle # "+'
+                                f'{self._battleNo}+" -> "+'
+                                f'{self._currentBattle}):""}}')),
+            ReportTrait(self._getBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+" current battle # "+'
+                                f'{self._currentBattle}+" -> "+'
+                                f'{self._battleNo}):""}}')),
+            ReportTrait(self._clearBattle,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+" Clear this global="+'
+                                f'{self._currentBattle}+" this="+'
+                                f'{self._battleNo}):""}}')),
+            ReportTrait(self._clearKey,
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+" To clear battle # global="+'
+                                f'{self._currentBattle}+" this="+'
+                                f'{self._battleNo}):""}}')),
+            ReportTrait(self._clearBattle+'Trampo',
+                        report=(f'{{{self._debug}?'
+                                f'("~ "+BasicName+" '
+                                f'Forward clear battle # global="+'
+                                f'{self._currentBattle}+" this="+'
+                                f'{self._battleNo}):""}}')),
+            MarkTrait(name=self._battleMark,value=True),
+            BasicTrait()
+        ]
+        prototypes.addPrototype(name        = self._currentBattle,
+                                description = '',
+                                traits      = traits)
+        
+    # ----------------------------------------------------------------
+    def addBattlePrototypes(self,prototypes):
+        if not self.addBattleControlPrototype(prototypes):
+            return
+        
+        self.addBattleCalculatePrototype(prototypes)
+        self.addBattleUnitPrototype(prototypes)
+        self.addBattleCorePrototype(prototypes)
 
-    # Set next available piece identifier on the game 
-    game.setAttribute('nextPieceSlotId',str(gpid+1))
+    # ----------------------------------------------------------------
+    def markerTraits(self):
+        return [DeleteTrait(),
+                RotateTrait()]
 
-    # Return the document 
-    return root, notes
+    # ----------------------------------------------------------------
+    def battleMarkerTraits(self,c):
+        '''Derives from the CurrentBattle prototype and adds a submenu
+        to place odds counter on the battle marker'''
+        traits = [PrototypeTrait(name=self._currentBattle),
+                  NonRectangleTrait(filename = c['filename'],
+                                    image    = c['img'])]
+        
+        subs  = []
+        ukeys = []
+        place = []
+        trig  = []
+        rept  = []
+        repp  = []
+        for i, odds in enumerate(self._oddsMarks):
+            on   = odds.replace('odds marker','').strip()
+            om   = odds.replace(':',r'\:')
+            kn   = self._markOdds+str(i+1)
+            gpid = self._game.nextPieceSlotId()
+            path = PlaceTrait.SKEL_PATH.format('OddsMarkers',om)
+            subs.append(on)
 
-# --------------------------------------------------------------------
-def create_data(title,version,desc='',verbose=False):
-    '''Create the module data XML
+            place.append(
+                PlaceTrait(command       = '',
+                           key           = kn,
+                           markerSpec    = path,
+                           markerText    = 'null',
+                           xOffset       = -6,
+                           yOffset       = -8,
+                           matchRotation = False,
+                           afterKey      = self._getBattle+'Details',
+                           gpid          = gpid,
+                           placement     = PlaceTrait.ABOVE,
+                           description   = f'Add odds marker {on}'))
+            trig.append(
+                TriggerTrait(name        = '',
+                             command     = on,
+                             key         = kn+'real',
+                             actionKeys  = [
+                                 self._setBattle,
+                                 kn]))
+            rept.append(
+                ReportTrait(kn+'real',
+                            report=(f'{{{self._debug}?'
+                                    f'("~ "+BasicName+": Set odds '
+                                    f'{on} ({kn})"):""}}')))
+            repp.append(
+                ReportTrait(kn,
+                            report=(f'{{{self._debug}?'
+                                    f'("~ "+BasicName+": Place odds '
+                                    f'{on} ({kn})"):""}}')))
+            ukeys.append(kn+'real')
 
-    Parameters
-    ----------
-    title : str
-        Title of the game
-    version : str
-        Version number of the module
-    desc : str
-        Short description
-    
-    Returns
-    -------
-    xml : str
-        The encoded document
-    '''
-    from xml.dom.minidom import Document
-    from time import time
+        auto = []
+        auton = []
+        if len(self._oddsMarks) > 0:
+            auton = ['Auto']
+            for i, odds in enumerate(self._oddsMarks):
+                trig.append(
+                    TriggerTrait(name        = '',
+                                 command     = '',
+                                 key         = self._markOdds+'Auto',
+                                 property    = f'{{{self._battleIdx}=={i+1}}}',
+                                 actionKeys  = [self._markOdds+str(i+1)]))
 
-    if verbose:
-        print('Create module data') 
+            auto = [GlobalHotkeyTrait(name = '',
+                                      key          = self._calcBattleOdds,
+                                      globalHotkey = self._calcBattleOdds,
+                                      description  = 'Calculate fraction'),
+                    DeselectTrait(command    = '',
+                                  key        = self._calcBattleOdds+'Deselect',
+                                  deselect   = DeselectTrait.ONLY),
+                    ReportTrait(self._calcBattleOdds+'Deselect',
+                                report=(f'{{{self._debug}?'
+                                        f'("~ "+BasicName+": Select only this "'
+                                        f'+" Attacker="+IsAttacker'
+                                        f'):""}}')),
+                    TriggerTrait(name        = '',
+                                 command     = '',
+                                 key         = self._markOdds+'Trampoline',
+                                 actionKeys  = [
+                                     self._calcBattleOdds,
+                                     self._markOdds+'Auto',
+                                     self._calcBattleOdds+'Deselect'],
+                                 property    = f'{{!IsAttacker}}'
+                                 ),
+                    TriggerTrait(name        = '',
+                                 command     = 'Auto',
+                                 key         = self._calcBattleOdds+'Start',
+                                 actionKeys  = [
+                                     self._setBattle,
+                                     self._markOdds+'Trampoline',
+                                 ]),
+                    ReportTrait(self._calcBattleOdds+'Start',
+                                report=(f'{{{self._debug}?'
+                                        f'("~ "+BasicName+": Battle odds "+'
+                                        f'{self._battleOdds}):""}}')),
+                    ReportTrait(self._markOdds+'Auto',
+                                report=(f'{{{self._debug}?'
+                                        f'("~ "+BasicName+": '
+                                        f'Auto battle odds "+'
+                                        f'{self._battleOdds}):""}}'))
+                    ]
+            
+        traits.extend([
+            RestrictCommandsTrait(
+                name='Hide when auto-odds are enabled',
+                hideOrDisable = RestrictCommandsTrait.HIDE,
+                expression    = f'{{{self._autoOdds}==true}}',
+                keys          = ukeys)]+
+                      place
+                      +trig
+                      +auto
+                      +rept
+                      +repp)
+        if len(subs) > 0:
+            traits.extend([
+                SubMenuTrait(subMenu = 'Odds',
+                             keys    = auton+subs),
+            ])
+
+        return traits
+
+    # ----------------------------------------------------------------
+    def oddsMarkerTraits(self,c=None):
+        '''Derives from the CurrentBattle prototype and adds a submenu
+        to replace odds counter with result marker'''
+        traits = [PrototypeTrait(name=self._currentBattle),
+                  NonRectangleTrait(filename = c['filename'],
+                                    image    = c['img']),
+                  DynamicPropertyTrait(
+                      ['',self._getBattle+'More',DynamicPropertyTrait.DIRECT,
+                       (f'{{{self._battleAF}+" vs "+{self._battleDF}+'
+                        f'" (odds "+{self._battleOdds}+" shift "+'
+                        f'{self._battleShift}+")"}}')],
+                      name = 'BattleDetails',
+                      value = '',
+                      numeric = False,
+                      description = 'Stored battle details'),
+                  TriggerTrait(command = '',
+                               key     = self._getBattle+'Details',
+                               actionKeys = [self._getBattle,
+                                             self._getBattle+'More'])]
+
+        subs  = []
+        place = []
+        trig  = []
+        rept  = []
+        ukeys = []
+        first = ''
+        for i, result in enumerate(self._resultMarks):
+            r    = result.replace('result marker','').strip()
+            kn   = self._markResult+str(i+1)
+            gpid = self._game.nextPieceSlotId()
+            ukeys.append(kn+'real')
+            subs.append(r)
+            if first == '': first = r
+            
+            path = PlaceTrait.SKEL_PATH.format('ResultMarkers',result)
+
+            place.append(
+                ReplaceTrait(command    = '',
+                             key        = kn,
+                             markerSpec = path,
+                             markerText = 'null',
+                             xOffset    = -6,
+                             yOffset    = -8,
+                             matchRotation = False,
+                             afterKey      = self._getBattle,
+                             gpid          = gpid,
+                             description   = f'Add result marker {r}'))
+            trig.append(
+                TriggerTrait(name        = '',
+                             command     = r,
+                             key         = kn+'real',
+                             actionKeys  = [
+                                 self._setBattle,
+                                 kn]))
+            rept.append(
+                ReportTrait(kn+'real',
+                            report=(f'{{{self._debug}?'
+                                    f'("~ "+BasicName+" setting result '
+                                    f'{r}"):""}}')))
+
+        auto = []
+        auton = []
+        if len(self._resultMarks) > 0:
+            auton = ['Auto']
+            for i, res in enumerate(self._resultMarks):
+                r = res.replace('result marker','').strip()
+                trig.append(
+                    TriggerTrait(
+                        name        = '',
+                        command     = '',
+                        key         = self._markResult+'Auto',
+                        property    = f'{{{self._battleResult}=="{r}"}}',
+                        actionKeys  = [self._markResult+str(i+1)]))
+
+            auto = [ # Override in the module
+                CalculatedTrait(
+                    name = 'Die',
+                    expression = '{GetProperty("1d6_result")}',
+                    description = 'Die roll'),
+                GlobalHotkeyTrait(
+                    name         = '',
+                    key          = self._rollDice,
+                    globalHotkey = self._diceKey,
+                    description  = 'Roll dice'),        
+                CalculatedTrait(
+                    name       = 'BattleResult',
+                    expression = f'{{"{first}"}}',
+                ),
+                GlobalPropertyTrait(
+                    ['',self._calcBattleRes+'real',GlobalPropertyTrait.DIRECT,
+                     '{BattleResult}'],
+                    name = self._battleResult,
+                    numeric = False,
+                    description = 'Set combat result'),
+                TriggerTrait(name        = '',
+                             command     = 'Resolve',
+                             key         = self._resolveKey,
+                             property    = f'{{{self._autoResults}==true}}',
+                             actionKeys  = [
+                                 self._setBattle,
+                                 self._rollDice,
+                                 self._calcBattleRes+'real',
+                                 self._markResult+'Auto',
+                             ]),
+                ReportTrait(self._calcBattleRes,
+                            report=(f'{{{self._debug}?'
+                                    f'("~ "+BasicName+": Battle result "+'
+                                    f'{self._battleOdds}):""}}')),
+                ReportTrait(self._markResult+'Auto',
+                            report=(f'{{{self._debug}?'
+                                    f'("~ "+BasicName+": Auto battle result "+'
+                                    f'{self._battleResult}):""}}')),
+                ReportTrait(self._markResult+'Auto',
+                            report=(f'{{"` Battle # "+{self._battleNo}+": "+'
+                                    f'BattleDetails+'
+                                    f'" with die roll "+Die+": "+'
+                                    f'{self._battleResult}'
+                                    # f'+ "<img src=\'result_marker_"'
+                                    # f'+{self._battleResult}+".png\'"'
+                                    # f'+" width=24 height=24>"'
+                                    f'}}')),
+                MarkTrait(name=self._battleOddsM,value='true')
+            ]
+
+        traits.extend(
+            [RestrictCommandsTrait(
+                name='Hide when auto-results are enabled',
+                hideOrDisable = RestrictCommandsTrait.HIDE,
+                expression    = f'{{{self._autoResults}==true}}',
+                keys          = ukeys)]+
+            place
+            +trig
+            +auto)
         
-    # The document
-    root = Document()
+        if len(subs) > 0:
+            traits.append(SubMenuTrait(subMenu = 'Result',
+                                       keys    = subs))
 
-    # Top of the document
-    data = root.createElement('data')
-    data.setAttribute("version","1")
-    root.appendChild(data)
+        return traits
 
-    # Version number of module 
-    vers = root.createElement('version')
-    vers.appendChild(root.createTextNode(str(version)))
-    data.appendChild(vers)
+    # ----------------------------------------------------------------
+    def resultMarkerTraits(self,c=None):
+        traits = [PrototypeTrait(name=self._currentBattle),
+                  NonRectangleTrait(filename = c['filename'],
+                                    image    = c['img'])]
 
-    # Name of the game 
-    name = root.createElement('name')
-    name.appendChild(root.createTextNode(title))
-    data.appendChild(name)
+        return traits
 
-    # VASSAL version (should get this in some other way)
-    vvers = root.createElement('VassalVersion')
-    vvers.appendChild(root.createTextNode("3.6.7"))
-    data.appendChild(vvers)
+    # ----------------------------------------------------------------
+    def factionTraits(self,faction):
+        traits = [ReportTrait(self._eliminateKey,
+                              self._restoreKey,
+                              self._trailKey),
+                  TrailTrait(),
+                  RotateTrait(),
+                  MovedTrait(xoff = int( 36 * self._counterScale),
+                             yoff = int(-38 * self._counterScale)),
+                  DeleteTrait(),
+                  SendtoTrait(mapName     = 'DeadMap',
+                              boardName   = f'{faction} pool',
+                              name        = 'Eliminate',
+                              key         = self._eliminateKey,
+                              restoreName = 'Restore',
+                              restoreKey  = self._restoreKey,
+                              description = 'Eliminate unit'),
+                  PrototypeTrait(name=self._battleUnit),                 
+                  MarkTrait(name='Faction',value=faction)]
 
-    # Description, if any 
-    desc = root.createElement('description')
-    data.appendChild(desc)
+        return traits
 
-    # Automatically set the save data 
-    save = root.createElement('dateSaved')
-    save.appendChild(root.createTextNode(f'{int(time())}'))
-    data.appendChild(save)
+    # ----------------------------------------------------------------
+    def getFactors(self,val):
+        cf = None
+        mf = None
+        df = None
+        ra = None
+        try:
+            if 'chit 1 factor' in val:
+                vv = val.replace('chit 1 factor=','')
+                cf = int(vv)
+            elif 'chit 2 factors artillery' in val:
+                vv       = val.replace('chit 2 factors artillery=','')
+                cf,mf,ra = [int(v) for v in vv.strip('=').split()]
+            elif 'chit 2 factors' in val:
+                vv    = val.replace('chit 2 factors=','')
+                cf,mf = [int(v) for v in vv.split()]
+            elif 'chit 3 factors' in val:
+                vv       = val.replace('chit 3 factors=','')
+                cf,df,mf = [int(v) for v in vv.split()]
 
-    return root
+            # Set defensive factor combat factor if not defined. 
+            if df is None and cf is not None:
+                df = cf
+                
+            
+        except Exception as e:
+            print(f'\nWarning when extracting factors: {e} '
+                  f'in "{val}" -> "{vv}"')
+            return None,None,None,None
+            pass
 
+        return cf,df,mf,ra
+        
+    # ----------------------------------------------------------------
+    def pieceTraits(self,subn,subc,cn,c):
+        from re import sub
 
-# ====================================================================
-def create_vmod(vmodname,
-                info,
-                title,
-                version,
-                desc     = '',
-                rules    = None,
-                tutorial = None,
-                pdfname  = '',
-                infoname = '',
-                patch    = None,
-                verbose  = False,
-                visible  = False):
-    '''Create a VASSAL module from the information passed
+        bb     = self.getBB(c['img'])
+        height = bb[3]-bb[1] if bb is not None else 1
+        width  = bb[2]-bb[0] if bb is not None else 1
+        cf     = subc.get(cn + ' flipped', None)
+        traits = [PrototypeTrait(name=f'{subn} prototype')]
 
-    Parameters
-    ----------
-    vmodname : str
-        Name of the module
-    info : list
-        List of images and image data
-    title : str
-        Name of the game
-    version : str
-        Version of the module
-    desc : str
-        Short description of the game
-    rules : str
-        Rules PDF (optional)
+        def clean(s):
+            return s.strip().replace(',',' ').replace('/',' ').strip()
+        
+        if not self._nonato:
+            mains = c.get('mains','')
+            m     = set([clean(mains)] +
+                        [clean(m) for m in mains.split(',')])
+            traits.extend([PrototypeTrait(name=f'{m.strip()} prototype')
+                           for m in set(m)])
+            for p in ['echelon','command']:
+                val = c.get(p,None)
+                if val is not None:
+                    pv = f'{val.strip()} prototype'
+                    traits.append(PrototypeTrait(name=pv))
+                    
+        if cf is not None:
+            traits.extend([
+                LayerTrait(images = [c['filename'],
+                                     cf['filename']],
+                           newNames = ['','Reduced +'],
+                           activateName = '',
+                           decreaseName = '',
+                           increaseName = 'Flip',
+                           increaseKey  = self._flipKey,
+                           decreaseKey  = '',
+                           name         = 'Step'),
+                ReportTrait(self._flipKey)])
 
-    Returns
-    -------
-    '''
-    from zipfile import ZipFile, ZIP_DEFLATED
-    from pprint import pprint
+        if not self._nochit:
+            def clean(value):
+                return sub(r'\[[^=]+\]=','',value)\
+                    .replace('{','')\
+                    .replace('}','')\
+                    .replace('/',' ')\
+                    .replace(',',' ')\
+                    .replace('\\',' ')
+            
+            # Add extra marks.  This may be useful later on. 
+            for field in ['upper left', 'upper right', 
+                          'lower left', 'lower right',
+                          'left',       'right',
+                          'factors']:
+                value = c.get('chit',{}).get(field,None)
+                if value is None:
+                    continue
+                val = clean(value)
+                val = val\
+                    .replace('chit identifier=','')\
+                    .replace('chit small identifier=','')
+                
+                traits.append(MarkTrait(name = field, value = val))
 
-    categories = {}
-    unittypes  = []
-    echelons   = []
-    commands   = []
-    with ZipFile(vmodname,'w',compression=ZIP_DEFLATED) as vmod:
-        # Create images in ZIP file 
-        for i in info:
-            if ignore_entry(i): continue
+                if field != 'factors': continue
 
-            # Store the name of the image file in catalogue
-            i['filename'] = i['name'].replace(' ','_') + '.png'
-            imgfn = 'images/'+i['filename']
-            if imgfn not in vmod.namelist():
-                vmod.writestr(imgfn,i['img'])
+                af, df, mf, ra = self.getFactors(val)
+                saf, sdf, smf, sra = None,None,None,None
+                if cf is not None:
+                    value = cf.get('chit',{}).get(field,None)
+                    if value is not None:
+                        val = clean(value)
+                        val = val\
+                            .replace('chit identifier=','')\
+                            .replace('chit small identifier=','')
+                        saf, sdf, smf, sra = self.getFactors(val)
 
-            # Categorize - assume counter 
-            typ = i.get('category','counter')
-            sub = i.get('subcategory','all')
-            if sub == '':
-                i['subcategory'] = 'all'
-                sub              = 'all'
+                rf  = []
+                srf = []
+                for f,sf,n in [[af,saf,'CF'],
+                               [df,sdf,'DF'],
+                               [mf,smf,'MF'],
+                               [ra,sra,'Range']]:
+                    if f is None: continue
+                    
+                    if sf is None:
+                        rf.append(MarkTrait(name=n,value=f))
+                    else:
+                        rf .append(MarkTrait(name='Full'+n,   value=f))
+                        srf.append(MarkTrait(name='Reduced'+n,value=sf))
+                        traits.append(CalculatedTrait(
+                            name = n,
+                            expression = (f'{{(Step_Level==2)?'
+                                          f'Reduced{n}:Full{n}}}')))
 
-            # Add to catalogue 
-            if typ not in categories:
-                categories[typ] = {}
-            cat = categories[typ]
+                traits.extend(rf+srf)
+                        
 
-            if sub not in cat:
-                cat[sub] = {}
-            tgt = cat[sub]
+                
 
-            tgt[i['name']] = i
+                
+                        
+        return height, width, traits
+        
+    # ----------------------------------------------------------------
+    def addCounters(self):
+        '''Add all counters (pieces) element to the module.
+        Prototypes are also created as part of this.
+        '''
+        from re import sub
 
-            natoapp6c = i.get('natoapp6c',None)
-            if natoapp6c is not None:
-                from re import sub
-                mains   = natoapp6c.get('main',None)
-                echelon = natoapp6c.get('echelon',None)
-                command = natoapp6c.get('command',None)
+        with VerboseGuard('Adding counters') as v:
+            protos       =  self._game.addPrototypes()
+            
+            self.addNatoPrototypes(protos)
+            self.addBattlePrototypes(protos)
+            
+            pieces = self._game.addPieceWindow(name   = 'Counters',
+                                               icon   = self.getIcon('unit-icon',
+                                                                     '/images/counter.gif'),
+                                               hotkey = self._countersKey)
+            tabs   = pieces.addTabs(entryName='Counters')
+            
+            for subn, subc in self._categories.get('counter',{}).items():
+            
+                subn  = subn.strip()
+                panel = tabs.addPanel(entryName = subn, fixed = False)
+                plist = panel.addList(entryName = f'{subn} counters')
 
-                if mains is not None:
-                    mains = sub(r'\[[^]]+\]','',mains)\
-                        .replace('{','').replace('}','').split(',')
-                    unittypes.extend(mains)
-                    i['mains'] = mains
+                traits = []
+                if   subn in ['BattleMarkers']:
+                    traits = self.battleMarkerTraits(list(subc.values())[0])
+                elif subn in ['OddsMarkers']:
+                    traits = self.oddsMarkerTraits(list(subc.values())[0])
+                elif subn in ['ResultMarkers']:
+                    traits = self.resultMarkerTraits(list(subc.values())[0])
+                elif subn.lower() in ['marker', 'markers']:
+                    traits = self.markerTraits()
+                else:
+                    traits = self.factionTraits(subn)
+
+                traits.append(BasicTrait())
+                
+                p = protos.addPrototype(name        = f'{subn} prototype',
+                                        description = f'Prototype for {subn}',
+                                        traits      = traits)
+                v('')
+             
+                with VerboseGuard(f'Adding pieces for "{subn}"') as vv:
+                    for i, (cn, c) in enumerate(subc.items()):
+                        if cn.endswith('flipped'): continue
+
+                        if i == 0: v('',end='')
+                        vv(f'[{cn}',end='',flush=True,noindent=True)
                     
-                if echelon is not None:
-                    echelons.append(natoapp6c['echelon'])
-                    i['echelon'] = echelon
+                        height, width, traits = self.pieceTraits(subn,subc,cn,c)
+                        if cn == self._hiddenName:
+                            traits = [
+                                PrototypeTrait(name=self._battleCtrl),
+                                PrototypeTrait(name=self._battleCalc)]
+                            if self._diceInit is not None:
+                                traits.extend(self._diceInit)
+                            traits.append(
+                                RestrictAccessTrait(sides=[],
+                                                    description='Fixed'))
 
-                if command is not None:
-                    commands.append(command)
-                    i['command'] = command
+                            
+                        #if cn.startswith('odds marker'):
+                        #    cn  = cn.replace(':','_')
+                            
+                        gpid = self._game.nextPieceSlotId()
+                        traits.extend([BasicTrait(name     = c['name'],
+                                                  filename = c['filename'],
+                                                  gpid     = gpid)])
+                        
+                        ps = plist.addPieceSlot(entryName = cn,
+                                                gpid      = gpid,
+                                                height    = height, 
+                                                width     = width,
+                                                traits    = traits)
+                        if cn == self._hiddenName:
+                            self._hidden = ps
+                        vv('] ',end='',flush=True,noindent=True)
+                    
+                    vv('')
+            
 
-            # Friendly message
-            # if verbose:
-            #    print(f'{i["name"]} -> {i["type"]}/{i["sub type"]} {typ}/{sub}')
+    # ----------------------------------------------------------------
+    def addNotes(self,**kwargs):
+        '''Add a `Notes` element to the module
 
-        unittypes = set(unittypes)
-        echelons  = set(echelons)
-        commands  = set(commands)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        '''
+        self._game.addNotes(**kwargs)
         
-        # Now create the `buildFile.xml` file
-        build, notes = create_build(info,
-                                    categories,
-                                    unittypes,
-                                    echelons,
-                                    commands,
-                                    vmod,
-                                    title,
-                                    version,
-                                    desc,
-                                    rules,
-                                    tutorial,
-                                    pdfname,
-                                    infoname,
-                                    verbose,
-                                    visible)
-        # Now create the `moduledata` file 
-        data  = create_data(title, version, desc,verbose)
+    # ----------------------------------------------------------------
+    def addInventory(self,**kwargs):
+        '''Add a `Inventory` element to module
 
-    
-        if patch:
-            from importlib.util import spec_from_file_location, \
-                module_from_spec
-            from pathlib import Path
-            from sys import modules
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        '''
+        filt = '{' + '||'.join([f'Faction=="{s}"' for s in self._sides])+'}'
+        grp  = 'Faction,Command,Echelon,Type'
+        self._game.addInventory(include = filt,
+                                groupBy = grp,
+                                sortFormat  = '$PieceName$',
+                                tooltip     ='Show inventory of all pieces',
+                                zoomOn      = True,
+                                **kwargs)
 
-            p = Path(patch)
-            if verbose:
-                pn = str(p.stem)
-                print(f'Will patch module with {pn}.patch function')
+    # ----------------------------------------------------------------
+    def addBoard(self,name,info,hasFlipped=False):
+        '''Add a `Board` element to module
 
-            spec = spec_from_file_location(p.stem, p.absolute())
-            module = module_from_spec(spec)
-            spec.loader.exec_module(module)
-            modules[p.stem] = module
+        Parameters
+        ----------
+        name : str
+            Name of board
+        info : dict
+            Information on board image
+        hasFlipped : bool
+            True if any piece can be flipped 
+        '''
+        with VerboseGuard(f'Adding board {name}') as v:
+            map    = self._game.addMap(mapName=name,
+                                       markUnmovedHotkey=self._clearMoved)
+            map.addCounterDetailViewer(
+                propertyFilter=f'{{{self._battleMark}!=true}}')
+            map.addHidePiecesButton()
+            map.addGlobalMap()
+            # Basics
+            map.addStackMetrics()
+            map.addImageSaver()
+            map.addTextSaver()
+            map.addForwardToChatter()     
+            map.addMenuDisplayer()        
+            map.addMapCenterer()          
+            map.addStackExpander()        
+            map.addPieceMover()           
+            map.addKeyBufferer()          
+            map.addSelectionHighlighters()
+            map.addHighlightLastMoved()   
+            map.addZoomer()               
+            
+            map.addMassKey(name='Eliminate',
+                           buttonHotkey = self._eliminateKey,
+                           hotkey       = self._eliminateKey,
+                           icon         = self.getIcon('eliminate-icon',
+                                                       '/icons/16x16/edit-undo.png'),
+                           tooltip      = 'Eliminate selected units')
+            map.addMassKey(name='Delete',
+                           buttonHotkey = self._deleteKey,
+                           hotkey       = self._deleteKey,
+                           icon         = self.getIcon('delete-icon',
+                                                       '/icons/16x16/no.png'),
+                           tooltip      = 'Delete selected units')
+            map.addMassKey(name='Rotate CW',
+                           buttonHotkey = self._rotateCWKey,
+                           hotkey       = self._rotateCWKey,
+                           icon         = '', #/icons/16x16/no.png',
+                           tooltip      = 'Rotate selected units')
+            map.addMassKey(name='Rotate CCW',
+                           buttonHotkey = self._rotateCCWKey,
+                           hotkey       = self._rotateCCWKey,
+                           icon         = '', #/icons/16x16/no.png',
+                           tooltip      = 'Rotate selected units')
+            map.addMassKey(name='Phase clear moved markers',
+                           buttonHotkey = self._clearMoved+'Phase',
+                           hotkey       = self._clearMoved+'Trampoline',
+                           canDisable   = True,
+                           target       = '',
+                           filter       = f'{{{self._battleCtrl}==true}}',
+                           propertyGate = f'{self._noClearMoves}',
+                           icon         = '', #/icons/16x16/no.png',
+                           tooltip      = 'Phase clear moved markers',
+                           reportFormat = (f'{{{self._debug}?'
+                                           f'("~ {name}: '
+                                           f'Phase Clear moved markers "+'
+                                           f'{self._noClearMoves})'
+                                           f':""}}'))
+            if hasFlipped:
+                map.addMassKey(name='Flip',
+                               buttonHotkey = self._flipKey,
+                               hotkey       = self._flipKey,
+                               icon         = self.getIcon('flip-icon',
+                                                           '/images/Undo16.gif'),
+                               tooltip      = 'Flip selected units')
 
+            if len(self._battleMarks) > 0:
+                v(f'Adding battle mark interface')
+                ctrlSel = f'{{{self._battleCtrl}==true}}'
+                oddsSel = f'{{{self._battleMark}==true}}'
+                calcSel = f'{{{self._battleCalc}==true}}'
+                curSel  = (f'{{{self._battleNo}=={self._currentBattle}}}')
+                curAtt  = (f'{{{self._battleNo}=={self._currentBattle}&&'
+                           f'{self._battleUnit}==true&&'
+                           f'IsAttacker==true}}')
+                curDef  = (f'{{{self._battleNo}=={self._currentBattle}&&'
+                           f'{self._battleUnit}==true&&'
+                           f'IsAttacker==false}}')
+                curUnt  = (f'{{{self._battleNo}=={self._currentBattle}&&'
+                           f'{self._battleUnit}==true}}')
+                markSel = (f'{{{self._battleNo}=={self._currentBattle}&&'
+                           f'{self._battleMark}==true}}')
 
-            # Patch must accept xml.dom.document,xml.dom.document,ZipFile
-            module.patch(build,data,vmod,verbose)
+                # ctrlSel = '{BasicName=="wg hidden unit"}'
+                map.addMassKey(name         = 'User mark battle',
+                               buttonHotkey = self._markKey,
+                               buttonText   = '',
+                               hotkey       = self._markBattle,
+                               icon         = 'battle-marker-icon.png',
+                               tooltip      = 'Mark battle',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = ctrlSel,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'User marks battle # "+'
+                                               f'{self._currentBattle})'
+                                               f':""}}'))
+                map.addMassKey(name         = 'Selected mark battle',
+                               buttonHotkey = self._markBattle,
+                               hotkey       = self._markBattle,
+                               icon         = '',
+                               tooltip      = '',
+                               singleMap    = False,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Mark battle # "+'
+                                               f'{self._currentBattle})'
+                                               f':""}}'))
+                map.addMassKey(name         = 'Clear current battle',
+                               buttonText   = '',
+                               buttonHotkey = self._clearBattle,
+                               hotkey       = self._clearBattle,
+                               icon         = '',
+                               tooltip      = '',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = curSel,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Clear battle # "+'
+                                               f'{self._currentBattle})'
+                                               f':""}}'))
+                map.addMassKey(name         = 'Clear selected battle',
+                               buttonText   = '',
+                               buttonHotkey = self._clearKey,
+                               hotkey       = self._clearKey,
+                               icon         = '',
+                               tooltip      = '',
+                               singleMap    = False,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Clear battle # "+'
+                                               f'{self._currentBattle})'
+                                               f':""}}'))
+                map.addMassKey(name         = 'Clear all battles',
+                               buttonText   = '',
+                               buttonHotkey = self._clearAllBattle,
+                               hotkey       = self._clearBattle,
+                               icon         = '',
+                               tooltip      = '',
+                               target       = '',
+                               singleMap    = False,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Clear all battle markers")'
+                                               f':""}}'))
+                map.addMassKey(name         = 'User clear all battles',
+                               buttonText   = '',
+                               buttonHotkey = self._clearAllKey,
+                               hotkey       = self._clearAllBattle,
+                               icon         = 'clear-battles-icon.png',
+                               tooltip      = 'Clear all battles',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = ctrlSel,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'User clears battle markers")'
+                                               f':""}}'))
+                map.addMassKey(name         = 'Phase clear all battles',
+                               buttonText   = '',
+                               buttonHotkey = self._clearBattlePhs,
+                               hotkey       = self._clearAllBattle,
+                               icon         = '',
+                               tooltip      = 'Clear all battles',
+                               canDisable   = True,
+                               propertyGate = f'{self._noClearBattles}',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = ctrlSel,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Phase clears battle markers "+'
+                                               f'{self._noClearBattles})'
+                                               f':""}}'))
+                map.addMassKey(name         = 'Selected resolve battle',
+                               buttonHotkey = self._resolveKey,
+                               hotkey       = self._resolveKey,
+                               icon         = 'resolve-battles-icon.png',
+                               tooltip      = 'Resolve battle',
+                               singleMap    = False,
+                               filter       = oddsSel,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Resolve battle # "+'
+                                               f'{self._currentBattle})'
+                                               f':""}}'))
+                map.addMassKey(name         = 'Sum AFs',
+                               buttonText   = '',
+                               buttonHotkey = self._calcBattleAF,
+                               hotkey       = self._calcBattleAF,
+                               icon         = '',
+                               tooltip      = '',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = curAtt,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Calculate total AF"):""}}'))
+                map.addMassKey(name         = 'Sum DFs',
+                               buttonText   = '',
+                               buttonHotkey = self._calcBattleDF,
+                               hotkey       = self._calcBattleDF,
+                               icon         = '',
+                               tooltip      = '',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = curDef,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Calculate total DF"):""}}'))
+                map.addMassKey(name         = 'Sum odds shifts',
+                               buttonText   = '',
+                               buttonHotkey = self._calcBattleShft,
+                               hotkey       = self._calcBattleShft,
+                               icon         = '',
+                               tooltip      = '',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = curUnt,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Calculate odds shift"):""}}'))
+                map.addMassKey(name         = 'Calc battle odds',
+                               buttonText   = '',
+                               buttonHotkey = self._calcBattleOdds,
+                               hotkey       = self._calcBattleOdds,
+                               icon         = '',
+                               tooltip      = '',
+                               target       = '',
+                               singleMap    = False,
+                               filter       = calcSel,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Calculate odds"):""}}'))
+                map.addMassKey(name         = 'Auto calc battle odds',
+                               buttonText   = '',
+                               buttonHotkey = self._calcBattleOdds+'Auto',
+                               hotkey       = self._calcBattleOdds+'Start',
+                               icon         = '',
+                               tooltip      = '',
+                               # target       = '',
+                               singleMap    = False,
+                               filter       = markSel,
+                               reportFormat = (f'{{{self._debug}?'
+                                               f'("~ {name}: '
+                                               f'Auto calculate odds"):""}}')) 
+               
             
-        buildstr = build.toprettyxml(indent=' ',
-                                     encoding="UTF-8",
-                                     standalone=False)
-        datastr = data.toprettyxml(indent=' ',
-                                   encoding="UTF-8",
-                                   standalone=False)
+            ulx,uly,lrx,lry = self.getBB(info['img'])
+            width           = abs(ulx - lrx)
+            height          = abs(uly - lry)
+            width, height   = self.getWH(info['img'])
+            height          += 20
+            width           += 5
+                
+            picker = map.addBoardPicker()
+            board  = picker.addBoard(name   = name,
+                                     image  = info['filename'],
+                                     width  = width,
+                                     height = height)
+            zoned  = board.addZonedGrid()
+            zoned.addHighlighter()
+            
+            if not 'zones' in info:
+                full = zoned.addZone(name = full,
+                                     useParentGrid = False,
+                                     path=(f'{ulx},{uly};' +
+                                           f'{lrx},{uly};' +
+                                           f'{lrx},{lry};' +
+                                           f'{ulx},{lry}'))
+                grid = zone.addHexGrid(color   = color,
+                                       dx      = HEX_WIDTH,
+                                       dy      = HEX_HEIGHT,
+                                       visible = self._visible)
+                grid.addNumbering(color   = color,
+                                  hType   = 'A',
+                                  hOff    = -1,
+                                  vType   = 'N',
+                                  vOff    = -1,
+                                  visible = self._visible)
+                return
+            
+            w = abs(ulx-lrx)
+            h = abs(uly-lry)
+            self.addZones(zoned,name,info['zones'],w,h)
+
+            if self._hidden is not None:
+                v(f'Adding hidden unit to map {name}')
+                at = map.addAtStart(name            = self._hiddenName,
+                                    location        = '',
+                                    useGridLocation = False,
+                                    owningBoard     = name,
+                                    x               = 0,
+                                    y               = 0)
+                at.addPieces(self._hidden)
+            
+
+    # ----------------------------------------------------------------
+    def addDeadMap(self):
+        '''Add a "Dead Map" element to the module 
+        '''
+        name = 'DeadMap'
+        with VerboseGuard(f'Adding board {name}') as v:
+            map    = self._game.addMap(mapName       = name,
+                                       buttonName    = '',
+                                       markMoved     = 'Never',
+                                       launch        = True,
+                                       icon          = self.getIcon('pool-icon',
+                                                                    '/images/playerAway.gif'),
+                                       allowMultiple = True,
+                                       hotkey        = self._deadKey)
+            # Basics
+            map.addStackMetrics()
+            map.addImageSaver()
+            map.addTextSaver()
+            map.addForwardToChatter()     
+            map.addMenuDisplayer()        
+            map.addMapCenterer()          
+            map.addStackExpander()        
+            map.addPieceMover()           
+            map.addKeyBufferer()          
+            map.addSelectionHighlighters()
+            map.addHighlightLastMoved()   
+            map.addZoomer()               
+            
+            map.addMassKey(name='Restore',
+                           buttonHotkey = self._restoreKey,
+                           hotkey       = self._restoreKey,
+                           icon         = self.getIcon('restore-icon',
+                                                       '/images/Undo16.gif'),
+                           tooltip      = 'Restore selected units')
+            
+            picker = map.addBoardPicker()
+            picker.addSetup(maxColumns=len(self._sides),mapName=name,
+                            boardNames=[s+' pool' for s in self._sides])
+            
+            for i, s in enumerate(self._sides):
+                v(f'Adding {s} pool')
+                color        = [0,0,0,64]
+                color[i % 3] = 255
+                w            = 400
+                h            = 400
+                c            = rgba(*color)
+                img          = ''
+                dimg         = self._categories.get('pool',{}).get('all',{})\
+                                                              .get(s,None)
+            
+                if dimg:
+                    bb  = self.getBB(dimg['img'])
+                    w   = bb[2] - bb[0]
+                    h   = bb[3] - bb[1]
+                    c   = ''
+                    img = dimg['filename']
+                    v(f'Using image provided by user {img}')
+            
+                board  = picker.addBoard(name   = f'{s} pool',
+                                         image  = img,
+                                         width  = w,
+                                         height = h,
+                                         color  = c)
+            
+                if dimg is None or not 'zones' in dimg:
+                    continue
+            
+                zoned  = board.addZonedGrid()
+                zoned.addHighlighter()
+                w = abs(w)
+                h = abs(h)
+                self.addZones(zoned,board['name'],dimg['zones'],w,h)
+                
+        
+    # --------------------------------------------------------------------
+    def getPictureInfo(self,picture,name,width,height):
+        '''
+        Returns
+        -------
+        hex_width, hex_height : float, float
+            Scale hex width
+        scx, scy : float, float, float, float
+            Scale to image and picture (x,y)
+        rot90 : bool
+            True if rotated +/-90 degrees
+        tran : callable
+            Translation function
+        '''
+        if picture is None:
+            print(f'WARNING: No Tikz picture information.'
+                  f"Are you sure you used the `[zoned]' option for the "
+                  f"tikzpicture environment of {name}?")
+            f = lambda x,y: (x,y)
+            return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
+        
+        # Get picture bounding box
+        tll = picture['lower left']
+        tur = picture['upper right']
+        # Get picture transformation
+        pa  = picture['xx']
+        pb  = picture['xy']
+        pc  = picture['yx']
+        pd  = picture['yy']
+        # Get picture offset (always 0,0?)
+        pdx = picture['dx']
+        pdy = picture['dy']
+        # Define picture global transformation
+        pr  = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
+        # Globally transform (rotate) picture bounding box 
+        pll  = pr(*tll)
+        pur  = pr(*tur)
+        # Calculate widht, height, and scaling factors 
+        pw   = pur[0] - pll[0]
+        ph   = pur[1] - pll[1]
+        scw  = width / pw
+        sch  = height / ph
+        # Extract picture scales and rotation
+        # Courtesy of
+        # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
+        from math import sqrt, atan2, degrees, isclose
+        psx   = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
+        psy   = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
+        prt   = degrees(atan2(pc,pd))
+        if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
+            raise RuntimeException('Rotations of Tikz pictures other than '
+                                   '0 or +/-90,+/- 180, or +/-270 not supported. '
+                                   'found {prt}')
+        rot90      = int(prt // 90)
+        if rot90 == 2: rot90 = -2
+        # Now supported 
+        # if any([isclose(prt,a) for a in [90,270,180,-180]]):
+        #     print(f'WARNING: rotations by {prt} not fully supported')
+
+        from math import sqrt
+        hex_width  = psx * scw * 2  # HEX_WIDTH
+        hex_height = psy * sch * sqrt(3) # HEX_HEIGHT
+        with VerboseGuard('Picture') as v:
+            v(f'Transformations:        {pa},{pb},{pc},{pd}')
+            v(f'Scale (x,y):            {psx},{psy}')
+            v(f'Rotation (degrees):     {prt} ({rot90})')
+            v(f'Scale to pixels (x,y):  {scw},{sch}')
+    
+        # When translating the Tikz coordinates, it is important to note
+        # that the Tikz y-axis point upwards, while the picture y-axis
+        # point downwards.  This means that the upper right corner is at
+        # (width,0) and the lower left corner is at (0,height).
+        def tranx(x,off=-pll[0]):
+            # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
+            return int(scw * (x + off)+.5)
+        def trany(y,off=-pur[1]):
+            # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
+            return -int(sch * (y + off)+.5)
+        tran  = lambda x,y : (tranx(x), trany(y))
+
+        return hex_width, hex_height, scw * psx, sch * psy, rot90, tran
+        
+    # --------------------------------------------------------------------
+    def getHexParams(self,
+                     llx,
+                     lly,
+                     urx,
+                     ury,
+                     mx,
+                     my,
+                     hex_width,
+                     hex_height,
+                     rot90,
+                     labels,
+                     coords,
+                     targs,
+                     nargs):
+        '''rot90 =  0  No rotation
+                 =  1  Rotated -90 (clock-wise)
+                 = -1  Rotated  90 (counter clock-wise)
+                 = -2  Rotated 180
+        '''
+        with VerboseGuard('Hex parameters') as v:
+            from math import sqrt
+            isodd   = lambda x : (x % 2 == 1)
+            iseven  = lambda x : (x % 2 == 0)
+            isfalse = lambda x : False
+            shorts  = {'isodd': isodd, 'iseven': iseven, 'isfalse': isfalse }
+            
+            # Funny scaling needed by VASSAL.  Seems like they only
+            # really about the absolute value of 'dy' and then the
+            # aspect ratio between dx and dy.
+            pxfac               = sqrt(3)/2
+            hex_pw              = hex_height * pxfac
+            hex_ph              = hex_width  * pxfac
+            stagger             = False
+            #
+            # Get parameters from coordinates. These should always be set  
+            #
+            rows                = coords  .get('row',   {})
+            columns             = coords  .get('column',{})
+            top_short           = columns .get('top short',   'isfalse')
+            bot_short           = columns .get('bottom short','isfalse')
+            inv_col             = columns .get('factor',1)
+            inv_row             = rows    .get('factor',1)
+            voff                = -rows   .get('offset',0) # 0:  from 0 -> -1
+            hoff                = -columns.get('offset',0) # -1: from 1 -> -2
+            vdesc               = inv_row == 1
+            hdesc               = inv_col == -1
+            #
+            # Calculate total dimensions, and number of columns and rows
+            #
+            w                   =  abs((urx-llx) - 2 * mx)
+            h                   =  abs((ury-lly) - 2 * my)
+            if abs(rot90) == 1: h, w  = w, h
+            nc                  = int(w // (hex_width  * 3 / 4))
+            nr                  = int(h // (hex_height))
+            namrot              = {0:   'none - 0',
+                                   -1: '-90 - CCW',
+                                   1:  '90 CW',
+                                   -2: '180 - half-turn'}
+            
+            v(f'Width:    {w}')
+            v(f'Height:   {h}')
+            v(f'Margins:  x={mx} y={my}')
+            v(f'Rotation: {rot90} ({namrot[rot90]})')
+            v(f'Labels:   {labels}')
+            v(f'Columns:')
+            v(f' size:          {nc}')
+            v(f' start:         {hoff}')
+            v(f' direction:     {inv_col}')
+            v(f' top short:     {top_short}')
+            v(f' bottom short:  {bot_short}')
+            v(f'Rows:')
+            v(f' size:          {nr}')
+            v(f' start:         {voff}')
+            v(f' direction:     {inv_row}')
+            v(f'Image:')
+            v(f' BB: ({llx},{lly}) x ({urx},{ury})')
+            #
+            # X0 and Y0 are in the local (rotated) frame of the hex grid.
+            # Thus X is always along hex breadth, and Y along the
+            # height. Thus the base offset (rotated into the hex frame) differs.
+            x0 =  ury if abs(rot90) == 1 else llx
+            y0 =  llx if abs(rot90) == 1 else ury
+            # Calculate column,row of corners
+            llc     = hoff
+            ulc     = hoff
+            lrc     = hoff+nc-1
+            urc     = hoff+nc-1
+            #
+            # Swap in directions
+            if hdesc: llc, lrc, ulc, urc = lrc, llc, urc, ulc
+            #
+            is_short_top  = shorts[columns.get('top short',   'isfalse')]
+            is_short_bot  = shorts[columns.get('bottom short','isfalse')]
+            if is_short_top is isfalse:
+                # Assume fully populated columns 
+                is_short_top = isodd if iseven(hoff) else iseven
+            if is_short_bot is isfalse:
+                is_short_bot = isodd if isodd(hoff)  else iseven
+            
+            #
+            # Now we have the hex coordinates of the corners.  We can
+            # now check how things are offset.  Before rotation, we
+            # will have that the first column is offset by hex_pw / 2.
+            x0 += hex_width / 2
+            #
+            # If the first column is _not_ short on top, then off set
+            # is simply hex_ph / 2. Otherwise, the offset is hex_ph
+            y0   +=  hex_ph / 2
+            voff -= 1
+            voff -= inv_row
+            v(f' Initial offset of image {x0},{y0}')
+            
+            # Treat each kind of rotation separately.  Note that -90 and
+            # 180 uses the `is_short_bot' while 0 and 90 uses
+            # `is_short_top'.  There might be a way to unify these, if
+            # offsets and so on may warrent it, but it may be complete
+            # overkill.
+            is_off  = False
+            col_map = {0  : (ulc, is_short_top, is_short_bot),
+                       -1 : (urc, is_short_top, is_short_bot),
+                       1  : (ulc, is_short_bot, is_short_top),
+                       -2 : (urc, is_short_bot, is_short_top) }
+            col_chk, is_s1, is_s2 = col_map[rot90]
+            
+            is_off = is_s1(col_chk)
+            if is_off:
+                y0     += hex_ph /2
+            
+            v(f'Is first column off: {is_off}')
+            
+            # For full columns, noting more is needed
+            #
+            # Below is if some columns are short both top and bottom.
+            # VASSAL seems to start numbering from a given place, and
+            # then use that for the rest numbering, and forgets to
+            # take into account various offsets and the like.  hence,
+            # we need to hack it hard.
+            if iseven(nc):
+                v(f'Even number of columns, perhaps hacks')
+                if rot90 == 0:
+                    # Hacks
+                    #
+                    # If the last column is short in both top and bottom,
+                    # and we have inverse columns, but not inverse rows,
+                    # then add to offset 
+                    if inv_col == -1 and inv_row == 1 and \
+                       is_s1(urc) and is_s2(urc):
+                        voff += 1
+                    # If the column we check for short is short both top
+                    # and bottom, and we have inverse rows, but not
+                    # inverse columns, then add offset
+                    if inv_row == -1 and inv_col == 1 and \
+                       is_s2(col_chk) and is_off:
+                        voff += 1
+                        
+                if rot90 == -1:
+                    # If the last column is short in both top and bottom,
+                    # and we have inverse columns, then add to offset
+                    if is_s1(urc) and inv_col == -1 and is_s2(urc):
+                        voff -= inv_row
+                        
+                if rot90 == 1:
+                    voff  += inv_row + (inv_row == 1)
+                    # If the first column is short in both top and bottom,
+                    # and we have inverse columns, then add to offset
+                    if is_s1(ulc) and is_s2(ulc) and inv_col == -1:
+                            voff += inv_row
+                            
+                if rot90 == -2:
+                    voff    += inv_row * 2
+                    # Hacks If the column we check for short is short both
+                    # top and bottom, and we have either inverse rows and
+                    # inverse columns, or rows and columns are normal,
+                    # then add offset
+                    if inv_col == inv_row and is_s1(col_chk) and is_s2(col_chk):
+                        voff += 1
+                    # If the first column is short in both top and bottom,
+                    # and we have inverse columns and rows, then add to
+                    # offset
+                    if inv_col == inv_row and inv_col == -1 and \
+                       is_s1(ulc) and is_s2(ulc):
+                        voff += 1
+            else:
+                v(f'Odd number of columns')
+                voff -= inv_row
+                if rot90 == 1:
+                    # If we offset in the column direction, add the
+                    # inverse row direction, and if we have inverse rows,
+                    # substract one, otherwise add 2.
+                    voff  += (inv_row * hoff + (-1 if inv_row == -1 else 2))
+                    # If we have a short column, and that column is even,
+                    # then add, otherwise subtract, the inverse row
+                    # direction, if the checked column is even.
+                    voff  += ((1 if is_off else -1) *
+                              inv_row if is_short_bot(2) else 0)
+                if rot90 == 2:
+                    voff    += inv_row * (2 + is_off) # OK for odd
                     
-        vmod.writestr('buildFile.xml',buildstr)
-        vmod.writestr('moduledata',datastr)
+                    
+            if rot90 == 0:
+                if inv_col == -1 and iseven(nc): # OK
+                    stagger =  not stagger
+                hoff    -= (inv_col == -1) # OK
+            
+            if rot90 == -1: # CCW
+                if inv_col == 1 and iseven(nc): # OK
+                    stagger =  not stagger
+                vdesc, hdesc =  hdesc, vdesc
+                vdesc        =  not vdesc
+                voff         += (inv_row == 1)
+                hoff         -= (inv_col == 1) # OK
+            
+            if rot90 == 1: # CW
+                if (inv_col == 1 and iseven(nc)) or isodd(nc): # OK
+                    stagger =  not stagger
+                vdesc, hdesc =  hdesc, vdesc
+                hdesc        =  not hdesc
+                hoff         -= (inv_col == -1) # OK
+            
+            if rot90 == -2:
+                if (inv_col == -1 and iseven(nc)) or isodd(nc): # OK
+                    stagger =  not stagger                
+                vdesc, hdesc = not vdesc, not hdesc
+                hoff    -= (inv_col == 1) # OK
+                
+            # Labels 
+            if labels is not None:
+                labmap = {
+                    'auto': {
+                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
+                    'auto=numbers' : {
+                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
+                    'auto=alpha column': {
+                        'hLeading': 0,'vLeading': 0,'hType': 'A','vType': 'N' },
+                    'auto=alpha 2 column': {# Not supported
+                        'hLeading': 1,'vLeading': 1,'hType': 'A','vType': 'N' },
+                    'auto=inv y x plus 1': {
+                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
+                    'auto=x and y plus 1': {
+                        'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' }
+                }
+                for l in labels.split(','): 
+                    nargs.update(labmap.get(l,{}))
+                    if 'alpha column' in l or 'alpha 2 column' in l:
+                        hoff -= 1 # VASSAL 0->A, wargame 1->A
+                    if l == 'auto=inv y x plus 1':
+                        hoff += 1
+                        #inv_row  =  not inv_row
+                    if l == 'auto=x and y plus 1':
+                        hoff -= 1
+                        voff -= 1
+            
+            # Add margins 
+            x0 += int(mx)
+            y0 += int(my)
+            
+            targs['dx']         = hex_pw
+            targs['dy']         = hex_ph
+            nargs['vOff']       = voff 
+            nargs['hOff']       = hoff 
+            nargs['vDescend']   = vdesc
+            nargs['hDescend']   = hdesc
+            targs['edgesLegal'] = True
+            targs['sideways']   = abs(rot90) == 1
+            nargs['stagger']    = stagger
+            targs['x0']         = int(x0+.5)
+            targs['y0']         = int(y0+.5)
+        
+    # --------------------------------------------------------------------
+    def getRectParams(self,i,llx,ury,width,height,targs,nargs):
+        targs['dx']       = width
+        targs['dy']       = height
+        targs['x0']       = int(llx - width/2)
+        targs['y0']       = int(ury + height/2)
+        targs['color']    = rgb(0,255,0)
+        nargs['color']    = rgb(0,255,0)
+        nargs['vDescend'] = True
+        nargs['vOff']     = -3
+        nargs.update({'sep':',','vLeading':0,'hLeading':0})
+        
+    # ----------------------------------------------------------------
+    def addZones(self,
+                 zoned,
+                 name,
+                 info,
+                 width,
+                 height,
+                 labels=None,
+                 coords=None,
+                 picinfo=None):
+        '''Add zones to the Zoned element.
 
-    return notes
+        Parameters
+        ----------
+        zoned : Zoned
+            Parent element
+        name : str
+            Name of Zoned
+        info : dict
+            Dictionary of zones informatio
+        width : int
+            Width of parent
+        height : int
+            Height of parent
+        labels : list
+            On recursive call, list of labels
+        coords : list
+            On recursive call, coordinates
+        picinfo : dict
+            On recursive call, picture information
+        '''
+        grids = []
+        picture = None
 
-# ====================================================================
-def export(vmodname     = 'Draft.vmod',
-           pdfname      = 'export.pdf',
-           infoname     = 'export.json',
-           title        = 'Test',
-           version      = 'Draft',
-           desc         = '',
-           rules        = None,
-           tutorial     = None,
-           patch        = None,
-           verbose      = False,
-           visible      = False):
-    '''Do the whole thing
+        with VerboseGuard(f'Adding zones to {name}') as v:
+            for k, val in info.items():
+                if k == 'labels': labels  = val;
+                if k == 'coords': coords  = val
+                if k == 'zoned':  picture = val
+                if 'zone' not in k or k == 'zoned':
+                    continue
+            
+                grids = [[k,val]] + grids  # Reverse order!
+                # grids.append([k,v])
+            
+            if len(grids) < 1:
+                return
+            
+            if picinfo is None:
+                picinfo = self.getPictureInfo(picture,name,width,height)
+                
+            hex_width, hex_height, scx, scy, rot90, tran = picinfo 
+                
+            for g in grids:
+                n, i = g
+                v(f'Adding zone {n}')
+            
+                if 'scope' in n:
+                    llx,lly = tran(*i['global lower left'])
+                    urx,ury = tran(*i['global upper right'])
+                    path    = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
+                    nm      = n.replace('zone scope ','')
+                elif 'path' in n:
+                    path    = [tran(*p) for p in i['path']]
+                    llx     = min([px for px,py in path])
+                    ury     = max([py for px,py in path])
+                    nm      = n.replace('zone path ','')
+            
+                # Checkf if we have "point" type elements in this object and
+                # add them to dict.
+                points = [ val for k,val in i.items()
+                           if (k.startswith('point') and
+                               isinstance(val,dict) and \
+                               val.get('type','') == 'point')]
+            
+                pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
+                v(f'Zone path ({llx},{ury}): {pathstr} ({len(points)})')
+            
+                ispool = 'pool' in n.lower() and len(points) <= 0
+                zone = zoned.addZone(name           = nm,
+                                     locationFormat = ("$name$"
+                                                       if ispool else
+                                                       "$gridLocation$"),
+                                     useParentGrid  = False,
+                                     path           = pathstr)
+            
+                # Do not add grids to pools 
+                if ispool:
+                    v('Board {n} is pool with no points')
+                    continue
+            
+                targs  = {'color':rgb(255,0,0),'visible':self._visible}
+                nargs  = {'color':rgb(255,0,0),'visible':self._visible}
+                # print(targs,nargs)
+                if 'turn' in n.lower(): nargs['sep'] = 'T'
+                if 'oob' in n.lower():  nargs['sep'] = 'O'
+            
+                if len(points) > 0:
+                    with VerboseGuard('Using region grid') as vv:
+                        grid = zone.addRegionGrid(snapto  = True,
+                                                  visible = self._visible)
+                        for j,p in enumerate(points):
+                            pn = p["name"].strip()
+                            pp = p.get('parent','').strip()
+                            pc = p["coords"]
+                            if j == 0: vv(f'',end='')
+                            vv(f'[{pn}] ',end='',flush=True,noindent=True)
+                        
+                            x, y = tran(*pc)
+                            r = grid.addRegion(name      = pn,
+                                               originx   = x,
+                                               originy   = y,
+                                               alsoPiece = True,
+                                               prefix    = pp)
+                        v('')
+                    
+                elif 'hex' in n.lower():
+                    margin = i.get('board frame',{}).get('margin',0)
+                    mx     = scx * margin
+                    my     = scy * margin
+                    # self.message(f'{margin} -> {scx},{scy} -> {mx},{my}')
+                    w      = abs(urx - llx)-2*mx
+                    h      = abs(ury - lly)-2*my
+                    self.getHexParams(llx         = llx,         
+                                      lly         = lly,         
+                                      urx         = urx,         
+                                      ury         = ury,         
+                                      mx          = mx,          
+                                      my          = my,          
+                                      hex_width   = hex_width,   
+                                      hex_height  = hex_height,  
+                                      rot90       = rot90,       
+                                      labels      = labels,      
+                                      coords      = coords,      
+                                      targs       = targs,       
+                                      nargs       = nargs)       
+            
+                    v(f'Adding hex grid')
+                    
+                    grid = zone.addHexGrid(**targs)
+                    grid.addNumbering(**nargs)
+                    
+                else:
+                    width  = hex_width / HEX_WIDTH * RECT_WIDTH
+                    height = hex_height / HEX_HEIGHT * RECT_HEIGHT
+                    self.getRectParams(i,llx,ury,width,height,targs,nargs)
+            
+                    v(f'Adding rectangular grid')
+                    
+                    grid = zone.addSquareGrid(**targs)
+                    grid.addNumbering(**nargs)
+                
+            
+                # Once we've dealt with this grid, we should see if we have
+                # any embedded zones we should deal with.
+                self.addZones(zoned,name,i,width,height,
+                              labels=labels,
+                              coords=coords,
+                              picinfo=picinfo)
+            
+    
+    # ----------------------------------------------------------------
+    def addBoards(self):
+        '''Add Boards to the module
+        '''
+        with VerboseGuard('Adding boards') as v:
+            hasFlipped = False
+            for cn,cd in self._categories.get('counter',{}).items():
+                for sn in cd:
+                    if ' flipped' in sn:
+                        hasFlipped = True
+                        break
+            
+            v(f'Has flipped? {hasFlipped}')
+            for bn, b in self._categories.get('board',{}).get('all',{}).items():
+                self.addBoard(bn, b,hasFlipped=hasFlipped)
 
-    - Read in information form the 'infoname' file (a CSV file
-      generated by LaTeX)
 
-    - Convert each page in the 'pdfname' file (a PDF generated by
-      LaTex) into images
+    # ----------------------------------------------------------------
+    def getIcon(self,name,otherwise):
+        with VerboseGuard(f'Get Icon {name}') as v:
+            icon   = self._categories\
+                         .get('icon',{})\
+                         .get('all',{})\
+                         .get(name,{
+                             'filename':otherwise})['filename']
+            v(f'Using "{icon}"')
+            return icon
+        
+    # ----------------------------------------------------------------
+    def addOOBs(self):
+        '''Add OOBs  to the game'''
+        oobc = self._categories.get('oob',{}).get('all',{}).items()
+        if len(oobc) < 1:
+            return
 
-    - Create the VASSAL module 
+        with VerboseGuard(f'Adding OOBs') as v:
+            icon   = self.getIcon('oob-icon','/images/inventory.gif')
+            v(f'Using icon "{icon}" for OOB')
+            charts = \
+                self._game.addChartWindow(name='OOBs',
+                                          hotkey      = self._oobKey,
+                                          description = 'OOBs',
+                                          text        = '',
+                                          icon       = icon,
+                                          tooltip     = 'Show/hide OOBs')
+            tabs = charts.addTabs(entryName='OOBs')
+            
+            for on, o in oobc:
+                widget = tabs.addMapWidget(entryName=on)
+                self.addOOB(widget, on, o)
 
-    '''
-    if verbose:
-        print(f'Module name : {vmodname}\n'
-              f'PDF name    : {pdfname}\n'
-              f'Info name   : {infoname}\n'
-              f'Title       : {title}\n'
-              f'Version     : {version}\n'
-              f'Description : {desc}\n'
-              f'Rules       : {rules}\n'
-              f'Tutorial    : {tutorial}')
-    info = convert_pages(pdfname=pdfname,infoname=infoname)
-    return create_vmod(vmodname = vmodname,
-                       info     = info,
-                       title    = title,
-                       version  = version,
-                       desc     = desc,
-                       rules    = rules,
-                       tutorial = tutorial,
-                       pdfname  = pdfname,
-                       infoname = infoname,
-                       patch    = patch,
-                       verbose  = verbose,
-                       visible  = visible)
+
+    # ----------------------------------------------------------------
+    def addOOB(self,widget,name,info):
+        '''Add a OOB elements to the game
+
+        Parameters
+        ----------
+        widget : Widget
+            Widget to add to
+        name : str
+            Name
+        info : dict
+            Information on the OOB image 
+        '''
+        map = widget.addWidgetMap(mapName   = name,
+                                  markMoved = 'Never',
+                                  hotkey    = '')
+        map.addCounterDetailViewer()
+        map.addStackMetrics()
+        map.addImageSaver()
+        map.addTextSaver()
+        map.addForwardToChatter()     
+        map.addMenuDisplayer()        
+        map.addMapCenterer()          
+        map.addStackExpander()        
+        map.addPieceMover()           
+        map.addKeyBufferer()          
+        map.addSelectionHighlighters()
+        map.addHighlightLastMoved()   
+        map.addZoomer()
         
+        picker          = map.addPicker()
+        ulx,uly,lrx,lry = self.getBB(info['img'])
+        board           = picker.addBoard(name  = name,
+                                          image = info['filename'])
+        zoned           = board.addZonedGrid()
+        zoned.addHighlighter()
+        
+        if not 'zones' in info:
+            zone = zoned.addZone(name = 'full',
+                                 useParentGrid = False,
+                                 path=(f'{ulx},{uly};' +
+                                       f'{lrx},{uly};' +
+                                       f'{lrx},{lry};' +
+                                       f'{ulx},{lry}'))
+            grid = zone.addSquareGrid()
+            grid.addNumbering()
+
+            return
+
+        # If we get here, we have board info!
+        w = abs(ulx-lrx)
+        h = abs(uly-lry)
+        self.addZones(zoned,name,info['zones'],w,h)
+
+    # ----------------------------------------------------------------
+    def addCharts(self):
+        '''Add Charts elements to game
+        '''
+        chartc = self._categories.get('chart',{}).get('all',{}).items()
+        if len(chartc) < 1:
+            return
+
+        with VerboseGuard('Adding charts') as v:
+            charts = self._game.addChartWindow(name = 'Charts',
+                                               hotkey = self._chartsKey,
+                                               description = '',
+                                               text = '',
+                                               tooltip = 'Show/hide charts',
+                                               icon    = self.getIcon('chart-icon',
+                                                                      '/images/chart.gif'))
+            tabs = charts.addTabs(entryName='Charts')
+            for i, (cn, c) in enumerate(chartc):
+                if i == 0: v('',end='')
+                v(f'[{cn}] ',end='',flush=True,noindent=True)
+            
+                tabs.addChart(chartName   = cn,
+                              description = cn,
+                              fileName    = c['filename'])
+            
+            v('')
+
+    # ----------------------------------------------------------------
+    def addDie(self):
+        '''Add a `Die` element to the module
+        '''
+        if self._dice is not None:
+            return
+        self._game.addDiceButton(name       = '1d6',
+                                 hotkey     = self._diceKey)
+
+#
+# EOF
+#
 # ====================================================================
+# From main.py
+
+# ====================================================================
 if __name__ == '__main__':
     from argparse import ArgumentParser, FileType
 
@@ -3695,6 +11549,10 @@
                     help='Short description of module',
                     type=str,
                     default='draft of module')
+    ap.add_argument('-W','--vassal-version',
+                    help='Vassal version number',
+                    type=str,
+                    default='3.6.7')
     ap.add_argument('-V','--verbose',
                     help='Be verbose',
                     action='store_true')
@@ -3701,9 +11559,15 @@
     ap.add_argument('-G','--visible-grids',
                     action='store_true',
                     help='Make grids visible in the module')
-    ap.add_argument('-n','--notes',
-                    help='Show notes',
-                    action='store_true')
+    ap.add_argument('-N','--no-nato-prototypes',
+                    action='store_true',
+                    help='Do not make prototypes for types,echelons,commands')
+    ap.add_argument('-C','--no-chit-information',
+                    action='store_true',
+                    help='Do not make properties from chit information')
+    ap.add_argument('-S','--counter-scale',
+                    type=float, default=1,
+                    help='Scale counters by factor')
 
 
     args = ap.parse_args()
@@ -3720,27 +11584,27 @@
     if args.version.lower() == 'draft':
         args.visible_grids = True
 
+    Verbose().setVerbose(args.verbose)
+        
     try:
-        notes = export(vmodname = vmodname,
-                       pdfname  = args.pdffile.name,
-                       infoname = args.infofile.name,
-                       title    = args.title,
-                       version  = args.version,
-                       desc     = args.description,
-                       rules    = rulesname,
-                       tutorial = tutname,
-                       patch    = patchname,
-                       verbose  = args.verbose,
-                       visible  = args.visible_grids)
-
-        if args.notes:
-            print(f'Module {vmodname} created. '
-                  'Here are the notes from the module')
-            print(notes)
-
+        exporter = LaTeXExporter(vmodname      = vmodname,
+                                 pdfname       = args.pdffile.name,
+                                 infoname      = args.infofile.name,
+                                 title         = args.title,
+                                 version       = args.version,
+                                 description   = args.description,
+                                 rules         = rulesname,
+                                 tutorial      = tutname,
+                                 patch         = patchname,
+                                 visible       = args.visible_grids,
+                                 vassalVersion = args.vassal_version,
+                                 nonato        = args.no_nato_prototypes,
+                                 nochit        = args.no_chit_information,
+                                 counterScale  = args.counter_scale)
+        exporter.run()
     except Exception as e:
         from sys import stderr 
-        print(f'Failed to build {vmodname}: e',file=stderr)
+        print(f'Failed to build {vmodname}: {e}',file=stderr)
         from os import unlink
         try:
             unlink(vmodname)
@@ -3748,3 +11612,10 @@
             pass
 
         raise e
+        
+#
+# EOF
+#
+##
+# End of generated script
+##

Modified: trunk/Master/texmf-dist/source/latex/wargame/utils/wgsvg2tikz.py
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/utils/wgsvg2tikz.py	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/utils/wgsvg2tikz.py	2023-03-30 20:06:53 UTC (rev 66713)
@@ -204,7 +204,7 @@
     '''
     from svg.path import Line, Move, Close, CubicBezier, Path
 
-    code = []
+    code = [f'% ']
     for e in path:
         if   isinstance(e, Move):        code.append(tikz_move(e, f))
         elif isinstance(e, Line):        code.append(tikz_line(e, f))

Modified: trunk/Master/texmf-dist/source/latex/wargame/wargame.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/wargame.dtx	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/wargame.dtx	2023-03-30 20:06:53 UTC (rev 66713)
@@ -44,6 +44,11 @@
 \date{\today}
 \begin{document}
 \maketitle
+\begin{center}
+  \begin{tikzpicture}
+    \wargamelogo
+  \end{tikzpicture}
+\end{center}
 \begin{abstract}
   This package provides tools to typesetting manuals, board, and
   counters for wargames using \LaTeX{}.
@@ -92,6 +97,10 @@
 % typeset using a similar interface.  Unit types are identified using
 % the NATO Joint Military Symbology \cite{app6c} standard.
 %
+% This document is meant as a reference manual (although far from
+% complete). A separate tutorial is available, and may be the best
+% starting point. 
+%
 % \iffalse
 % --------------------------------------------------------------------
 %
@@ -127,10 +136,18 @@
 % \item[\keyval{town}{\meta{town-keys}}] specifies that a town (or
 %   similar) is present in the hex.  The various keys are described in
 %   \sectionname~\ref{sec:hex:town}.
+% \item[\keyval{bevel}{\meta{bevel-keys}}] specifies that a bevel
+%   should be added to the hex.   The various keys are described in
+%   \sectionname~\ref{sec:hex:bevel}.
 % \item[\keyval{extra}{\meta{extra-keyx}} \textrm{and} \keyval{extra
 %   clipped}{\meta{extra-keyx}}]  allows the user to put custom
 %   graphics in the hexes.  See also \sectionname~\ref{sec:hex:extra}
 %   for more.
+% \item[\keyval{row}{\meta{row}} \textrm{and}
+%   \keyval{column}{\meta{column}}] Keys to set hex coordinates.
+%   Mainly used when using \cs{node} rather than \cs{hex}.  These
+%   coordinates should be specified in the \spec{hex cs} coordinate
+%   system (\sectionname~\ref{sec:hex:cs}).
 % \item any style key defined for \TikZ{} pictures.
 % \end{description}
 % 
@@ -137,7 +154,8 @@
 % The \meta{location} argument specifies the coordinates, in the hex
 % coordinate system where to put the hex.  More about the coordinate
 % system is given in \sectionname~\ref{sec:hex:cs}. Note, the numbers
-% starts from the lower--left corner.
+% by default starts from the lower--left corner, but can be changed
+% via options. 
 %
 % The elements are rendered in the following order
 % \begin{enumerate}
@@ -146,6 +164,7 @@
 % \item The ridges, if any
 % \item The label, if any
 % \item Extra graphics clipped to the hex
+% \item Bevel if selected
 % \item Town, if any
 % \item Extra graphics which may extend beyond the confines of the
 %   hex. 
@@ -180,7 +199,46 @@
 %   indicate two units of length.}
 %   \label{fig:hex:parts}
 % \end{figure}
+%
+% Note that the macro \cs{hex} is really a short hand for \TikZ{}'s
+% \cs{node} macro, but with preset options.  An alternative to using
+% the \cs{hex} macro is to do
 % 
+% \begin{MacroSyntax}{\hex}
+%   \cs{node}[hex=\marg{key-value-pairs}] \parg{name} at \parg{location};
+% \end{MacroSyntax}
+%
+% This can be useful when placing explanatory graphics or the like.
+% The main difference between using \cs{hex} and the raw
+% \cs{node}\texttt{[hex=\textellipsis]} is that the former can
+% automatically generate labels and set shape coordinates in the
+% picture.  If you want that for your board, it is recommended to use
+% \cs{hex}.  For example, if one does
+%
+% \begin{verbatim}
+%   \begin{tikzpicture}[
+%     every hex={label={auto=alpha column}},
+%     hex/labels is name=true]
+%     \hex(c=1,r=1);
+%   \end{tikzpicture}
+% \end{verbatim}
+%
+% then one can refer to the location of the hex by its label i.e.,
+% \texttt{(A1)}.  Since the hex is really a \TikZ{} \texttt{node}, we
+% can also use anchors defined for \texttt{hex} node shape, such as
+% \texttt{(A1.west)}, \texttt{(A1.north edge)}, and so on.  This is
+% not possible if one uses the \cs{node} macro. 
+%
+% \subsection{Hex bevels}
+% \label{sec:hex:bevel}
+%
+% A bevel (or ``shadow-effect'') can be added to hexes using the key
+% \spec{bevel}, with a value that specifies where the light comes from
+% (e.g., north west or NW).  The percentage of the half width of a
+% chit of the bevel can be specified by the key \spec{bevel fraction}
+% (default 10\%). 
+%
+%
 % \subsection{Styling hexes}
 %
 % Typical \TikZ{} options can be passed to the \cs{hex} macro.  For
@@ -200,6 +258,18 @@
 %   \end{scope}
 % \end{tikzpicture}
 % \end{verbatim}
+%
+% For example, to render only the corners of the hexes, as popular
+% among some designers, one can do
+%
+% \begin{verbatim}
+%   every hex/.style={
+%     dash pattern=on .2cm off .6cm on .2cm off 0cm
+%   },
+% \end{verbatim}
+% Note that the dash pattern should be 1cm long and the last element
+% should be \spec{off 0cm} so the dash pattern is started afresh on
+% each hex edge. 
 % 
 % \subsection{Hex coordinate system}
 % \label{sec:hex:cs}
@@ -216,7 +286,7 @@
 % where \meta{vertex} and \meta{edge} are optional.  The hex row and
 % column defaults both to 0 and can be decimal numbers. The
 % \spec{row}, \spec{column}, \spec{vertex}, and \spec{edge} keywords
-% may be shorted to \spec{r}, \spec{c}, \spec{v}, and \spec{e},
+% may be shortened to \spec{r}, \spec{c}, \spec{v}, and \spec{e},
 % respectively.  Possible vertexes and edges are listed in
 % \tablename~\ref{tab:hex:ve}.
 %
@@ -320,9 +390,15 @@
 % coordinates are shifted upward or downward for smaller or larger
 % numbers.  Figure~\ref{fig:hex:coord:nb} illustrates. this.  This can
 % make it a little hard to specify coordinates relative to a hex
-% centre.  Alternatively one may use vertex or edge specifciations
-% together with a relative offset in those directions.
-% \
+% centre.  Alternatively one may use vertex or edge specifications
+% together with a relative offset in those directions.   If one
+% require even more flexibility, one can use the \TikZ{} library
+% \spec{calc} to add arbitrary offsets, e.g.,
+%
+% \begin{verbatim}
+%   \coordinate at ($(hex cs:c=1,r=10)+(.2,.2)$);
+% \end{verbatim}
+% 
 % \begin{figure}
 %   \centering
 %   \begin{tikzpicture}
@@ -1014,6 +1090,15 @@
 %   \caption{Adding rivers, boarders, and roads}
 % \end{figure}
 % 
+% \subsubsection{Styling paths}
+%
+% Rivers, roads, railroads, and borders are styled by
+% \spec{hex/river}, \spec{hex/road}, \spec{hex/railroad}, and
+% \spec{border}, respectively, and the keys \spec{every hex river},
+% \spec{every hex road}, \spec{every hex railroad}, and \spec{every
+% hex border} will also be applied.   The latter can be defined by the
+% user.
+%  
 % \subsection{Board clipping and frame}
 % 
 % In the river, border, and road example above, the roads extend beyond
@@ -1117,6 +1202,134 @@
 % The \cs{boardframe} macro prints the position of the rectangle to
 % the log output, if one needs to do some more stuff around the board.
 %
+% \subsection{Constructing the physical board}
+%
+% If the board is not too large, so that it may fit on a paper format
+% that can easily be printed (say A4, A3, Letter, or Tabloid), one can
+% simply print the board and glue it onto a sturdy surface (say
+% 1\textonehalf\,mm poster carton).   However, if the board is large,
+% meaning it does not fit on a piece of printable paper, then one has
+% two options.
+% \begin{description}
+% \item[\textit{Either}] scale the board down so that it fits.  Use
+%   the \TikZ{} key \spec{scale=}\meta{factor} as an argument to the
+%   \spec{tikzpicture} environment in which you create the board. In
+%   this case, you should make sure you also scale the chits by the
+%   same \meta{factor}, again via the \spec{scale} key.
+% \item[\textit{Or}] you can split the board over several pages. The
+%   package provides a number of tools to help with this. 
+% \end{description}
+%
+% \subsubsection{Split the board over multiple sheets}
+%
+% First, make sure you produce a standalone PDF of the board only.
+%
+% \begin{verbatim}
+% \documentclass{standalone}
+% \usepackage{wargame}
+% \begin{tikzpicture}[scale=SCALE]
+%   % Define the board here. 
+% \end{tikzpicture}
+% \end{verbatim}
+%
+% and that you have created this PDF --- say \texttt{board.pdf}.
+%
+% Next, prepare another document in which we will do the calculations.
+% For example
+%
+% \begin{verbatim}
+%   \documentclass[11pt]{standalone}
+%   \usepackage{wargame}
+%   \begin{document}
+%   \splitboard{paper=letter,margin=.7,ncol=2,nrow=2,overlap=1}
+%   \end{document}
+% \end{verbatim}
+%
+% to calculate the split of \spec{board.pdf} over $2\times2$ letter
+% paper sheets, with a non-printable margin of 7mm, and an overlap
+% between the segments of 1cm.
+% 
+% The possible keys for the \cs{splitboard} macro are
+% 
+%  \begin{itemize}
+%  \item \spec{paper}=\meta{format}: Specifies the paper format.  One
+%    of \spec{a4}, \spec{a3}, \spec{letter}, \spec{tabloid}. Default
+%    is \spec{a4}.
+%  \item \spec{landscape}: Sets the paper format to be in landscape
+%    mode (default is portrait).
+%  \item \spec{margin}=\meta{size in centimetres}: Size of margins on
+%    each sheet in centimetres \emph{without} unit.  That is put
+%    \spec{0.6} for 6mm, \emph{not} \spec{6mm}. Default is \spec{0.6}.
+%    This should be \emph{slightly} larger (by roughly 5\%) than the
+%    \emph{least} margin required by the printer used. \emph{Must} be
+%    given \emph{before} \spec{paper} to have any effect.
+%  \item \spec{ncol}=\meta{number of columns}: Sets the number of
+%    columns of sheets.  
+%  \item \spec{nrow}=\meta{number of rows}: Set the number of rows of
+%    sheets.
+%  \item \spec{overlap}=\meta{size in centimetres}: Sets the size of
+%    the overlap between sheets in centimetres \emph{without} unit.
+%    That is put \spec{2} for 2cm, \emph{not} \spec{2cm}. Default is
+%    \spec{2}. 
+%  \item \spec{image}=\meta{image file name}: File name of the board
+%    image (a PDF). Default is \spec{board}
+%  \item \spec{output}=\meta{output file name}: File name (without
+%    \spec{.tex} ending) to write calculated split to.
+%  \item \spec{standalone}: Boolean flag.  If true, then output file
+%    will be a standalone document (i.e., has a \cs{documentclass}).
+%  \item \spec{scale}=\meta{scale}: Set scale of board. 
+%  \end{itemize}
+%
+%  The macro will produce a file named \meta{output file
+%  name}\spec{.tex} which can be included in another document to
+%  generate the split board PDF.  Crop marks will be added to the
+%  board segments to make it easier to align the parts.
+%
+% \subsubsection{Foldable board}
+%
+% To make a fold-able board use for example the below template to
+% create grooves and cuts.
+%
+% \begin{tikzpicture}[
+%   cut/.style={red!50!black,thick},
+%   front groove/.style={green!50!black,dashed,thick},
+%   back groove/.style={blue!50!black,dash dot,thick},
+%   ]
+%   \node[minimum width=8.4cm,
+%   minimum height=5.9cm,
+%   inner sep=0pt,
+%   draw,
+%   thick] (a) {};
+%   \coordinate (middle)       at (a);
+%   \coordinate (top)          at (a.north);
+%   \coordinate (top left)     at ($(a.north west)!.5!(a.north)$);
+%   \coordinate (middle left)  at ($(a.west)!.5!(a)$);
+%   \coordinate (bottom left)  at ($(a.south west)!.5!(a.south)$);
+%   \coordinate (top right)    at ($(a.north)!.5!(a.north east)$);
+%   \coordinate (middle right) at ($(a)!.5!(a.east)$);
+%   \coordinate (bottom right) at ($(a.south)!.5!(a.south east)$);
+%   \coordinate (bottom)       at (a.south);
+%   \draw[cut] (middle)--(bottom);
+%   \draw[cut] (top left)--(middle left);
+%   \draw[cut] (top right)--(middle right);
+%   \draw[back groove] (middle)--(top);
+%   \draw[back groove] (bottom left)--(middle left);
+%   \draw[back groove] (bottom right)--(middle right);
+%   \draw[front groove] (a.west)--(a.east);
+% 
+%   \draw[cut] ($(a.south west)+(0,-1)$)--++(2,0) node[anchor=west]{%
+%     Cut through carton};
+%   \draw[back groove] ($(a.south west)+(0,-1.5)$)--++(2,0) node[anchor=west]{%
+%     Cut groove (\textonehalf{} through) in carton on \emph{back} side};
+%   \draw[front groove] ($(a.south west)+(0,-2)$)--++(2,0) node[anchor=west]{%
+%     Cut groove (\textonehalf{} through) in carton on \emph{front} side};
+% \end{tikzpicture}
+%
+% This will fold the board down to a fourth of the size of the full
+% map.  For example, if the board is A1
+% ($84\,\mathrm{cm}\times59.4\,\mathrm{cm}$) it will fold down to A4
+% ($21\,\mathrm{cm}\times29.7\,\mathrm{cm}$) for easier storage.
+% 
 % \iffalse
 % --------------------------------------------------------------------
 %
@@ -1228,7 +1441,22 @@
 %   \caption{Example of chits fit within hexes. }
 %   \label{fig:chit:hex}
 % \end{figure}
+%
+% Just as \cs{hex} is really a wrapper around \TikZ{}'s \cs{node}
+% macro, so it is with \cs{chit}.  This means that an alternative way
+% of making a chit is to do
+%
+% \begin{MacroSyntax}{\chit}
+%   \cs{node}[chit=\marg{key-value-pairs}] \parg{name} at \parg{location};
+% \end{MacroSyntax}
+%
+% Since chits are really \TikZ{} \texttt{node}s we can use anchors
+% on the chit.  Unlike for \cs{hex} where there are additional
+% features available when using the dedicated macro, there really
+% isn't much difference between \cs{chit} and
+% \cs{node}\texttt{[chit=\textellipsis]}. 
 % 
+%
 % \subsection{Styling chits}
 %
 % Typical \TikZ{} options can be passed to the \cs{chit} macro.  For
@@ -1269,9 +1497,21 @@
 % similar keys.  For example, the picture \spec{chit/identifier} is
 % styled by \spec{tikz/chit/identifier}.
 %
+%
+% A bevel (or ``shadow-effect'') can be added to chits using the key
+% \spec{bevel}, with a value that specifies where the light comes from
+% (e.g., north west or NW).  The percentage of the half width of a
+% chit of the bevel can be specified by the key \spec{bevel fraction}
+% (default 10\%). This can be used for both symbol or full chits. 
+%
 % In addition, one can define the key \spec{tikz/every chit} to be the
 % default options for all chits.
 %
+% By default, the outer ``frame'' of a chit is drawn with the same
+% graphics options as the chit it self (i.e., same fill and stroke
+% colour).  To change that, one can pass \spec{frame}=\marg{options}
+% as part of the chit options. 
+% 
 % \subsection{Defining preset chit types}
 %
 % One can conveniently pre-define some chit styles.  For example,
@@ -1314,7 +1554,7 @@
 % The NATO markers are designed to fit within the template shown in
 % \figurename~\ref{fig:natoapp:template}. The template is serves as a
 % placement guide of the the various parts of the NATO marker as
-% illustrated in \figurename{fig:natoapp:usage}.
+% illustrated in \figurename~\ref{fig:natoapp:usage}.
 %
 % \begin{figure}[htbp]
 %   \centering
@@ -1688,6 +1928,9 @@
 %
 % \subsection{Unit type identification}
 %
+%
+% See \tablename~\ref{tab:natoapp6c:abbr}.
+%
 % \begin{table}
 %   \centering%
 %   \def\typemark#1{%
@@ -1737,7 +1980,7 @@
 %           & FA\\
 %       \typemark{main=infantry}
 %         & Infantry
-%           & IN\\
+%           & IN\footnote{Sometimes just `I'}\\
 %       \rowcolor{altcol}
 %       \typemark{main={infantry,armoured}}
 %         & Mechanised infantry
@@ -1775,7 +2018,7 @@
 %       \rowcolor{altcol}
 %       \sizemark{division}
 %         & Division
-%           & D\\
+%           & D\footnote{Sometimes `DIV'}\\
 %       \sizemark{brigade}
 %         & Brigade
 %           & BD\\
@@ -1828,7 +2071,9 @@
 % 
 % \begin{thebibliography}{99}
 % \bibitem{fb} Hanover,C., Hendrix,C.E., \& Llewelyn,S.,
-%   \textit{First Blood}, 1997, \url{https://grognard.com/fb/}
+%   \textit{First Blood}, 1997, \url{https://grognard.com/fb/}.  See
+%   also implementation using this package at
+%   \url{https://gitlab.com/wargames_tex/firstblood_tex}. 
 % \bibitem{app6c} \textit{NATO Joint Military Symbology}, APP-6(C),
 %   May 2011,
 %   \url{https://en.wikipedia.org/wiki/NATO_Joint_Military_Symbology}.
@@ -2453,3 +2698,5 @@
 %   TeX-command-extra-options: "-shell-escape"
 % End:
 % \fi
+
+

Modified: trunk/Master/texmf-dist/source/latex/wargame/wargame.ins
===================================================================
--- trunk/Master/texmf-dist/source/latex/wargame/wargame.ins	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/source/latex/wargame/wargame.ins	2023-03-30 20:06:53 UTC (rev 66713)
@@ -81,10 +81,17 @@
   \file{symbols.tex} {\from{wargame.dtx}{driver,symbols}}
   %
   \file{tikzlibrarywargame.util.code.tex}{%
-    \from{util/core.dtx}{utils}}
+    \from{util/core.dtx}    {utils}
+    \from{util/misc.dtx}    {utils}
+    \from{util/compound.dtx}{utils}
+    \from{util/bb.dtx}      {utils}
+    \from{util/tikz.dtx}    {utils}
+    \from{util/randomid.dtx}{utils}
+    \from{util/icons.dtx}   {utils}
+  }
   %
   \file{wgexport.cls}{%
-    \from{util/core.dtx}{exportcls}}
+    \from{util/export.dtx}{exportcls}}
   %
   \file{tikzlibrarywargame.hex.code.tex}{
     \from{hex/core.dtx}               {hex}
@@ -107,7 +114,8 @@
     \from{hex/labels.dtx}             {hex}
     \from{hex/extra.dtx}              {hex}
     \from{hex/paths.dtx}              {hex}
-    \from{hex/board.dtx}              {hex}}
+    \from{hex/board.dtx}              {hex}
+    \from{hex/split.dtx}              {hex}}
   %
   \file{tikzlibrarywargame.natoapp6c.code.tex}{
     \from{natoapp6c/core.dtx}           {natoapp6c}
@@ -125,10 +133,16 @@
     \from{natoapp6c/list.dtx}           {natoapp6c}}
   %
   \file{tikzlibrarywargame.chit.code.tex}{
-    \from{chit/core.dtx}    {chit}
-    \from{chit/shape.dtx}   {chit}
-    \from{chit/elements.dtx}{chit}
-    \from{chit/misc.dtx}    {chit}}
+    \from{chit/core.dtx}     {chit}
+    \from{chit/shape.dtx}    {chit}
+    \from{chit/elements.dtx} {chit}
+    \from{chit/modifiers.dtx}{chit}
+    \from{chit/stack.dtx}    {chit}
+    \from{chit/oob.dtx}      {chit}
+    \from{chit/table.dtx}    {chit}
+    \from{chit/battle.dtx}   {chit}
+    \from{chit/dice.dtx}     {chit}
+    \from{chit/misc.dtx}     {chit}}
   %
   \file{wargame.beach.tex}	{\from{hex/tile.dtx}{tile,beach}}
   \file{wargame.city.tex}	{\from{hex/tile.dtx}{tile,city}}

Modified: trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.chit.code.tex
===================================================================
--- trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.chit.code.tex	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.chit.code.tex	2023-03-30 20:06:53 UTC (rev 66713)
@@ -7,6 +7,12 @@
 %% chit/core.dtx  (with options: `chit')
 %% chit/shape.dtx  (with options: `chit')
 %% chit/elements.dtx  (with options: `chit')
+%% chit/modifiers.dtx  (with options: `chit')
+%% chit/stack.dtx  (with options: `chit')
+%% chit/oob.dtx  (with options: `chit')
+%% chit/table.dtx  (with options: `chit')
+%% chit/battle.dtx  (with options: `chit')
+%% chit/dice.dtx  (with options: `chit')
 %% chit/misc.dtx  (with options: `chit')
 %% 
 %% Copyright (C) 2019 Christian Holm.
@@ -40,6 +46,7 @@
 \newcount\chitdbglvl\chitdbglvl=\wargamedbglvl
 \def\chit at dbg#1#2{%
   \ifnum#1>\chitdbglvl\relax\else\message{^^J#2}\fi}
+\newif\ifchit at clip\chit at cliptrue
 \tikzset{%
   /chit/.search also={/tikz},
   /chit/.cd,
@@ -57,6 +64,21 @@
   setup/.store in=\chit at setup,             setup/.initial=,%
   id/.store in=\chit at id,                   id/.initial=,%
   frame/.store in=\chit at frame,             frame/.initial=,%
+  extra/.store in=\chit at extra,             extra/.initial=,%
+  bev/.store in=\chit at bevel,               bev/.initial=,
+  bevel fraction/.store in=\chit at bevel@frac,bevel fraction/.initial=10,
+  bevel/.is choice,
+  bevel/none/.style       = {/chit/bev=},
+  bevel/north west/.style = {/chit/bev=1},
+  bevel/north east/.style = {/chit/bev=2},
+  bevel/south west/.style = {/chit/bev=3},
+  bevel/south east/.style = {/chit/bev=4},
+  bevel/NW/.style         = {/chit/bev=1},
+  bevel/NE/.style         = {/chit/bev=2},
+  bevel/SW/.style         = {/chit/bev=3},
+  bevel/SE/.style         = {/chit/bev=4},
+  bevel/.default          = north west,
+  clip/.is if=chit at clip%
 }
 \tikzset{
   chit/symbol/.style={scale=.4,transform shape},
@@ -70,7 +92,28 @@
   chit/lower right/.style={chit/parts,anchor=south east},
   chit/setup/.style={chit/parts},
   chit/full/.style={chit/parts},
+  chit/frame/.try={draw=pgfstrokecolor},
+  chit/bevel highlight/.style={fill=white,opacity=.25},
+  chit/bevel shadow/.style={fill=black,opacity=.25},
 }
+\def\chit at bevel@frac{10}
+\newif\ifchit at draw@frame\chit at draw@frametrue
+\tikzset{
+  chit/frame style/.search also={/tikz},
+  chit/frame style/.cd,
+  none/.code={\chit at draw@framefalse},
+  draw/.code={%
+    \chit at dbg{2}{Frame draw option `#1'}
+    \edef\tikz at temp{#1}%
+    \ifx\tikz at temp\tikz at nonetext%
+      \chit at draw@framefalse%
+    \else%
+      \chit at draw@frametrue%
+      \tikzset{/tikz/draw=#1}
+    \fi
+  }
+}
+
 \tikzset{%
   chit/.code={%
     \pgfkeys{/tikz/transform shape,/tikz/shape=chit}
@@ -78,7 +121,7 @@
 \newcounter{chit at id}\setcounter{chit at id}{0}
 \def\chit at n@to#1#2{%
   %% Without a following start square bracket '[' by-pass to final
-  \chit at dbg{4}{Chit NATO App6(c) first step `#1' `#2'}
+  \chit at dbg{1}{Chit NATO App6(c) first step `#1' `#2'}
   \@ifnextchar[{%
     %\message{^^JStart square bracket}%
     \@chit at n@to{#1}{#2}}{%
@@ -86,7 +129,7 @@
     \@chit at n@to@{#1}{#2}}%]]
 }
 \def\@chit at n@to@#1#2#3\@end at chit@n at to{%
-  \chit at dbg{4}{Chit NATO App6(c) w/o offset:
+  \chit at dbg{1}{Chit NATO App6(c) w/o offset:
     ^^J  Options:  #3
     ^^J  ID:       #1
     ^^J  Position: #2}
@@ -94,11 +137,11 @@
   \chit at dbg{4}{Chit NATO App6(c) ended}%
 }
 \def\@chit at n@to#1#2[#3]{%
-  \chit at dbg{4}{Chit NATO App6(c) second step `#1' `#2' `#3'}
+  \chit at dbg{1}{Chit NATO App6(c) second step `#1' `#2' `#3'}
   \@ifnextchar({\@@chit at n@to{#1}{#2}{#3}}{\@@chit at n@to{#1}{#2}{#3}(0,0)}%)
 }
 \def\@@chit at n@to#1#2#3(#4)\@end at chit@n at to{%
-  \chit at dbg{4}{Chit NATO App6(c) w/offset:
+  \chit at dbg{1}{Chit NATO App6(c) w/offset:
     ^^J  Options:  #3
     ^^J  ID:       #1
     ^^J  Position: #2
@@ -115,6 +158,36 @@
   zone turn/.style={},
   zone mult/.style={}
 }
+\def\chit at bevel@path#1{
+  \scope[#1]
+  \wg at tmpc=\wg at tmpa\multiply\wg at tmpc by \chit at bevel@frac
+  \wg at tmpd=\wg at tmpb\multiply\wg at tmpd by \chit at bevel@frac
+  \divide\wg at tmpc100
+  \divide\wg at tmpd100
+  \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move down along edge
+  \wg at tmpb=-\wg at tmpb
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move left along edge
+  \wg at tmpa=-\wg at tmpa
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move in and up
+  \advance\wg at tmpa\wg at tmpc%
+  \advance\wg at tmpb\wg at tmpd%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move right, but in
+  \advance\wg at tmpa-\wg at tmpc\wg at tmpa=-\wg at tmpa%
+  \advance\wg at tmpa-\wg at tmpc%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Move up but down
+  \advance\wg at tmpb-\wg at tmpd\wg at tmpb=-\wg at tmpb%
+  \advance\wg at tmpb-\wg at tmpd%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  \pgfclosepath%
+  \pgfusepath{fill}
+  \endscope
+}
+
 \pgfdeclareshape{chit}{
   \savedanchor\center{\pgf at x=0cm\pgf at y=0cm}
   \savedanchor\northeast{\pgf at x=0.6cm\pgf at y=0.6cm}
@@ -136,7 +209,7 @@
     \let\chitframeopt\pgfutil at empty%
     \@ifundefined{chit at frame}{}{%
       \edef\chitframeopt{\chit at frame}}
-    \n at to@pp at dbg{3}{Chit Frame options: \meaning\chitframeopt}%
+    \chit at dbg{3}{Chit Frame options: \meaning\chitframeopt}%
   }
   \anchor{center}{\center}
   \anchor{north east}{\northeast}
@@ -181,6 +254,7 @@
   \backgroundpath{%
     %% This is the outline of the chit only.  The rest of the chit is
     %% made on the foreground "path".
+    \chit at dbg{1}{Chit drawing background path}
     \northeast%
     \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
     \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
@@ -190,43 +264,56 @@
     \pgfclosepath
   }
   \behindforegroundpath{%
-    \chit at dbg{4}{%
-      Chit foreground: \meaning\id
-      ^^J  ID (set):    \meaning\chit at id
-      ^^J  Symbol:      \meaning\chit at symbol
-      ^^J  Full:        \meaning\chit at full
-      ^^J  Factors:     \meaning\chit at factors
-      ^^J  Left:        \meaning\chit at left
-      ^^J  Right:       \meaning\chit at right
-      ^^J  Upper left:  \meaning\chit at upper@left
-      ^^J  Lower left:  \meaning\chit at lower@left
-      ^^J  Upper right: \meaning\chit at upper@right
-      ^^J  Lower right: \meaning\chit at lower@right}
+    \chit at dbg{1}{Chit drawing foreground path}
+    % \chit at dbg{4}{%
+    %   Chit foreground: \meaning\id
+    %   ^^J  ID (set):    \meaning\chit at id
+    %   ^^J  Symbol:      \meaning\chit at symbol
+    %   ^^J  Full:        \meaning\chit at full
+    %   ^^J  Factors:     \meaning\chit at factors
+    %   ^^J  Left:        \meaning\chit at left
+    %   ^^J  Right:       \meaning\chit at right
+    %   ^^J  Upper left:  \meaning\chit at upper@left
+    %   ^^J  Lower left:  \meaning\chit at lower@left
+    %   ^^J  Upper right: \meaning\chit at upper@right
+    %   ^^J  Lower right: \meaning\chit at lower@right
+    %   ^^J  Extra:       \meaning\chit at extra
+    %   ^^J  Bevel:       \meaning\chit at bevel
+    %   ^^J  Frame:       \meaning\chit at frame}
+    \chit at dbg{1}{Chit report}
     \chit at report{}
+    \chit at dbg{1}{Chit start scope}
     \pgfscope
     %
-    \northeast%
-    \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
-    \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \pgfclosepath
-    \pgfusepath{clip}
+    \ifchit at clip%
+      \chit at dbg{1}{Chit clip path}
+      \northeast%
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \pgfclosepath%
+      \pgfusepath{clip}%
+    \fi%
     \@ifundefined{chit at symbol}{%
       %% Draw full stuff
       \@ifundefined{chit at full}{}{%
+        \chit at dbg{1}{Chit draw full image: `\meaning\chit at full'}
         \center\wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
         \wg at pic@all{\chit at full}{}{\the\wg at tmpa,\the\wg at tmpb}{chit/full}}%
-    }{%
+    }{% With NATO symbol
+      \chit at dbg{1}{Chit draw symbol image}
       \edef\symid{\id symbol}%
       \symbol%
       \edef\args{{\symid}{\the\pgf at x,\the\pgf at y}\chit at symbol}%
       \chit at dbg{6}{Arguments to chit NATO symbol: \meaning\args}%
+      \chit at dbg{1}{Chit draw nato image}
       \expandafter\chit at n@to\args\@end at chit@n at to%
       \chit at dbg{6}{After making NATO symbol in chit}%
       % Put in left of symbol
       \@ifundefined{chit at left}{}{%
+        \chit at dbg{1}{Chit draw left: `\meaning\chit at left'}
         \begin{scope}[]
           \pgfpointanchor{\symid}{west}%
           \wg at tmpa=\pgf at x\advance\wg at tmpa-\margin%
@@ -235,6 +322,7 @@
         \end{scope}}%
       % Put in right of symbol
       \@ifundefined{chit at right}{}{%
+        \chit at dbg{1}{Chit draw left: `\meaning\chit at right'}
         \begin{scope}[]
           \pgfpointanchor{\symid}{east}%
           \wg at tmpa=\pgf at x\advance\wg at tmpa+\margin%
@@ -249,6 +337,7 @@
       \advance\wg at tmpb-\margin%
       % Put in upper left corner
       \@ifundefined{chit at upper@left}{}{%
+        \chit at dbg{1}{Chit draw upper left: `\meaning\chit at upper@left'}
         \begin{scope}[]
           \wg at pic@all{\chit at upper@left}{}{-\the\wg at tmpa,\the\wg at tmpb}{%
             chit/upper left}%
@@ -255,6 +344,7 @@
         \end{scope}}
       % Put in upper right corner
       \@ifundefined{chit at upper@right}{}{%
+        \chit at dbg{1}{Chit draw upper right: `\meaning\chit at upper@right'}
         \begin{scope}[]
           \wg at pic@all{\chit at upper@right}{}{\the\wg at tmpa,\the\wg at tmpb}{%
             chit/upper right}%
@@ -261,6 +351,7 @@
         \end{scope}}
       % Put in lower left corner
       \@ifundefined{chit at lower@left}{}{%
+        \chit at dbg{1}{Chit draw lower left: `\meaning\chit at lower@left'}
         \begin{scope}[]
           \wg at pic@all{\chit at lower@left}{}{-\the\wg at tmpa,-\the\wg at tmpb}{%
             chit/lower left}%
@@ -267,6 +358,7 @@
         \end{scope}}
       % Put in lower right corner
       \@ifundefined{chit at lower@right}{}{%
+        \chit at dbg{1}{Chit draw lower right: `\meaning\chit at lower@right'}
         \begin{scope}[]
           \wg at pic@all{\chit at lower@right}{}{\the\wg at tmpa,-\the\wg at tmpb}{%
             chit/lower right}%
@@ -273,24 +365,62 @@
         \end{scope}}
       % Put in factors
       \@ifundefined{chit at factors}{}{%
+        \chit at dbg{1}{Chit draw factors: `\meaning\chit at factors'}
         \advance\wg at tmpb-\margin%
         \begin{scope}[]
           \wg at pic@all{\chit at factors}{}{0,-\the\wg at tmpb}{chit/factors}%
         \end{scope}}%
-    }%
+      % Put in extra
+      \@ifundefined{chit at extra}{}{%
+        \chit at dbg{1}{Chit draw extra: `\meaning\chit at extra'}
+        \begin{scope}[]
+          \wg at pic@all{\chit at extra}{}{0,0}{chit/factors}%
+        \end{scope}}%
+    }% End of full or symbol
     \endpgfscope%
+    % Make bevel?
+    \@ifundefined{chit at bevel}{\let\chit at bevel\empty}{}
+    \ifx\chit at bevel\empty\else%
+      \chit at dbg{1}{Chit draw bevel}
+      %% South east bevel
+      \northeast%
+      \wg at tmpa=-\pgf at x\wg at tmpb=-\pgf at y%
+      \ifcase\chit at bevel\relax%
+      \or% 1
+      \or\wg at tmpa=-\wg at tmpa% 2
+      \or\wg at tmpb=-\wg at tmpb% 3
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpb=-\wg at tmpb%4
+      \fi
+      \chit at bevel@path{chit/bevel highlight}
+      %% North west bevel
+      \northeast%
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \ifcase\chit at bevel\relax%
+      \or% 1
+      \or\wg at tmpa=-\wg at tmpa% 2
+      \or\wg at tmpb=-\wg at tmpb% 3
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpb=-\wg at tmpb%4
+      \fi
+      \chit at bevel@path{chit/bevel shadow}
+    \fi
     % Draw frame?
-    \edef\tmp at opt{[\chitframeopt]}
+    \chit at dbg{1}{Chit draw frame: `\meaning\chitframeopt'}
+    \edef\tmp at opt{[chit/frame style/.cd,chit/frame/.try,\chitframeopt]}
+    \chit at dbg{1}{Chit draw frame: `\meaning\tmp at opt}
     \expandafter\scope\tmp at opt
-    \northeast%
-    \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
-    \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
-    \pgfclosepath
-    \pgfusepath{stroke}
-    \endscope
+      \northeast%
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpb=-\wg at tmpb \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \wg at tmpa=-\wg at tmpa \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+      \pgfclosepath%
+      \chit at dbg{3}{Line width for frame: `\the\pgflinewidth'}
+      \ifchit at draw@frame\pgfusepath{stroke}\fi%
+      \chit at draw@frametrue%
+      %\iftikz at mode@fill\pgfusepath{fill}\fi%
+    \endscope%
+    \chit at dbg{1}{Chit end of shape}
   }
 }
 \def\chit{%
@@ -311,7 +441,7 @@
     ^^J  Name:       `#3'}
   \let\name\pgfutil at empty%
   \chit at dbg{1}{=== Before chit node}%
-  \node[draw,chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {};
+  \node[chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {};
   \chit at dbg{2}{=== After chit node}%
   \ifx|#3|\relax%
   \else%
@@ -320,6 +450,10 @@
   \fi%
   \@ifnextchar;{\@gobble}{}%
 }
+\DeclareRobustCommand\chit at sep[2][/]{%
+  \foreach[count=\is] \s in {#2}{%
+    \ifnum\is>1\relax#1\fi%
+    \s}}
 \tikzset{%
   chit/1 factor/.pic={
     \chit at dbg{4}{ Chit 1 factor: #1}%
@@ -331,24 +465,32 @@
   pics/chit/2 factors artillery/.style args={#1,#2,#3}{%
     code={
       \chit at dbg{4}{ Chit 2 factors w/artillery: `#1' `#2' `#3'}%
-      \node[chit/factor,chit/2 factors,pic actions]{%
-        #1$\overset{\text{\scriptsize #3}}{\text{--}}$#2};}},
+      \node[chit/factor,chit/2 factors]{%
+        {#1}$\overset{\text{\scriptsize #3}}{\text{--}}${#2}};}},
   pics/chit/3 factors/.style args={#1,#2,#3}{%
     code={
       \chit at dbg{4}{ Chit 3 factors: `#1' `#2' `#3'}%
-      \node[chit/factor,chit/3 factors,pic actions]{#1-#2-#3};}},
+      \node[chit/factor,chit/3 factors]{#1-#2-#3};}},
   pics/chit/4 factors/.style args={#1,#2,#3,#4}{%
     code={
       \chit at dbg{4}{ Chit 3 factors: `#1' `#2' `#3' `#4'}%
-      \node[chit/factor,chit/4 factors,pic actions]{#1-#2-#3-#4};}},
+      \node[chit/factor,chit/4 factors]{#1-#2-#3-#4};}},
   chit/identifier/.pic={
     \chit at dbg{4}{  Chit identifier: `#1'}%
     \node[chit/identifier,pic actions]{#1};
   },
+  chit/identifiers/.pic={
+    \chit at dbg{4}{  Chit identifiers: `#1'}%
+    \node[chit/identifier,pic actions]{\chit at sep{#1}};
+  },
   chit/small identifier/.pic={
     \chit at dbg{4}{  Chit small identifier: `#1'}%
     \node[chit/small identifier,pic actions]{#1};
   },
+  chit/small identifiers/.pic={
+    \chit at dbg{4}{  Chit small identifiers: `#1'}%
+    \node[chit/small identifier,pic actions]{\chit at sep{#1}};
+  },
   chit/identifier macro/.pic={%
     \chit at dbg{4}{ Chit identifier macro: \meaning#1}
     \edef\chit at i@tmp{#1}
@@ -357,32 +499,35 @@
 \tikzset{%
   chit/factor/.style={
     shape=rectangle,
-    font=\sffamily\bfseries\large,
+    font=\sffamily\bfseries\fontsize{12}{14}\selectfont,
     anchor=base,
     inner sep=0,
     %text=pgfstrokecolor,
     draw=none,
     fill=none,
+    transform shape,
   },
   chit/1 factor/.style={},
   chit/2 factors/.style={},
   chit/3 factors/.style={},
-  chit/4 factors/.style={text/.append style=\small},
+  chit/4 factors/.style={text/.append style=\fontsize{10}{12}\selectfont},
   chit/identifier/.style={
     shape=rectangle,
-    font=\sffamily\bfseries\scriptsize,
+    font=\sffamily\bfseries\fontsize{8}{9}\selectfont,
     inner sep=0,
     % text=pgfstrokecolor,
     draw=none,
     fill=none,
+    transform shape,
   },
   chit/small identifier/.style={
     shape=rectangle,
-    font=\sffamily\bfseries\tiny,
+    font=\sffamily\bfseries\fontsize{6}{7}\selectfont,
     inner sep=0,
     % text=pgfstrokecolor,
     draw=none,
     fill=none,
+    transform shape,
   },
 }
 \tikzset{
@@ -394,11 +539,21 @@
       \path[fill=red,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
   pics/chit/shade/.default=0.5,
   pics/chit/eliminate/.default=0.25,
+  dummy chit/.style={draw=none,fill=none,chit={}},
 }
-\def\shadechit(#1){%
-  \pic at (#1) {chit/shade};}
-\def\eliminatechit(#1){%
-  \pic at (#1) {chit/eliminate};}
+\def\shadechit{%
+  \@ifnextchar[{\sh at dechit}{\sh at dechit[.5]}%]
+}
+\def\eliminatechit{%
+  \@ifnextchar[{\elimin at techit}{\elimin at techit[.25]}%]
+}
+\def\sh at dechit[#1](#2){%
+  % \message{^^JShading chit with opacity `#1'}%
+  \pic[transform shape] at (#2) {chit/shade=#1};%
+  \@ifnextchar;{\@gobble}{}}
+\def\elimin at techit[#1](#2){%
+  \pic[transform shape] at (#2) {chit/eliminate=#1};%
+  \@ifnextchar;{\@gobble}{}}
 \tikzset{%
   chit/stack direction/.store in=\chit at stack@dir,
   chit/stack direction/.initial={(.3,.3)},
@@ -407,28 +562,29 @@
 \def\stackchits(#1){%
   \@ifnextchar({\st at ckchits{#1}}{\st at ckchits{#1}(.3,.3)}%)
 }
-\def\st at ckchits#1(#2)#3{
-  \chit at dbg{2}{Stacking chits `#1', `#2', `#3'}
-  \edef\xy{#1}
-  \chit at dbg{4}{Stack start at \xy}
+\def\st at ckchits#1(#2)#3{%
+  \chit at dbg{2}{Stacking chits `#1', `#2', `#3'}%
+  \edef\xy{#1}%
+  \chit at dbg{4}{Stack start at \xy}%
   \foreach[count=\i from 0] \c/\o in {#3} {%
     \ifx\c\empty\else%
-      \edef\ccc{\c}
-      \chit at dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'}
-      \expandafter\ccc(\xy)
+      \edef\ccc{\c}%
+      \chit at dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'}%
+      \expandafter\ccc(\xy)%
       %%
-      \ifx\c\o\else
+      \ifx\c\o\else%
         %\chit at dbg{0}{Option: \o}
-        \edef\ccc{\o}
-        \expandafter\ccc(\xy)
+        \edef\ccc{\o}%
+        \expandafter\ccc(\xy)%
       \fi
-      \expandafter\ccc(\xy)
+      \expandafter\ccc(\xy)%
       \tikzmath{%
-        coordinate \cc;
+        coordinate \cc;%
         \cc = (\xy) + (#2);}
-      \xdef\xy{\cc}
-    \fi
-  }
+      \xdef\xy{\cc}%
+    \fi%
+  }%
+  \@ifnextchar;{\@gobble}{}%
 }
 \tikzset{
   chit/oob turn/.pic={\node[pic actions]{#1};}}
@@ -460,6 +616,7 @@
   \chit at dbg{2}{ \space\space-> update `\string#1'=#1,`\string#2'=#2}
 }
 \newif\ifwg at oob@inv\wg at oob@invfalse
+\def\chit at oob@spacer{hspace}
 \def\wg at star@oob{\wg at oob@invtrue\wg at oob}
 \def\wg at nostar@oob{\wg at oob@invfalse\wg at oob}
 \def\oob{%
@@ -469,16 +626,16 @@
 }
 \def\wg at oob#1#2#3#4{
   \def\r{0}
-  \chit at dbg{1}{OOB: `#1'}
+  \chit at dbg{2}{OOB: `#1'}
   \foreach[count=\ti from 0] \t/\y in #1{
     \xdef\o{\r}
     \def\c{0}
     \ifx\t\y\def\y{0}\fi
-    \chit at dbg{1}{Turn \ti\space(\r,\t,y=\y):'}
+    \chit at dbg{2}{Turn \ti\space(\r,\t,y=\y):'}
     \ifwg at oob@inv%
-      \pic at ( .5,\r) {chit/oob turn=\ti};%
+      \pic[transform shape] at ( .5*#3,\r) {chit/oob turn=\ti};% was dx=0.5
     \else
-      \pic at (-.5,\r) {chit/oob turn=\ti};%
+      \pic[transform shape] at (-.5*#3,\r) {chit/oob turn=\ti};% was dx=-0.5
     \fi%
     \ifx\t\empty\else%
       \foreach \u/\m in \t{
@@ -487,10 +644,21 @@
           \ifx\m\@empty\def\m{1}\fi
           \ifx\u\m\def\m{1}\fi
           \foreach \n in {1,...,\m}{%
-            \ifx\u\chit at blank\else
-              \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r);
+            \chit at dbg{2}{OOB Chit is `\u'}%
+            \ifx\u\chit at oob@spacer%
+              \chit at dbg{3}{Chit `\u' is spacer `\chit at oob@spacer'}
+              \pgfmathparse{\c+#4}%
+              \xdef\c{\pgfmathresult}%
+            \else
+              \ifnum\chitdbglvl>2%
+                \node[minimum width=#3cm,minimum height=#3cm,
+                      draw,transform shape] at (\c,\r) {};
+              \fi
+              \ifx\u\chit at blank\else%
+                \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r);%
+              \fi%
+              \chit at oob@cellupdate(\c,\r){#2}{#3}{\y}
             \fi
-            \chit at oob@cellupdate(\c,\r){#2}{#3}{\y}
           }
         \fi
       }
@@ -527,10 +695,10 @@
     % This will zero \c.  However, if on entry |\c|>0, then we also
     % increment the row
     \chit at oob@turnupdate(\c,\r){#3}{#4}
-    \chit at dbg{1}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
+    \chit at dbg{2}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
   }
-  \chit at dbg{2}{End of OOB (c=`\c',r=`\r',y=`\y')}
-}
+  \chit at dbg{3}{End of OOB (c=`\c',r=`\r',y=`\y')}
+  \@ifnextchar;{\@gobble}{}}
 \tikzset{
   chit/cell background/.style={fill=black},
   blank chit/.style={/chit/frame={draw=none,fill=none}},
@@ -577,7 +745,7 @@
       }%
     \fi%
   }%
-}
+  \@ifnextchar;{\@gobble}{}}
 \def\chit at dbl@flip(#1,#2)#3{%
   \pgfmathparse{-#1}%
   \xdef\mc{\pgfmathresult}%
@@ -621,22 +789,256 @@
       }
     \fi
   }
-  \draw[dashed](0,-3*#3/4)--(0,\r-#3/4);
-  \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[anchor=west]{Back};
-  \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[anchor=east]{Front};
+  \draw[dashed](0,-3*#3/4)--(0,\r-#3/4);%
+  \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[transform shape,anchor=west]{Back};%
+  \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[transform shape,anchor=east]{Front};%
+  \@ifnextchar;{\@gobble}{}}
+\tikzset{%
+  battle marker/.pic={
+    \node[shape=circle,
+    font=\sffamily\bfseries,
+    inner sep=0pt,
+    minimum size=5mm,
+    draw=black,
+    fill=yellow!85!black,
+    every battle marker/.try] at (-.3,.3) {%
+      \ifnum#1>0\relax #1\fi%
+    };
+  },
+  battle marker/.style={
+    chit={full={battle marker=#1},frame={draw=none}}},
 }
+\tikzset{%
+  pics/odds marker/.style args={#1,#2}{
+    code={
+      \node[shape=circle,
+      font=\sffamily\bfseries\large,
+      inner sep=0pt,
+      minimum size=8mm,
+      draw=black,
+      fill=#2,
+      every odds marker/.try] at (.2,-.2) {#1};
+    }
+  },
+  odds marker/.style args={#1,#2}{
+    chit={full={odds marker={#1,#2}},frame={draw=none}}},
+}
 \tikzset{
-  number chit/.pic={
-    \node[shape=rectangle,font=\sffamily\bfseries\LARGE]{%
-      \begin{tabular}{c} #1\end{tabular}};},
-  game turn/.pic={
-    \node[shape=rectangle,font=\sffamily\bfseries]{%
-      \begin{tabular}{c} Game\\Turn\end{tabular}};},
-  game turn/.style={
-    /chit/full={game turn},
+  pics/result marker/.style args={#1,#2}{
+    code={
+      \message{^^JResults marker #1 (#2)}
+      \node[shape=circle,
+      font=\sffamily\bfseries\large,
+      inner sep=0pt,
+      minimum size=8mm,
+      draw=black,
+      fill=#2,
+      every result marker/.try] at (0,0) {#1};}},
+  result marker/.style args={#1,#2}{
+    chit={full={result marker={#1,#2}},frame={draw=none}}}
+}
+\tikzset{
+  dice bg/.style={
+    % /utils/exec={
+    %   \pgfgettransformentries{%
+    %     \wg at jaca}{%
+    %     \wg at jacb}{%
+    %     \wg at jacc}{%
+    %     \wg at jacd}{%
+    %     \wg at tmp}{%
+    %     \wg at tmp}%
+    %   \pgfmathsetmacro{\wg at tmp}{%
+    %     sqrt(abs(\wg at jaca*\wg at jacd-\wg at jacb*\wg at jacc))}
+    %   \xdef\wg at tmp{\wg at tmp}},%
+    fill=black,
+    draw=none,
+    minimum width=1cm,
+    minimum height=1cm,
+    scale rounded corners,
+    rounded corners=.1cm,
+    inner sep=0pt,
+    transform shape},
+  dice fg/.style={
+    fill=white,
+    shape=circle,
+    inner sep=0pt,
+    minimum size=.2cm,
+    transform shape},
+  pics/dice/.style={
+    code={
+      \node[dice bg] (dice bg) {};
+      \ifodd#1\node[dice fg] at (dice bg) {};\fi
+      \ifnum#1>1%
+      \node[dice fg] at ($(dice bg)+(-45:.4)$){};%
+      \node[dice fg] at ($(dice bg)+(135:.4)$){};%
+      \fi%
+      \ifnum#1>3%
+      \node[dice fg] at ($(dice bg)+(  45:.4)$){};%
+      \node[dice fg] at ($(dice bg)+(-135:.4)$){};%
+      \fi%
+      \ifnum#1=6%
+      \node[dice fg] at ($(dice bg)+(-.282,0)$){};
+      \node[dice fg] at ($(dice bg)+( .282,0)$){};
+      \fi
+    }
+  },
+  pics/dice/.default=3
+}
+\newcommand\dicemark[2][scale=.5]{%
+  \tikz[baseline={($(dice bg.south east)!.25!(dice bg.north east)$)},#1]{
+    \pic[transform shape]{dice=3};}}
+\pgfdeclareshape{d4}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.4cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{-.35cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{-.35cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.4cm}}
+    % \pgfusepath{draw}  %draw border
+    % \pgfusepath{draw}  %draw rectangle
+  }}
+\pgfdeclareshape{d6}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathrectanglecorners{\pgfpoint{.4cm}{.4cm}}{\pgfpoint{-.4cm}{-.4cm}}
+    % \pgfusepath{draw}  %draw rectangle
+  }}
+\pgfdeclareshape{d8}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{.25cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{.25cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+\pgfdeclareshape{d10}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{
+    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.294cm}{-.154cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.3cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{.1cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{-.1cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{.1cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathmoveto{\pgfpoint{.294cm}{-.154cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}}
+    \pgfpathmoveto{\pgfpoint{-.475cm}{-.1cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}}
+    \pgfpathmoveto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.3cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+\pgfdeclareshape{d12}{
+  \anchor{center}{\pgfpointorigin}    % within the node, (0,0) is the center
+  \anchor{text}{   % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{0.294cm}{.405cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{.475cm}{-.173cm}}
+    \pgfpathlineto{\pgfpoint{.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{-.173cm}}
+    \pgfpathlineto{\pgfpoint{-.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{-.294cm}{.405cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.349cm}}
+    \pgfpathlineto{\pgfpoint{.332cm}{.108cm}}
+    \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}}
+    \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}}
+    \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.349cm}}
+    \pgfpathmoveto{\pgfpoint{.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{.332cm}{.108cm}}
+    \pgfpathmoveto{\pgfpoint{.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}}
+    \pgfpathmoveto{\pgfpoint{-.294cm}{-.405cm}}
+    \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}}
+    \pgfpathmoveto{\pgfpoint{-.475cm}{.173cm}}
+    \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+\pgfdeclareshape{d20}{
+  \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+  \anchor{text}{    % this is used to center the text in the node
+    \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+  \backgroundpath{ % draw border
+    \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+    \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+    \pgfpathlineto{\pgfpoint{.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+    \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+    \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}}
+    \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+    \pgfpathmoveto{\pgfpoint{.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+    \pgfpathmoveto{\pgfpoint{-.454cm}{-.262cm}}
+    \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+    % \pgfusepath{draw}  %draw interiaor
+  }}
+\tikzset{
+  chit/text base/.style={
+    shape=rectangle,
+    inner sep=0pt,
+    align=center,
+    text width=1.1cm},
+  chit/number/.style={
+    chit/text base,
+    font=\sffamily\bfseries\fontsize{12}{14}\selectfont},
+  chit/game turn/.style={
+    chit/text base,
+    font=\sffamily\bfseries},
+  chit/text/.style={
+    chit/text base,
+    font=\sffamily\bfseries},
+  chit/small text/.style={
+    chit/text base,
+    font=\sffamily\bfseries\fontsize{9}{10}\selectfont},
+  chit/number/.pic={\node[chit/number]{#1};},
+  chit/game turn/.pic={\node[chit/game turn]{Game\\Turn};},
+  chit/text/.pic={\node[chit/text]{#1};},
+  chit/small text/.pic={\node[chit/small text]{#1};},
+  game turn chit/.style={
+    /chit/full={chit/game turn},
     color=black,
     fill=white},
-  game turn flipped/.style={game turn},
+  game turn chit flipped/.style={game turn chit},
   dummy chit/.style={fill=white},
 }
 \providecommand\chitmark[2][]{\tikz[scale=.25,#1]{\chit[#2]}}
@@ -651,7 +1053,7 @@
       \noexpand\chit[wg stacking],
       \noexpand\chit[wg stacking]}}}
 \DeclareRobustCommand\zocmark[1][]{%
-  \tikz[baseline=(current bounding box.center),scale=.1,#1]{%
+  \tikz[baseline=($(current bounding box.center)!.5!(current bounding box.south)$),scale=.1,#1]{%
     \begin{scope}[hex/first row and column are=0,
       hex/row direction is=normal,
       hex/column direction is=normal,

Modified: trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.hex.code.tex
===================================================================
--- trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.hex.code.tex	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.hex.code.tex	2023-03-30 20:06:53 UTC (rev 66713)
@@ -25,6 +25,7 @@
 %% hex/extra.dtx  (with options: `hex')
 %% hex/paths.dtx  (with options: `hex')
 %% hex/board.dtx  (with options: `hex')
+%% hex/split.dtx  (with options: `hex')
 %% 
 %% Copyright (C) 2019 Christian Holm.
 %% 
@@ -214,31 +215,42 @@
   \tikzset{/hex/coords/.cd, #1}%
   \pgfmathparse{int(\hex at coords@col at fac*(\hex at col+\hex at coords@col at off))}%
   \xdef\hex at eff@col{\pgfmathresult}%
-  \hex at dbg{2}{Effective column: \hex at coords@col at fac * (\hex at col -
+  \hex at dbg{2}{Effective column: \hex at coords@col at fac * (\hex at col +
     \hex at coords@col at off) -> \hex at eff@col}%
   \pgfmathparse{\hex at eff@col*1.5}%
   \xdef\hex at x{\pgfmathresult}%
-  \expandafter\pgf at x=\hex at x cm%
   \pgfmathparse{int(\hex at coords@row at fac*(\hex at row+\hex at coords@row at off))}%
   \xdef\hex at eff@row{\pgfmathresult}%
   \hex at dbg{2}{Effective row: \hex at coords@row at fac * (\hex at row +
     \hex at coords@row at off) -> \hex at eff@row}%
   \pgfmathparse{(2*\hex at eff@row-mod(round((\hex at col+\hex at coords@col at off)),2))*\hex at yy}%
+  \pgfmathparse{(2*\hex at eff@row-mod(abs(round(\hex at col+\hex at coords@col at off)),2))*\hex at yy}%
   \xdef\hex at y{\pgfmathresult}%
-  \expandafter\pgf at x=\hex at y cm%
-  \ifx\hex at vtx\@empty\pgfpointxy{\hex at x}{\hex at y}\else%
-  \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
-    \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at vtx}{1}}}\fi%
+  \ifx\hex at vtx\@empty\else%
+    \pgfmathparse{\hex at x+\hex at off*cos(\hex at vtx)}\xdef\hex at x{\pgfmathresult}
+    \pgfmathparse{\hex at y+\hex at off*sin(\hex at vtx)}\xdef\hex at y{\pgfmathresult}
+  \fi%
+  % \ifx\hex at vtx\@empty\pgfpointxy{\hex at x}{\hex at y}\else%
+  % \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
+  %   \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at vtx}{1}}}\fi%
   \ifx\hex at edg\@empty\else%
-  \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
-    \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at edg}{\hex at yy}}}\fi
+    \pgfmathparse{\hex at x+\hex at off*\hex at yy*cos(\hex at edg)}%
+    \xdef\hex at x{\pgfmathresult}%
+    \pgfmathparse{\hex at y+\hex at off*\hex at yy*sin(\hex at edg)}%
+    \xdef\hex at y{\pgfmathresult}%
+  \fi%
+  % \ifx\hex at edg\@empty\else%
+  % \pgfpointadd{\pgfpointxy{\hex at x}{\hex at y}}{%
+  %   \pgfpointscale{\hex at off}{\pgfpointpolarxy{\hex at edg}{\hex at yy}}}\fi
+  \pgfpointxy{\hex at x}{\hex at y}
   \hex at dbg{2}{Hex coordinates: #1
-    ^^J c=\hex at col
-    ^^J r=\hex at row
-    ^^J v=\hex at vtx
-    ^^J e=\hex at edg
-    ^^J x=\hex at x
-    ^^J y=\hex at y}%
+    ^^J c=`\hex at col'
+    ^^J r=`\hex at row'
+    ^^J v=`\hex at vtx'
+    ^^J e=`\hex at edg'
+    ^^J o=`\hex at off'
+    ^^J x=`\hex at x'
+    ^^J y=`\hex at y'}%
   \global\let\hex at x\hex at x%
   \global\let\hex at y\hex at y%
   \global\let\hex at row\hex at row%
@@ -246,6 +258,67 @@
 }
 \tikzdeclarecoordinatesystem{hex}{%
   \hex at coords@conv{#1}}
+\tikzset{%
+  /hex/.cd,
+  bev/.store in=\hex at bevel,               bev/.initial=,
+  bevel fraction/.store in=\hex at bevel@frac,bevel fraction/.initial=10,
+  bevel/.is choice,
+  bevel/none/.style       = {/hex/bev=},
+  bevel/north west/.style = {/hex/bev=1},
+  bevel/north east/.style = {/hex/bev=2},
+  bevel/south west/.style = {/hex/bev=3},
+  bevel/south east/.style = {/hex/bev=4},
+  bevel/NW/.style         = {/hex/bev=1},
+  bevel/NE/.style         = {/hex/bev=2},
+  bevel/SW/.style         = {/hex/bev=3},
+  bevel/SE/.style         = {/hex/bev=4},
+  bevel/.default          = {north west},
+}
+\def\hex at bevel@frac{10}
+\tikzset{
+  hex/bevel highlight/.style={fill=white,opacity=.25},
+  hex/bevel shadow/.style={fill=black,opacity=.25},
+}
+\newdimen\wg at tmpe
+\newdimen\wg at tmpf
+\newdimen\wg at tmpg
+\def\hex at bevel@path#1{%
+  \scope[#1]
+  \wg at tmpe=\wg at tmpa\multiply\wg at tmpe by \hex at bevel@frac
+  \wg at tmpf=\wg at tmpb\multiply\wg at tmpf by \hex at bevel@frac
+  \wg at tmpg=\wg at tmpc\multiply\wg at tmpg by \hex at bevel@frac
+  \divide\wg at tmpe100
+  \divide\wg at tmpf100
+  \divide\wg at tmpg100
+  % Start
+  \pgfpathmoveto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Left
+  \pgfpathlineto{\pgfqpoint{-\wg at tmpa}{\wg at tmpb}}%
+  % Left-down
+  \pgfpathlineto{\pgfqpoint{\wg at tmpc}{\wg at tmpd}}%
+  % Right down
+  \wg at tmpa=-\wg at tmpa%
+  \wg at tmpb=-\wg at tmpb%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Up, in
+  \advance\wg at tmpa\wg at tmpe%
+  \advance\wg at tmpb\wg at tmpf%
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Left-down, in
+  \advance\wg at tmpc-\wg at tmpg
+  \pgfpathlineto{\pgfqpoint{\wg at tmpc}{\wg at tmpd}}%
+  % Left, down in
+  \advance\wg at tmpb-\wg at tmpf\wg at tmpb-\wg at tmpb%
+  \advance\wg at tmpb-\wg at tmpf
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % Start, down in
+  \advance\wg at tmpa-\wg at tmpe\wg at tmpa-\wg at tmpa%
+  \advance\wg at tmpa-\wg at tmpe
+  \pgfpathlineto{\pgfqpoint{\wg at tmpa}{\wg at tmpb}}%
+  % %
+  \pgfclosepath%
+  \pgfusepath{fill}
+  \endscope}%
 \hex at dbg{5}{Base vertex: \hex at xx,\hex at yy}
 \hex at dbg{5}{Base edges: \hex at e@xx,\hex at e@yy}
 \pgfdeclareshape{hex/hex}{%
@@ -355,6 +428,35 @@
       \fi%
       \@ifundefined{hex at label}{\let\hex at label\empty}{}
       \ifx\hex at label\empty\else\hex at do@label\fi%
+    \@ifundefined{hex at bevel}{\let\hex at bevel\empty}{}
+    \ifx\hex at bevel\empty\else%
+      \northeast
+      \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
+      \west
+      \wg at tmpc=\pgf at x\wg at tmpd=\pgf at y%
+      \ifcase\hex at bevel\relax
+      \or%1
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%2
+      \or\wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%3
+      \or% 4
+        \wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%
+        \wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%
+      \fi
+      \hex at bevel@path{chit/bevel highlight}
+      \northeast
+      \wg at tmpa=-\pgf at x\wg at tmpb=-\pgf at y%
+      \west
+      \wg at tmpc=-\pgf at x\wg at tmpd=-\pgf at y%
+      \ifcase\hex at bevel\relax
+      \or%1
+      \or\wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%2
+      \or\wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%3
+      \or% 4
+        \wg at tmpa=-\wg at tmpa\wg at tmpc=-\wg at tmpc%
+        \wg at tmpb=-\wg at tmpb\wg at tmpd=-\wg at tmpd%
+      \fi
+      \hex at bevel@path{chit/bevel shadow}
+    \fi
     \endscope%
     \@ifundefined{hex at town}{\let\hex at town\empty}{}
     \@ifundefined{hex at c@pic}{\let\hex at c@pic\empty}{}
@@ -432,7 +534,7 @@
   pic/.default=,
   image/.default=,
   code/.default=,
-  clip/.default,
+  clip/.default=,
 }
 \iffalse
 \tikzset{
@@ -472,8 +574,9 @@
       ^^J  clip:  \meaning\hex at t@clip}
     \@ifundefined{hex at t@clip}{\let\hex at t@clip\empty}{}
     \ifx\hex at t@clip\empty\else%
+      \edef\hex at t@cc{\hex at t@clip}%
       \def\hex at t@c{}
-      \foreach \c in \hex at t@clip{%
+      \foreach \c in \hex at t@cc{%
         \hex at dbg{5}{Clipping to `\c'}
         \expandafter\wg at pic\c\@endwg at pic {}{\wg at tmpa,\wg at tmpb}{%
           save path=\hex at t@tmp}%
@@ -486,6 +589,9 @@
     %% macros are undefined, define them to be empty
     \@ifundefined{hex at t@pic}{\let\hex at t@pic\empty}{}
     \@ifundefined{hex at t@image}{\let\hex at t@image\empty}{}
+    \@ifundefined{hex at t@code}{\let\hex at t@code\empty}{}
+    \@ifundefined{hex at t@code}{\let\hex at t@code\empty}{}
+    \ifx\hex at t@code\empty\else\hex at t@code\fi%
     % If we have no image, check if we have pictures.
     \ifx\hex at t@image\empty%
       \hex at dbg{8}{No terrain images}%
@@ -493,7 +599,8 @@
         % We have pictures
         \hex at dbg{5}{Terrain pictures}%
         \pgfpointorigin\wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
-        \wg at pic@all{\hex at t@pic}{}{\the\wg at tmpa,\the\wg at tmpb}{}%
+        \foreach \i in \hex at t@pic{%
+          \wg at pic@all{\i}{}{\the\wg at tmpa,\the\wg at tmpb}{}}%
       \fi% We have pictures.
       \else % We have images
         \hex at dbg{5}{Terrain images}%
@@ -12078,20 +12185,23 @@
   }
 }
 \tikzset{%
-  hex/ridges/.style={
+  hex/ridges pre/.style={
     line cap=round,
     draw=pgfstrokecolor,
-    rounded corners=.25cm,
-    scale line widths,
+    solid,
+    /hex/ridges/.cd,%
+    radius=0.85,%
+    n=4,
+    R=.25,
+  },
+  hex/ridges/.style={
+    get scale,
     decoration={
       path has corners=true,
-      pre=waves,
-      post=waves,
-      pre length=-.1cm,
-      post length=0cm,
       waves,
-      radius=.2cm,
-      segment length=.2cm},
+      radius=\wg at scale\hex at r@R,
+      segment length=\wg at scale\hex at r@s,
+    },
     decorate}}
 \newif\ifhex at r@ne
 \newif\ifhex at r@n
@@ -12109,6 +12219,7 @@
   south/.is if=hex at r@s,
   south east/.is if=hex at r@se,
   radius/.store in=\hex at r@r,
+  curve radius/.store in=\hex at r@w,
   NE/.is if=hex at r@ne,
   N/.is if=hex at r@n,
   NW/.is if=hex at r@nw,
@@ -12116,11 +12227,14 @@
   S/.is if=hex at r@s,
   SE/.is if=hex at r@se,
   r/.store in=\hex at r@r,
+  n/.store in=\hex at r@n,
+  R/.store in=\hex at r@w,
 }
+\newdimen\hex at r@s
+\newdimen\hex at r@R
 \def\hex at do@ridges{%
   \edef\hex at r@tmp{[
-    /hex/ridges/.cd,%
-    radius=0.8,%
+    hex/ridges pre,
     /tikz/every hex ridges/.try,
     \hex at ridges]}
   \expandafter\scope\hex at r@tmp%
@@ -12132,44 +12246,70 @@
       ^^Jsouth     =\ifhex at r@s  yes\else no\fi
       ^^Jsouth east=\ifhex at r@se yes\else no\fi
       ^^Jradius    =\hex at r@r
+      ^^Jn         =\hex at r@n
     }
+    \pgfmathparse{\hex at r@r/\hex at r@n}\xdef\hex at r@t{\pgfmathresult}
+    \hex at r@s=\hex at r@t cm
+    \hex at r@R=\hex at r@w cm
     \def\hex at r@p{}
     % Hand written algorithm
     \ifhex at r@ne
-      \def\hex at r@p{(0:\hex at r@r)--(60:\hex at r@r)}
+      \ifhex at r@se
+        \xdef\hex at r@p{(0:\hex at r@r)--(60:\hex at r@r)}
+      \else
+        \xdef\hex at r@p{($(0:\hex at r@r)+(-60:\hex at r@t/2)$)--(60:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along north east edge: `\hex at r@p'}
     \fi
     \ifhex at r@n
-      \hex at dbg{4}{Ridge along north edge: `\hex at r@p'}
       \ifhex at r@ne\else
-        \xdef\hex at r@p{\hex at r@p ( 60:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($( 60:\hex at r@r)+(0:\hex at r@t/2)$)}
       \fi
       \xdef\hex at r@p{\hex at r@p --(120:\hex at r@r)}
+      \hex at dbg{4}{Ridge along north edge: `\hex at r@p'}
     \fi
     \ifhex at r@nw
       \ifhex at r@n\else
-        \xdef\hex at r@p{\hex at r@p (120:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(120:\hex at r@r)+(60:\hex at r@t/2)$)}
       \fi
       \xdef\hex at r@p{\hex at r@p --(180:\hex at r@r)}
+      \hex at dbg{4}{Ridge along north west: `\hex at r@p'}
     \fi
     \ifhex at r@sw
       \ifhex at r@nw\else
-        \xdef\hex at r@p{\hex at r@p (180:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(180:\hex at r@r)+(120:\hex at r@t/2)$)}
       \fi
-      \xdef\hex at r@p{\hex at r@p --(240:\hex at r@r)}
+      \ifhex at r@s
+        \xdef\hex at r@p{\hex at r@p --(240:\hex at r@r)}
+      \else
+        \xdef\hex at r@p{\hex at r@p --(240:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along south west: `\hex at r@p'}
     \fi
     \ifhex at r@s
       \ifhex at r@sw\else
-        \xdef\hex at r@p{\hex at r@p (240:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(240:\hex at r@r)+(-\hex at r@t/2,0)$)}
       \fi
-      \xdef\hex at r@p{\hex at r@p --(300:\hex at r@r)}
+      \ifhex at r@se
+        \xdef\hex at r@p{\hex at r@p --(300:\hex at r@r)}
+      \else
+        \xdef\hex at r@p{\hex at r@p --(300.5:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along south: `\hex at r@p'}
     \fi
     \ifhex at r@se
       \ifhex at r@s\else
-        \xdef\hex at r@p{\hex at r@p (300:\hex at r@r)}
+        \xdef\hex at r@p{\hex at r@p ($(300:\hex at r@r)+(-120:\hex at r@t/2)$)}
       \fi
-      \xdef\hex at r@p{\hex at r@p --(360:\hex at r@r)}
+      \ifhex at r@ne
+        \xdef\hex at r@p{\hex at r@p --cycle}
+      \else
+        \xdef\hex at r@p{\hex at r@p --(.5:\hex at r@r)}
+      \fi
+      \hex at dbg{4}{Ridge along se: `\hex at r@p'}
     \fi
     \hex at dbg{3}{ Ridges path: \hex at r@p}
+    % \draw[red] \hex at r@p;
     \draw[hex/ridges] \hex at r@p;
   \endscope% End of ridges scope
 }
@@ -12176,6 +12316,7 @@
 \tikzset{%
   hex/town/.style={
     scale line widths,
+    solid,
     thin,
     fill=pgfstrokecolor,
     color=pgfstrokecolor},
@@ -12184,7 +12325,7 @@
     shape=rectangle,
     above right=.1,
     color=pgfstrokecolor,
-    font=\sffamily\normalsize}
+    font=\sffamily\fontsize{11}{13}\selectfont}
 }
 \tikzset{%
   /hex/town/.search also={/tikz},%
@@ -12199,11 +12340,11 @@
   city/.style={pic=hex/town/city}
 }
 \tikzset{%
-  hex/town/village/.pic={\path[fill,pic actions] circle(.1);},
-  hex/town/town/.pic={\path[fill,pic actions] circle(.2);},
+  hex/town/village/.pic={\path[fill,solid,pic actions] circle(.1);},
+  hex/town/town/.pic={\path[fill,solid,pic actions] circle(.2);},
   hex/town/city/.pic={%
-    \path[fill,pic actions] circle(.25);
-    \path[draw,pic actions] circle(.35);}
+    \path[fill,solid,pic actions] circle(.25);
+    \path[draw,solid,pic actions] circle(.35);}
 }
 \def\hex at c@nameparse{%
   \@ifnextchar[{\hex at c@namep at rse}{\hex at c@namep at rse[]}%]
@@ -12327,7 +12468,7 @@
 }
 \tikzset{%
   hex/fortress/.pic={
-    \path[draw,pic actions]
+    \path[draw,solid,pic actions]
     (0:  .9) --
     (0:  .7) --
     (60: .7) -- ( 60:.9) -- ( 60:.7) --
@@ -12401,7 +12542,20 @@
     }
   }%
 }%
-
+\pgfdeclaredecoration{fortification}{initial}
+{
+  \state{initial}[width=4\pgflinewidth]
+  {
+    \pgfpathlineto{\pgfpoint{2\pgflinewidth}{0}}
+    \pgfpathlineto{\pgfpoint{2\pgflinewidth}{2\pgflinewidth}}
+    \pgfpathlineto{\pgfpoint{4\pgflinewidth}{2\pgflinewidth}}
+    \pgfpathlineto{\pgfpoint{4\pgflinewidth}{0}}
+  }
+  \state{final}
+  {
+    \pgfpathlineto{\pgfpointdecoratedpathlast}
+  }
+}
 \tikzset{
   hex/road/.style={
     rounded corners=3\pgflinewidth,% .25cm,
@@ -12408,12 +12562,15 @@
     color=black,
     transform shape,
     scale line widths,
-    thick},
+    thick,
+    every hex road/.try,
+  },
   hex/railroad/.style={
     %scale line widths,
     rounded corners=.25cm,
     color=gray!50!black,
     transform shape,
+    every hex railroad/.try,
     postaction={draw,decorate},
     decoration={ticks,
       segment length=9\pgflinewidth,
@@ -12423,50 +12580,68 @@
   hex/river/.style={
     color=blue,
     scale line widths,
+    scale rounded corners,
     line width=3pt,
     transform shape,
+    every hex river/.try,
     decorate,
     decoration={random steps,
-      segment length=.3cm,
-       amplitude=.15cm,
+      segment length=3\pgflinewidth,
+       amplitude=1.5\pgflinewidth,
        pre=lineto,
        post=lineto,
-       pre length=.05cm,
-       post length=.05cm},
-     rounded corners=.08cm},
+       pre length=.5\pgflinewidth,
+       post length=.5\pgflinewidth},
+     rounded corners=.75\pgflinewidth},
    hex/border/.style={
      color=gray,
-     rounded corners=3pt,
      dashed,
      transform shape,
      scale line widths,
-     very thick
+     very thick,
+     rounded corners=3\pgflinewidth,
+     every hex border/.try
    },
-   every river/.style={},
-   every road/.style={},
-   every railroad/.style={},
-   every border/.style={},
+   %
+   % Fortification line
+   %
+   hex/fortified line/.style={
+     draw=brown!50!black,
+     scale line widths,
+     line width=2pt,
+     every hex fortification line/.try,
+     decoration={fortification,raise=-2\pgflinewidth},
+     decorate},
+   % every river/.style={},
+   % every road/.style={},
+   % every railroad/.style={},
+   % every border/.style={},
 }
 \def\road{%
-  \hex at dbg{3}{Road}
+  %\hex at dbg{3}{Road}
   \@ifnextchar[{\road@}{\road@[]}%]
 }
 \def\road@[#1]{\draw[hex/road,every hex road/.try,#1]}
 \def\railroad{%
-  \hex at dbg{3}{Rail road}
+  %\hex at dbg{3}{Rail road}
   \@ifnextchar[{\railroad@}{\railroad@[]}%]
 }
 \def\railroad@[#1]{\draw[hex/railroad,every hex railroad/.try,#1]}
 \def\river{%
-  \hex at dbg{3}{River}
+  %\hex at dbg{3}{River}
   \@ifnextchar[{\river@}{\river@[]}%]
 }
-\def\river@[#1]{\draw[hex/river,every hex river/.try,#1]}
+\def\river@[#1]{\draw[hex/river,#1]}
 \def\border{%
   \hex at dbg{3}{Border}
   \@ifnextchar[{\border@}{\border@[]}%]
 }
 \def\border@[#1]{\draw[hex/border,every hex border/.try,#1]}
+\def\fortifiedline{%
+  \@ifnextchar[{\fortifiedline@}{\fortifiedline@[]}%]
+}%
+\def\fortifiedline@[#1]{%
+  \draw[hex/fortified line,every hex fortified line/.try,#1]}
 \def\shiftScalePath#1#2{%
   \let\tmp at path\@undefined%
   \foreach \x/\y in {#2}{%
@@ -12502,10 +12677,10 @@
       markings,
       mark=between positions 0 and 1 step 0.75*\hex at scale*\hex at dy with {
         \node [single arrow,
-               single arrow head extend=3pt,
+               single arrow head extend=.1*\hex at scale*\hex at dy,
                fill=#1,
-               inner sep=\hex at scale*.5mm,
-               minimum width=\hex at scale*2mm,
+               inner sep=0.05*\hex at scale*\hex at dy,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=\hex at scale*\hex at dy/2,
                transform shape]{};
       }
@@ -12519,10 +12694,10 @@
       markings,
       mark=between positions 0 and 1 step 0.5*\hex at scale*\hex at dy with {
         \node [single arrow,
-               single arrow head extend=3pt,
+               single arrow head extend=.1*\hex at scale*\hex at dy,
                fill=#1,
-               inner sep=\hex at scale*.5mm,
-               minimum width=\hex at scale*2mm,
+               inner sep=0.05*\hex at scale*\hex at dy,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=\hex at scale*\hex at dy/3,
                transform shape]{};
       }
@@ -12543,7 +12718,7 @@
                anchor=west,
                inner sep=\hex at scale*.25mm,
                outer sep=.3*\hex at scale*\hex at dy,
-               minimum width=\hex at scale*2mm,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=1.4*\hex at scale*\hex at dy,
                transform shape]{};
       }
@@ -12563,10 +12738,10 @@
         transform shape] {};},
       mark=between positions 0 and 1 step 0.75*\hex at scale*\hex at dy with {
         \node [single arrow,
-               single arrow head extend=\hex at scale*3pt,
+               single arrow head extend=.1*\hex at scale*\hex at dy,
                fill=#1,
-               inner sep=\hex at scale*1mm,
-               minimum width=\hex at scale*3mm,
+               inner sep=0.05*\hex at scale*\hex at dy,
+               minimum width=0.02*\hex at scale*\hex at dy,
                minimum height=\hex at scale*\hex at dy/2,
                transform shape]{};
       }
@@ -12585,7 +12760,7 @@
     fill=#2,
     transform shape,
     text=#1,
-    font=\sffamily\bfseries\Large},
+    font=\sffamily\bfseries\fontsize{14.4}{17}\selectfont},
   hex/move cost/.default={black}{none},
   % Argument is fill colour
   hex/short line/.style={%
@@ -12636,152 +12811,247 @@
   hex/advance/.style={hex/short line=#1},
   hex/advance/.default={green!70!black},
 }
-\tikzset{
-  hex/board frame/.style={draw}
-}
 \def\boardframe{%
-  \@ifnextchar[{\bo at rdframe}{\bo at rdframe[0]}%}
+  \@ifnextchar[{\bo at rdframe}{\bo at rdframe[0]}%]
 }
+\def\bo at rdfr@me{
+  \@ifnextchar[{\bo at rdfr@me@}{\bo at rdfr@me@[]}%]
+}
+\def\bo at rdfr@me at u(#1)#2#3#4#5{
+  \hex at coords@conv{#1}
+  % \hex at dbg{0}{#1 -> `\hex at x',`\hex at y'}
+  \pgfmathparse{min(#2,\hex at x)}\xdef#2{\pgfmathresult}
+  \pgfmathparse{min(#3,\hex at y)}\xdef#3{\pgfmathresult}
+  \pgfmathparse{max(#4,\hex at x)}\xdef#4{\pgfmathresult}
+  \pgfmathparse{max(#5,\hex at y)}\xdef#5{\pgfmathresult}
+  \hex at dbg{2}{#1 -> ll=`#2',`#3', ur=`#4',`#5'}
+}
+\def\bo at rdfr@me@[#1]#2#3#4#5{
+  % Define rtmp and a ctmp to by directions
+  \pgfmathparse{int(\hex at coords@row at fac)}\edef\rtmp{\pgfmathresult}
+  \pgfmathparse{int(\hex at coords@col at fac)}\edef\ctmp{\pgfmathresult}
+  % Define vertices for path
+  \def\ctfv{SW}
+  \def\ctsv{SE}
+  \def\cbfv{NE}
+  \def\cbsv{NW}
+  \def\rrfv{E}
+  \def\rrsv{NE}
+  \def\rlfv{W}
+  \def\rlsv{SW}
+  % Swap around some definitions based on the row direction
+  \ifnum\rtmp<0
+    \let\max at short\hex at bot@short at col
+    \let\min at short\hex at top@short at col
+    \let\swp\ctfv\let\ctfv\cbsv\let\cbsv\swp
+    \let\swp\ctsv\let\ctsv\cbfv\let\cbfv\swp
+    \def\rrsv{SE}
+    \def\rlsv{NW}
+  \else
+    \let\max at short\hex at top@short at col
+    \let\min at short\hex at bot@short at col
+  \fi
+  % Swap around some definitions based on the column direction
+  \ifnum\ctmp<0
+    \let\swp\ctfv\let\ctfv\ctsv\let\ctsv\swp
+    \let\swp\cbfv\let\cbfv\cbsv\let\cbsv\swp
+    \let\swp\rrfv\let\rrfv\rlsv\let\rlsv\swp
+    \let\swp\rrsv\let\rrsv\rlfv\let\rlfv\swp
+  \fi
+  % Define tmp = 0 if no shorts, 1 if top short, 2 if both
+  \pgfmathparse{ifthenelse(\hex at got@top at short,
+    ifthenelse(\hex at got@bot at short,2,1),0)}\edef\tmp{\pgfmathresult}
+  % If top-short, set factors
+  \ifnum\tmp=1
+    \def\mnf{-1}
+    \def\mxf{-1}
+    \def\mnn{}
+    \def\mxn{}
+  % If both short, set factors
+  \else\ifnum\tmp=2
+    \def\mnf{\rtmp}
+    \def\mxf{(-\rtmp)}
+    % If inverse rows, set factors
+    \ifnum\rtmp<0
+      \def\mnn{}
+      \def\mxn{not}
+    \else
+      \def\mnn{not}
+      \def\mxn{}
+    \fi
+  % If none is short
+  \else
+    \def\mnf{1}
+    \def\mxf{1}
+    \def\mnn{not}
+    \def\mxn{not}
+  \fi\fi
+  % Define row at mn to give least row of column
+  \def\row at mn##1{%
+    \pgfmathparse{int(#3+\mnf*
+      \hex at coords@row at fac*\min at short(##1)*
+      \mnn(\min at short(\hex at coords@col at off)))}
+    \edef\lr{\pgfmathresult}}
+  % Define row at mx to give largest row of column
+  \def\row at mx##1{%
+    \pgfmathparse{int(#5+\mxf*
+      \hex at coords@row at fac*\max at short(##1)*
+      \mxn(\max at short(\hex at coords@col at off)))}
+    \edef\ur{\pgfmathresult}}
+  %
+  %
+  % Below defines a path around the perimeter of the hexes.
+  %
+  \def\@llx{10000}
+  \def\@lly{10000}
+  \def\@urx{-10000}
+  \def\@ury{-10000}
+  % Start with an empty path
+  \def\p{}
+  % Loop across least row (can be top if \rtmp<0)
+  \foreach \c in {#2,...,#4}{%
+    \row at mn{\c}
+    \row at mx{\c}
+    % \message{^^JColumn: `\c' -> `\lr',`\ur' (#3,#5)}
+  }
+  \foreach \c in {#2,...,#4}{%
+    \row at mn{\c}
+    \xdef\p{\p
+      (hex cs:c=\c,r=\lr,v=\ctfv)--
+      (hex cs:c=\c,r=\lr,v=\ctsv)--}
+    \bo at rdfr@me at u(c=\c,r=\lr,v=\ctfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=\c,r=\lr,v=\ctsv)\@llx\@lly\@urx\@ury
+  }
+  % Go up (down if \rtmp<0) right side
+  \row at mn{#4}
+  \row at mx{#4}
+  \foreach \r in {\lr,...,\ur}{%
+    \xdef\p{\p
+      (hex cs:c=#4,r=\r,v=\rrfv)--
+      (hex cs:c=#4,r=\r,v=\rrsv)--}
+    \bo at rdfr@me at u(c=#4,r=\r,v=\rrfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=#4,r=\r,v=\rrsv)\@llx\@lly\@urx\@ury
+  }
+  % Go across largest row (can be bottom if \rtmp<0)
+  \foreach \c in {#4,...,#2}{%
+    \row at mx{\c}
+    % \message{^^JColumn: `\c', max:`\ur'}
+    \xdef\p{\p
+      (hex cs:c=\c,r=\ur,v=\cbfv)--
+      (hex cs:c=\c,r=\ur,v=\cbsv)--}
+    \bo at rdfr@me at u(c=\c,r=\ur,v=\cbfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=\c,r=\ur,v=\cbsv)\@llx\@lly\@urx\@ury
+  }
+  % Go up (down if \rtmp<0) left side.
+  \row at mn{#2}
+  \row at mx{#2}
+  \foreach \r in {\ur,...,\lr}{%
+    \xdef\p{\p
+       (hex cs:c=#2,r=\r,v=\rlfv)--
+       (hex cs:c=#2,r=\r,v=\rlsv)--}
+    \bo at rdfr@me at u(c=#2,r=\r,v=\rlfv)\@llx\@lly\@urx\@ury
+    \bo at rdfr@me at u(c=#2,r=\r,v=\rlsv)\@llx\@lly\@urx\@ury
+  }
+  % End path with cycle
+  \edef\p{\p cycle}
+  % Define global path
+  \global\let\hex at board@path\p
+  \hex at dbg{3}{Hex board path: `\meaning\hex at board@path'}
+  % If an optional argument was given, then use that to actually make
+  % hexes.
+  \ifx|#1|\else
+    \foreach[count=\nc] \c in {#2,...,#4}{%
+      \row at mn{\c}
+      \row at mx{\c}
+      \foreach \r in {\lr,...,\ur}{%
+        \hex[#1={\c,\r}](c=\c,r=\r)
+      }
+    }
+  \fi
+}
+\tikzset{%
+  /hex/board/no op/.style args={#1,#2}{}}
+\def\boardhexes{%
+  \@ifnextchar[{\bo at rdhexes}{\bo at rdhexes[board/no op]}%]
+}
+\def\bo at rdhexes[#1](#2)(#3){%
+  \hex at coords@conv{#2}
+  \edef\llc{\hex at col}
+  \edef\llr{\hex at row}
+  \hex at coords@conv{#3}
+  \edef\urc{\hex at col}
+  \edef\urr{\hex at row}
+  \bo at rdfr@me[#1]{\llc}{\llr}{\urc}{\urr}}
+\tikzset{board frame bb/.code={
+    \pgfkeys{
+      %/tikz/local bounding box=tmp board frame,
+      /tikz/transform shape,
+      /tikz/execute at end scope={%
+        % \hex at dbg{1}{Getting board frame BB}
+        %\wg at get@bb{tmp board frame}
+        \global\let\llx\@llx
+        \global\let\lly\@lly
+        \global\let\urx\@urx
+        \global\let\ury\@ury
+        % \hex at dbg{0}{Board bounding box (\llx,\lly)x(\urx,\ury)}
+      }}}}
+
 \def\bo at rdframe[#1](#2)(#3){%
   \hex at coords@conv{#2}
-  \edef\llx{\hex at x}
-  \edef\lly{\hex at y}
   \edef\llc{\hex at col}
   \edef\llr{\hex at row}
-  \edef\ellc{\hex at eff@col}
-  \edef\ellr{\hex at eff@row}
   %
   \hex at coords@conv{#3}
-  \edef\urx{\hex at x}
-  \edef\ury{\hex at y}
   \edef\urc{\hex at col}
   \edef\urr{\hex at row}
-  \edef\eurc{\hex at eff@col}
-  \edef\eurr{\hex at eff@row}
   %
   \def\margin{#1}
   %
-  \hex at dbg{2}{%
-    Board Hex range: (\llc,\llr)x(\urc,\urr)
-    ^^JEffective range: (\ellc,\ellr)x(\eurc,\eurr)
-    ^^JBB:              (\llx,\lly)x(\urx,\ury)}%
-  \ifnum\hexdbglvl>1
-    %\draw[red,very thick](hex cs:c=\llc,r=\llr) rectangle(hex cs:c=\urc,r=\urr);
-    \draw[red,ultra thick,dashed](\llx,\lly) rectangle(\urx,\ury);
-    \draw[->,very thick,blue] (0,0) -- (0,1) (0,0) -- (1,0);
-  \fi
-  % Calculate how many half hex hides to add to the "bottom"
+  % This will store the bounding box in tmp node `board frame'
+  \bo at rdfr@me{\llc}{\llr}{\urc}{\urr}%
+  \begin{scope}[board frame bb]
+    \expandafter\path\hex at board@path;
+  \end{scope}
+  \hex at dbg{1}{Board frame LL: -> `\llx',`\lly'}
+  \pgfmathparse{\llx+ifthenelse(\llx<0,-1,1)*\margin}\edef\llx{\pgfmathresult}
+  \pgfmathparse{\lly+ifthenelse(\lly<0,-1,1)*\margin}\edef\lly{\pgfmathresult}
   %
-  \def\oddeven{isodd}
-  \ifnum\hex at coords@row at fac<0\def\oddeven{iseven}\fi%
-  \pgfmathparse{
-    ifthenelse(\hex at got@bot at short(\ellc),
-     ifthenelse(\hex at bot@short at col(\llc)*not(\oddeven(\ellc)),2,
-      ifthenelse(\hex at bot@short at col(\llc),0,1)),
-     ifthenelse(\oddeven(\ellc),1,2))}
-  \edef\olly{\pgfmathresult}%
-  \hex at dbg{2}{Delta lly: \olly half heights}
-  % Calculate how many half hex heights to add to the "top"
-  \def\oddeven{iseven}
-  \ifnum\hex at coords@row at fac<0\def\oddeven{isodd}\fi%
+  \hex at dbg{1}{Board frame UR: -> `\urx',`\ury'}
+  \pgfmathparse{\urx+ifthenelse(\urx<0,-1,1)*\margin}\edef\urx{\pgfmathresult}
+  \pgfmathparse{\ury+ifthenelse(\ury<0,-1,1)*\margin}\edef\ury{\pgfmathresult}
   %
-  \pgfmathparse{
-    ifthenelse(\hex at got@top at short(\urc),
-     ifthenelse(\hex at top@short at col(\urc)*\oddeven(\eurc),0,
-      ifthenelse(\hex at top@short at col(\urc),2,1)),
-     ifthenelse(\oddeven(\eurc),1,2))}
-  \edef\oury{\pgfmathresult}%
-  \hex at dbg{2}{Delta ury: \oury half heights}
-  % Calculate new LLY and URY
-  \pgfmathparse{\lly-\hex at coords@row at fac*(\olly*\hex at yy+\margin)}
-  \edef\lly{\pgfmathresult}
-  \pgfmathparse{\ury+\hex at coords@row at fac*(\oury*\hex at yy+\margin)}
-  \edef\ury{\pgfmathresult}
-  % Calculate new LLX and URX
-  \pgfmathparse{\llx-1-\margin}\edef\llx{\pgfmathresult}
-  \pgfmathparse{\urx+1+\margin}\edef\urx{\pgfmathresult}
-  % Calculate width and height
   \pgfmathparse{\urx-\llx}\edef\w{\pgfmathresult}
   \pgfmathparse{\ury-\lly}\edef\h{\pgfmathresult}
+  %% Print to the log
   \hex at dbg{0}{Board Frame: (\llx,\lly)x(\urx,\ury) (\w x\h) (\llc,\llr)x(\urc,\urr)}
+  %% Possibly draw
   \draw[hex/board frame/.try](\llx,\lly) rectangle(\urx,\ury);
+  %% Store macros
   \xdef\boardXmin{\llx}%
   \xdef\boardYmin{\lly}%
   \xdef\boardXmax{\urx}%
   \xdef\boardYmax{\ury}%
 }
-
-\def\boardpath(#1)(#2){%
+\def\boardpath(#1)(#2){
   \hex at coords@reset%
   \tikzset{/hex/coords/.cd, #1}
-  \edef\llx{\hex at col}
-  \edef\lly{\hex at row}
+  \edef\llc{\hex at col}
+  \edef\llr{\hex at row}
   %%
   \hex at coords@reset%
   \tikzset{/hex/coords/.cd, #2}
-  \edef\urx{\hex at col}
-  \edef\ury{\hex at row}
-  \let\board at odd\@undefined%
-  \hex at dbg{1}{Board BB in hex: (\llx,\lly)x(\urx,\ury)}
-  %%
-  \def\fv{south}
-  \def\sv{north}
-  \ifnum\hex at coords@row at fac<0
-    \def\fv{north}
-    \def\sv{south}
-  \fi
+  \edef\urc{\hex at col}
+  \edef\urr{\hex at row}
 
-  \edef\hex at board@path{(hex cs:c=\llx,r=\lly,v=\fv\space west)}
-  %% First the left side
-  \foreach \r in {\lly,...,\ury} {%
-    \edef\t{
-      --(hex cs:c=\llx,r=\r,v=west)
-      --(hex cs:c=\llx,r=\r,v=\sv\space west)}
-    \wg at addto@macro{\hex at board@path}{\t}}
-  %% Then for top of board
-  \foreach \c in {\llx,...,\urx} {%
-    % To be done
-    %\pgfmathparse{int(ifthenelse(\hex at bot@short at col(\c),1,0))}
-    %\edef\tmp{\pgfmathresult}
-    %\ifnum\tmp>0
-    %\edef\t{
-    %  --(hex cs:c=\c,r=\ury,v=\sv\space west)
-    %  --(hex cs:c=\c,r=\ury,v=\sv\space east)}
-    %\else
-    \edef\t{
-      --(hex cs:c=\c,r=\ury,v=\fv\space east)
-      --(hex cs:c=\c,r=\ury,v=\fv\space west)}
-    %\fi
-    \wg at addto@macro{\hex at board@path}{\t}}
-  %% Then for right of board
-  \foreach \r in {\ury,...,\lly} {%
-    \edef\t{
-      --(hex cs:c=\urx,r=\r,v=east)
-      --(hex cs:c=\urx,r=\r,v=\fv\space east)}
-    \wg at addto@macro{\hex at board@path}{\t}}
-
-  %% Then for bottom of board
-  \edef\t{--(hex cs:r=\lly,c=\urx,v=\fv\space west)}
-  \wg at addto@macro{\hex at board@path}{\t}
-  \foreach \c in {\urx,...,\llx} {%
-    \pgfmathparse{int(ifthenelse(\hex at bot@short at col(\c),1,0))}
-    \edef\tmp{\pgfmathresult}
-    \ifnum\tmp>0
-    \edef\t{
-      --(hex cs:c=\c,r=\lly,v=\sv\space east)
-      --(hex cs:c=\c,r=\lly,v=\sv\space west)}
-    \else
-    \edef\t{
-      --(hex cs:c=\c,r=\lly,v=\fv\space east)
-      --(hex cs:c=\c,r=\lly,v=\fv\space west)}
-    \fi
-    \wg at addto@macro{\hex at board@path}{\t}}
-
-  \def\t{--cycle}
-  \wg at addto@macro{\hex at board@path}{\t}
+  % This will store the bounding box in tmp node `board frame'
+  \bo at rdfr@me{\llc}{\llr}{\urc}{\urr}%
+  %% Use the path to extract the bounding box
+  %\begin{scope}[local bounding box=board frame]
+  %  \expandafter\path\hex at board@path;
+  %\end{scope}
   \global\let\hexboardpath\hex at board@path
 }
-%% New definition - much simpler
+
 \def\boardclip(#1)(#2)#3{%
   \boardpath(#1)(#2)
   \draw \ifx|#3|\else[preaction={#3}]\fi%
@@ -12806,6 +13076,301 @@
   zone scope/.style={},
   zone path/.style={}
 }
+\tikzset{%
+  % Margin must be <1cm
+  split/paper outline/.style={
+    shape=rectangle,
+    draw=red!50!black,
+    line width=.5mm},
+  split/effective outline/.style={
+    shape=rectangle,
+    draw=green!50!black,
+    dashed,
+    line width=.5mm},
+  split/board outline/.style={%
+    draw=magenta,
+    line width=.5mm,
+    dotted},
+}
+\newdimen\split at tmp
+\def\split at getem#1{%
+  \draw (#1.north east);%
+  \pgfgetlastxy{\split at ulx}{\split at uly}%
+  \xdef\split at ulx{\split at ulx}%
+  \xdef\split at ulx{\split at ulx}%
+  \draw (#1.south west);%
+  \pgfgetlastxy{\split at lrx}{\split at lry}%
+  \xdef\split at lrx{\split at lrx}%
+  \xdef\split at lry{\split at lry}%
+}
+\def\split at getboard#1{%
+  \split at getem{#1}%
+  \xdef\split at bulx{\split at ulx}%
+  \xdef\split at buly{\split at uly}%
+  \xdef\split at blrx{\split at lrx}%
+  \xdef\split at blry{\split at lry}%
+  \split at w{\@percentchar\space Board:
+    (\split at bulx,\split at buly)(\split at blrx,\split at blry)}}
+\def\split at adj#1#2{%
+  \split at tmp=#2%
+  \divide\split at tmp by 2%
+  \advance\split at tmp by #1%
+  \edef\t{\the\split at tmp}}
+\def\split at get@init#1#2#3#4{%
+  \pgfmathparse{((#1 * #3 - (#1 - 1) * #2) - #4)/2}%
+  \xdef\split at off{\pgfmathresult}%
+  \hex at dbg{2}{((#1 * #3 - (#1 - 1) * #2) - #4)/2 -> `\split at off'}}
+\def\split at getinit#1#2#3#4#5#6#7{%
+  \split at get@init{#1}{#3}{#4}{#6}\xdef\dy{\split at off cm}
+  \split at get@init{#2}{#3}{#5}{#7}\xdef\dx{\split at off cm}}
+\def\split at getcoords#1#2#3{%
+  \hex at dbg{2}{Getting coords `c#1r#2'}%
+  \split at getem{c#1r#2}%
+  \edef\sulx{\split at ulx}%
+  \edef\suly{\split at uly}%
+  \edef\slrx{\split at lrx}%
+  \edef\slry{\split at lry}%
+  \edef\mlx{\split at blrx}%
+  \edef\mrx{\split at bulx}%
+  \edef\mty{\split at buly}%
+  \edef\mby{\split at blry}%
+  \pgfmathparse{int(#1-1)}\edef\pc{\pgfmathresult}%
+  \pgfmathparse{int(#2-1)}\edef\pr{\pgfmathresult}%
+  \pgfmathparse{int(#1+1)}\edef\nc{\pgfmathresult}%
+  \pgfmathparse{int(#2+1)}\edef\nr{\pgfmathresult}%
+  \pgfutil at ifundefined{pgf at sh@ns at c\pc r#2}{}{% Left
+    \hex at dbg{3}{\space Getting left `c\pc r#2'}%
+    \split at getem{c\pc r#2}\split at adj{\split at ulx}{-#3}\edef\mlx{\t}}%
+  \pgfutil at ifundefined{pgf at sh@ns at c\nc r#2}{}{% Right
+    \hex at dbg{3}{\space Getting right `c\nc r#2'}%
+    \split at getem{c\nc r#2}\split at adj{\split at lrx}{#3}\edef\mrx{\t}}%
+  \pgfutil at ifundefined{pgf at sh@ns at c#1r\pr}{}{% Above
+    \hex at dbg{3}{\space Getting above `c#1 r\pr'}%
+    \split at getem{c#1r\pr}\split at adj{\split at lry}{#3} \edef\mty{\t}}%
+  \pgfutil at ifundefined{pgf at sh@ns at c#1r\nr}{}{% Below
+    \hex at dbg{3}{\space Getting below `c#1 r\nr'}%
+    \split at getem{c#1r\nr}\split at adj{\split at uly}{-#3}\edef\mby{\t}}%
+  \draw[fill=red]  (\mlx,\mty) circle(.2);%
+  \draw[fill=green](\mrx,\mty) circle(.4);%
+  \draw[fill=blue] (\mlx,\mby) circle(.6);%
+  \draw[fill=cyan] (\mrx,\mby) circle(.8);%
+  \split at w{%
+    \@percentchar^^J%
+    \string\segment(\sulx,\suly)(\slrx,\slry){\mlx}{\mrx}{\mby}{\mty}
+    \@percentchar\space c#1r#2}
+}
+\newwrite\split at calcout
+\def\split at w{\immediate\write\split at calcout}
+\def\split at header#1{%
+  \immediate\openout\split at calcout=#1.tex
+  \ifsplit at standalone
+  \pgfmathparse{\split at margin*.95}\edef\tmp{\pgfmathresult}
+  \split at w{\@percentchar\@percentchar\space These are made with
+    `calcsplit' with `-jobname \jobname'}
+  \split at w{
+    ^^J\string\documentclass[twoside]{article}
+    ^^J\string\usepackage{geometry}
+    ^^J\string\geometry{papersize={\the\paperwidth,\the\paperheight},margin=\tmp cm}
+    ^^J\string\usepackage{wargame}
+    ^^J\string\setlength{\string\parindent}{0pt}
+    ^^J\string\setlength{\string\parskip}{0pt}
+    ^^J\string\begin{document}
+    ^^J\string\ignorespaces\@percentchar}
+  \fi
+  \split at w{\string\def\string\boardfile{\split at img}\@percentchar}
+  \split at w{\string\def\string\boardscale{\split at scale}\@percentchar}
+}
+\def\split at footer{%
+  \ifsplit at standalone
+  \split at w{^^J\string\end{document}}
+  \fi
+  \split at w{^^J\@percentchar\@percentchar End of `\jobname'^^J}
+  \immediate\closeout\split at calcout
+}
+\def\split at init#1{%
+  \node[scale=\split at scale,
+  inner sep=0pt,
+  outer sep=0pt,
+  anchor=north west,
+  transform shape](b){\includegraphics{#1}};
+  \split at getboard{b}
+  %x
+  \split at tmp=\split at blrx cm\advance\split at tmp by -\split at bulx%
+  \wg at pt@to at cm{\split at tmp}\edef\split at bw{\pgfmathresult}%
+  \pgfmathparse{abs(\split at bw)}\edef\split at bw{\pgfmathresult}%
+  %
+  \split at tmp=\split at buly cm\advance\split at tmp by -\split at blry%
+  \wg at pt@to at cm{\split at tmp}\edef\split at bh{\pgfmathresult}%
+  \pgfmathparse{abs(\split at bh)}\edef\split at bh{\pgfmathresult}%
+  %
+  \wg at pt@to at cm{\paperwidth}\edef\split at pw{\pgfmathresult}%
+  \wg at pt@to at cm{\paperheight}\edef\split at ph{\pgfmathresult}%
+  %
+  \wg at pt@to at cm{\textwidth}\edef\split at ew{\pgfmathresult}%
+  \wg at pt@to at cm{\textheight}\edef\split at eh{\pgfmathresult}%
+  %
+  \hex at dbg{1}{Board:
+    (\split at bulx,\split at buly)(\split at blrx,\split at blry) \split at bw x\split at bh
+    ^^JPaper: \split at pw x\split at ph
+    ^^JEffective: \split at ew x\split at eh
+  }
+  \tikzset{
+    split/paper size/.style={
+      shape=rectangle,
+      minimum width=\paperwidth,
+      minimum height=\paperheight,
+      split/paper outline,
+    },
+    split/effective size/.style={
+      shape=rectangle,
+      minimum width=\textwidth,
+      minimum height=\textheight,
+      split/effective outline},
+    split/board size/.style={
+      shape=rectangle,
+      minimum width=\split at bw cm,
+      minimum height=\split at bh cm,
+      split/board outline}}
+  \node[board/.try,split/board size,anchor=north west] {};
+}
+\def\split at text@dim#1{%
+  \textwidth=\paperwidth%
+  \textheight=\paperheight%
+  \advance\textwidth by -#1cm%
+  \advance\textwidth by -#1cm%
+  \advance\textheight by -#1cm%
+  \advance\textheight by -#1cm%
+  \global\textwidth=\textwidth%
+  \global\textheight=\textheight%
+}
+\newif\ifsplit at standalone\split at standalonetrue
+\tikzset{%
+  split/.search also={/tikz},%
+  split/.cd,%
+  margin/.store in=\split at margin,
+  paper/.is choice,%
+  paper/a4/.code={%
+    \hex at dbg{3}{A4 paper for split}%
+    \global\paperwidth=21cm%
+    \global\paperheight=29.7cm%
+    \split at text@dim{\split at margin}},
+  paper/a3/.code={%
+    \hex at dbg{3}{A3 paper for split}%
+    \global\paperheight=42cm%
+    \global\paperwidth=29.7cm%
+    \split at text@dim{\split at margin}},
+  paper/letter/.code={%
+    \hex at dbg{3}{Letter paper for split}
+    \paperheight=27.9cm,%
+    \paperwidth=21.6cm,%
+    \split at text@dim{\split at margin}},%
+  paper/tabloid/.code={%
+    \hex at dbg{3}{Tabloid paper for split}%
+    \paperheight=43.2cm,%
+    \paperwidth=27.9cm,%
+    \split at text@dim{\split at margin}},
+  landscape/.code={%
+    \hex at dbg{3}{Landscape option for split}
+    \split at tmp=\paperheight
+    \global\paperheight=\paperwidth
+    \global\paperwidth=\split at tmp
+    \split at tmp=\textheight
+    \global\textheight=\textwidth
+    \global\textwidth=\split at tmp},
+  standalone/.is if=split at standalone,
+  scale/.store in=\split at scale,
+  output/.store in=\split at out,
+  ncol/.store in=\split at ncol,
+  nrow/.store in=\split at nrow,
+  overlap/.store in=\split at ov, % Centimeter, no unit
+  image/.store in=\split at img,
+  paper/.default=a4,  paper/.initial=a4,
+  margin/.default=.6, margin/.initial=.6,
+  ncol/.default=0,    ncol/.initial=0,
+  nrow/.default=0,    nrow/.initial=0,
+  overlap/.default=2, overlap/.initial=2,
+  image/.default=board, image/.initial=board,
+  output/.default=\jobname_out,
+  standalone/.default=true,
+  scale/.default=1,
+}
+\def\splitboard#1{%
+  \pgfkeys{/tikz/split/.cd,%
+    standalone,%
+    output,%
+    margin,%
+    paper,%
+    image,%
+    overlap,%
+    scale,%
+    ncol,%
+    nrow,%
+    #1}
+  \hex at dbg{1}{%
+       Paper:     `\the\paperwidth'x`\the\paperheight'
+    ^^JEffective: `\the\textwidth'x`\the\textheight'
+    ^^JNcols:     `\split at ncol'
+    ^^JNrows:     `\split at nrow'
+    ^^JOverlap:   `\split at ov' cm}
+  \split at header{\split at out}
+  \begin{tikzpicture}
+    \split at init{\split at img}
+    \split at getinit{%
+      \split at nrow}{%
+      \split at ncol}{%
+      \split at ov}{\split at eh}{\split at ew}{\split at bh}{\split at bw}
+    \node[split/effective size,
+    above left=\dy and \dx of b.north west,
+    anchor=north west] (c1r1) {};
+    \node[split/paper size] at (c1r1) {};
+    %
+    \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split at nrow}{%
+      \ifnum\r>1
+        \hex at dbg{3}{Placing first column of row `\r'}
+        \node[split/effective size,
+        below=-\split at ov cm of c1r\pr.south west,anchor=north west] (c1r\r){};
+        \node[split/paper size] at (c1r\r) {};
+      \fi
+      \foreach \c [remember=\c as \pc (initially 1)] in {2,...,\split at ncol}{%
+        \ifnum\c>\split at ncol\else%
+          \ifnum\c>\pc
+          \hex at dbg{3}{Placing column `\c' (`\pc') of row `\r'}
+            \node[split/effective size,
+            right=-\split at ov cm of c\pc r\r.north east,anchor=north west]
+            (c\c r\r) {};
+            \node[split/paper size] at (c\c r\r) {};
+          \fi
+        \fi
+      }
+    }
+    \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split at nrow}{%
+      \foreach \c [remember=\c as \pc (initially 0)] in {1,...,\split at ncol}{%
+        \split at getcoords{\c}{\r}{\split at ov cm}}}
+  \end{tikzpicture}
+  \split at footer
+}
+\def\segment(#1)(#2)#3#4#5#6{%
+  \begin{tikzpicture}%
+    \begin{scope}
+      \clip (#1) rectangle (#2);
+      \node[scale=\boardscale,
+      inner sep=0pt,
+      outer sep=0pt,
+      anchor=north west,
+      transform shape]{\includegraphics{\boardfile}};
+    \end{scope}
+    \pgfinterruptboundingbox
+    \draw(#3,#6)--++( 0.0, 0.3);
+    \draw(#3,#6)--++(-0.3, 0.0);
+    \draw(#3,#5)--++( 0.0,-0.3);
+    \draw(#3,#5)--++(-0.3, 0.0);
+    \draw(#4,#6)--++( 0.0, 0.3);
+    \draw(#4,#6)--++( 0.3, 0.0);
+    \draw(#4,#5)--++( 0.0,-0.3);
+    \draw(#4,#5)--++( 0.3, 0.0);
+    \endpgfinterruptboundingbox
+  \end{tikzpicture}%
+  \cleardoublepage}%
 %% Local Variables:
 %%   mode: LaTeX
 %% End:

Modified: trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.natoapp6c.code.tex
===================================================================
--- trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.natoapp6c.code.tex	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.natoapp6c.code.tex	2023-03-30 20:06:53 UTC (rev 66713)
@@ -463,6 +463,27 @@
     \pgfusepath{stroke}%
   }
 }
+\pgfdeclareshape{natoapp6c friendly none}{%
+  \inheritsavedanchors[from=natoapp6c base]
+  \savedanchor\northeast{%
+    \pgf at x=1.5\n at to@pp at r%
+    \pgf at y=\n at to@pp at r}
+  \anchor{north east}{\northeast}
+  \anchor{north west}{\northeast\pgf at x=-\pgf at x}
+  \anchor{south east}{\northeast\pgf at y=-\pgf at y}
+  \anchor{south west}{\northeast\pgf at x=-\pgf at x\pgf at y=-\pgf at y}
+  \anchor{north}{\northeast\pgf at x=0cm}
+  \anchor{south}{\northeast\pgf at x=0cm\pgf at y=-\pgf at y}
+  \anchor{east}{\northeast\pgf at y=0cm}
+  \anchor{west}{\northeast\pgf at x=-\pgf at x\pgf at y=0cm}
+  \inheritanchor[from=natoapp6c base]{upper}
+  \inheritanchor[from=natoapp6c base]{lower}
+  \inheritanchor[from=natoapp6c base]{left}
+  \inheritanchor[from=natoapp6c base]{right}
+  \inheritanchor[from=natoapp6c base]{center}
+  \backgroundpath{}
+  \behindforegroundpath{}
+}
 \def\n at to@hostile@@ir{%
   \southeast \wg at tmpa=\pgf at x\wg at tmpb=\pgf at y%
   \cntrl \wg at tmpc=\pgf at y%
@@ -1581,6 +1602,7 @@
   command/space/.style={cmd=space},
   command/sub surface/.style={cmd=sub surface},
   command/sea mine/.style={cmd=sub surface},
+  command/none/.style={cmd=none},
 }
 \tikzset{
   /natoapp6c/.cd,
@@ -1617,7 +1639,11 @@
   decoy/.is if=natoapp at decoy,%
 }
 \tikzset{
-  natoapp6c/parts/.style={draw,shape=rectangle,transform shape},
+  natoapp6c/parts/.style={
+    scale line widths,
+    draw,
+    shape=rectangle,
+    transform shape},
   natoapp6c/main/.style={natoapp6c/parts},
   natoapp6c/modifiers/.style={natoapp6c/parts,scale=.6},
   natoapp6c/lower/.style={natoapp6c/parts},
@@ -1832,8 +1858,15 @@
   \node[draw,transform shape,natoapp6c={#1}] (#3) at (#2) {};%
   \@ifnextchar;{\@gobble}{}}
 \providecommand\natoappmark[2][]{%
-  \tikz[scale=.25,#1]{\natoapp[faction=friendly,command=land,main=#2]}}
-\providecommand\echelonmark[2][]{\tikz[scale=.5,#1]{%
+  \tikz[transform shape,
+  scale=.25,
+  baseline=(natoapp6c mark.south east),
+  natoapp6c mark/.try,
+  #1]{%1
+    \node[draw,transform
+    shape,natoapp6c={faction=friendly,command=land,
+      main=#2}] (natoapp6c mark){}}}
+\providecommand\echelonmark[2][]{\tikz[transform shape,scale=.5,#1]{%
     \pic[scale line widths,line width=1pt] {natoapp6c/s/echelon=#2};}}
 \DeclareRobustCommand\armouredmark[1][]{\natoappmark[#1]{armoured}}
 \DeclareRobustCommand\infantrymark[1][]{\natoappmark[#1]{infantry}}

Modified: trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.util.code.tex
===================================================================
--- trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.util.code.tex	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/tex/latex/wargame/tikzlibrarywargame.util.code.tex	2023-03-30 20:06:53 UTC (rev 66713)
@@ -5,6 +5,12 @@
 %% The original source files were:
 %%
 %% util/core.dtx  (with options: `utils')
+%% util/misc.dtx  (with options: `utils')
+%% util/compound.dtx  (with options: `utils')
+%% util/bb.dtx  (with options: `utils')
+%% util/tikz.dtx  (with options: `utils')
+%% util/randomid.dtx  (with options: `utils')
+%% util/icons.dtx  (with options: `utils')
 %% 
 %% Copyright (C) 2019 Christian Holm.
 %% 
@@ -31,6 +37,53 @@
 %% 
 %% See the file wargame.dtx for further comments and documentation.
 %% 
+
+\tikzset{
+  wargame logo text/.style={
+    font=\sffamily\bfseries\fontsize{12}{14}\selectfont,
+    scale=2.8,
+    inner sep=0,
+    text width=1.8cm,
+    transform shape,
+    align=center},
+  wargame logo text content/.store in=\wg at logo@text at content,
+  wargame logo text content={{\huge\LaTeX} wargame},
+  wargame logo chit/.style={
+    chit={symbol={[
+        faction=friendly,
+        command=land,
+        echelon=division,
+        main=infantry]},
+      factors={chit/2 factors={4,3}},
+      left={chit/identifier=III},
+      right={chit/small identifier={10\textsuperscript{th}}},
+      color=white,
+      fill=red!50!black
+    }
+  },
+  wargame logo/.style={
+    transform shape,
+    every hex/.style={fill=gray!5!white,draw=gray!75!black},
+    hex/first row is=0,
+    hex/first column is=0,
+    hex/short top columns=none,
+    hex/short bottom columns=none,
+    hex/row direction is=normal,
+    hex/column direction is=normal
+  }
+}
+\newcommand\wargamelogo[1][]{%
+  \begin{scope}[wargame logo,#1]
+    \node[hex={fill=gray!30!white}] (logo center)      at (hex cs:c=0,r=0) {};
+    \node[hex={terrain=light woods}](logo light woods) at (hex cs:c=0,r=1) {};
+    \node[hex={terrain=city}]       (logo city)        at (hex cs:c=0,r=-1){};
+    \node[hex={terrain=woods}]      (logo woods)       at (hex cs:c=-1,r=0){};
+    \node[hex={terrain=mountains}]  (logo mountains)   at (hex cs:c=-1,r=1){};
+    \node[hex={terrain=beach}]      (logo beach)       at (hex cs:c=1,r=1) {};
+    \node[hex={terrain=swamp}]      (logo swamp)       at (hex cs:c=1,r=0) {};
+    \node[wargame logo chit]        (logo chit)        at (hex cs:)        {};
+    \node[wargame logo text]        (logo text) {\wg at logo@text at content};
+  \end{scope}}
 \newcount\wargamedbglvl\wargamedbglvl=0
 \def\wg at dbg#1#2{%
   \ifnum#1>\wargamedbglvl\relax\else\message{^^J#2}\fi}
@@ -181,6 +234,40 @@
   \gdef\pgf at sh@pi at M{\pgfpictureid}
   \global\let\pgf at protocolsizes\old at pgf@protocolsize
 }
+\def\wg at pt@to at cm#1{\pgfmathparse{#1 * 0.0351367}}
+\def\ptpoint at to@cm#1#2{%
+  \wg at pt@to at cm{#1}\edef\x{\pgfmathresult}%
+  \wg at pt@to at cm{#2}\edef\y{\pgfmathresult}}
+\def\wg at get@nchor#1#2{%
+  \wg at dbg{2}{Get anchor coordinates #1.#2}
+  \pgfpointanchor{#1}{#2}%
+  \wg at dbg{2}{ `\the\pgf at x',`\the\pgf at y'}
+  \pgfgetlastxy\tmp at x\tmp at y%
+  \wg at dbg{2}{ `\tmp at x',`\tmp at y'}
+  \wg at pt@to at cm{\tmp at x}\edef\tmp at x{\pgfmathresult}
+  \wg at pt@to at cm{\tmp at y}\edef\tmp at y{\pgfmathresult}
+}
+\def\wg at get@global at nchor#1#2{%
+  \pgfpointanchor{#1}{#2}%
+  \pgfgetlastxy\tmp at x\tmp at y%
+  \pgfpointtransformed{\pgfpoint{\tmp at x}{\tmp at y}}
+  \pgf at xa=\pgf at x
+  \pgf at ya=\pgf at y
+  %% \message{^^JAnchor #1.#2 @ (\the\pgf at xa,\the\pgf at ya)}
+  \wg at pt@to at cm{\the\pgf at xa}\edef\tmp at x{\pgfmathresult}
+  \wg at pt@to at cm{\the\pgf at ya}\edef\tmp at y{\pgfmathresult}
+}
+\def\wg at get@bb#1{%
+  \wg at get@nchor{#1}{south west}
+  \edef\llx{\tmp at x}
+  \edef\lly{\tmp at y}
+  \wg at get@nchor{#1}{north east}
+  \edef\urx{\tmp at x}
+  \edef\ury{\tmp at y}
+}
+\def\wglogbb#1{%
+  \wg at get@bb{#1}%
+  \message{^^J`#1' BB: (\llx,\lly) x (\urx,\ury)^^J}}
 \tikzstyle{reverseclip}=[insert path={(current bounding box.north east) --
   (current bounding box.south east) --
   (current bounding box.south west) --
@@ -205,25 +292,56 @@
   save clip/.default={true},
   save clip/.initial={false},
 }
+\def\wg at setcornersarched#1{%
+  \ifx|#1|\else%
+  \edef\pgf at corner@arc{{#1}{#1}}%
+  \pgf at arccornerstrue%
+  \ifdim#1=0pt%
+    \pgf at arccornersfalse%
+    \fi\fi}
+\newdimen\wg at lw@scaled\wg at lw@scaled=1pt
+\def\wg at getscale{%
+  \pgfgettransformentries{%
+    \wg at jaca}{%
+    \wg at jacb}{%
+    \wg at jacc}{%
+    \wg at jacd}{%
+    \wg at tmp}{%
+    \wg at tmp}%
+  \pgfmathsetmacro{\wg at jac}{sqrt(abs(\wg at jaca*\wg at jacd-\wg at jacb*\wg at jacc))}%
+  \wg at dbg{4}{Scale is \wg at jac}
+  \xdef\wg at scale{\wg at jac}}
+\def\wg at scaled#1{%
+  \wg at getscale%
+  \wg at dbg{4}{Scaling #1 by \wg at scale}
+  \pgfmathsetmacro{\wg at tmp}{\wg at scale*#1}%
+  \xdef\wg at tmp{\wg at tmp}%
+  \xdef\wg at lw@scale{\wg at tmp}%
+  \wg at dbg{4}{Scaled #1 -> \wg at tmp}}
+%% \message{^^JRounded corners: \meaning\pgfsetcornersarced}
 \tikzset{
+  %% Get current scale and store in \wg at scale
+  get scale/.code={\wg at getscale},
   scale line widths/.style={%
     /utils/exec=\def\tikz at semiaddlinewidth##1{%
-      \pgfgettransformentries{%
-        \wg at jaca}{%
-        \wg at jacb}{%
-        \wg at jacc}{%
-        \wg at jacd}{%
-        \wg at tmp}{%
-        \wg at tmp}%
-      \pgfmathsetmacro{\wg at jac}{sqrt(abs(\wg at jaca*\wg at jacd-\wg at jacb*\wg at jacc))}%
-      \wg at dbg{4}{Scaling line width ##1 by \wg at jac}
-      \pgfmathsetmacro{\wg at lw}{\wg at jac*##1}%
-      \wg at dbg{4}{Scaled ##1 -> \wg at lw}
-      \tikz at addoption{\pgfsetlinewidth{\wg at lw pt}}%
-      \wg at dbg{4}{Added scaled option \wg at lw}
-      \pgfmathsetlength\pgflinewidth{\wg at lw pt}
-      \wg at dbg{4}{Did set line width \wg at lw pt}
-    }},
+      \wg at scaled{##1}
+      \wg at lw@scaled=\wg at tmp pt
+      \tikz at addoption{\pgfsetlinewidth{\wg at lw@scaled}}%
+      \wg at dbg{4}{Added scaled option \wg at tmp}
+      \pgfmathsetlength\pgflinewidth{\wg at tmp pt}
+      \wg at dbg{4}{Did set line width \wg at tmp pt}
+    }
+  },
+  scale rounded corners/.style={%
+    /utils/exec=\def\pgfsetcornersarced##1{%
+      \pgf at process{##1}%
+      \pgf at xa=\pgf at x%
+      \wg at scaled{\the\pgf at xa}%
+      % \tikz at addoption{\wg at setcornersarched{\wg at tmp pt}}%
+      \wg at dbg{4}{Scaled rounded corners: \the\pgf at xa -> \wg at tmp}%
+      \wg at setcornersarched{\wg at tmp pt}%
+    }
+  },
   relative line width/.style={%
     /utils/exec=\def\tikz at semiaddlinewidth##1{%
       \wg at dbg{4}{Relative line width #1 times ##1}%
@@ -262,6 +380,84 @@
   \foreach \i in {1,...,8}{%
     \pgfmathparse{Hex(random(0,15))}
     \xdef\wg at uuid{\wg at uuid\pgfmathresult}}}
+\tikzset{
+  trash can line/.style={scale line widths,scale rounded corners,
+    line width=.5mm,->},
+}
+\tikzset{
+  trash can body/.pic={%
+    \path[fill=black,scale line widths,scale rounded corners,
+    rounded corners=.05cm]
+    (-.3,.2) --++(.6,0) --++(-.1,-.7) --++(-.4,0) --cycle;
+    \path[fill=white]
+    (-.025,-.4) arc(180:360:.025) --++(   0,.5) arc(0:180:.025)   --cycle;
+    \path[fill=white]
+    (-.125,-.4) arc(180:360:.025) --++(-.07,.5) arc(0:180:.025) --cycle;
+    \path[fill=white]
+    ( .075,-.4) arc(180:360:.025) --++( .07,.5) arc(0:180:.025) --cycle;
+  },
+  trash can lid/.pic={%
+    \path[fill=black,scale line widths,scale rounded corners,
+    rounded corners=.05cm]
+    (-.35,.23)--++(.7,0)--++(-.07,.07)--++(-.56,0)--cycle;
+    \path[fill=black]
+    (-.15,.3) --++(.05,0) --++(0,.05) --++(.2,0) --++(0,-.05)
+    --++(.05,0) --++(0,.05) arc(0:90:.05) --++(-.2,0) arc(90:180:.05)
+    --cycle;
+  },
+}
+\tikzset{
+  trash can/.pic={
+    \pic{trash can body};
+    \pic{trash can lid};
+  },
+  trash can open/.pic={
+    \pic{trash can body};
+    \pic[rotate=-30] at (0,.1) {trash can lid};
+  },
+}
+\tikzset{
+  eliminate icon/.pic={
+    \pic{trash can open};
+    \draw[trash can line,color=red!50!black]
+    (-.5,.2) to[looseness=1.5] (-.1,.23);
+  },
+  restore icon/.pic={
+    \pic{trash can open};
+    \draw[trash can line,<-,color=green!50!black]
+    (-.5,.2) to[looseness=1.5] (-.1,.23);
+  },
+  pool icon/.pic={
+    \pic{trash can};
+  },
+}
+\tikzset{
+  flip icon/.pic={
+    \draw[scale line widths,scale rounded corners,
+    line width=1mm,->,color=blue!50!black]
+    (-.5,-.5) arc(180:0:.5);% (.5,-.5);
+  },
+  pics/oob icon/.style n args={2}{code={%
+      \begin{scope}[box/.style args={##1,##2,##3,##4}{
+          minimum width=##1cm,
+          minimum height=##2cm,
+          fill=##3,
+          anchor=##4,
+          draw=gray!50!black,
+          scale line widths,
+          line width=.5pt,
+          transform shape},
+        under/.style={
+          below=.05cm of ##1}
+        ]
+        \node[box={.5,.2,#1,north west,fill=#1}] (r1) at (.05,.45) {};
+        \node[under=r1.south west,box={.3,.25,#1,north west}] (r2) {};
+        \node[under=r2.south west,box={.2,.3, #1,north west}] (r3) {};
+        \node[box={.2,.4,#2,north east}] (l1) at (-.05,.45) {};
+      \end{scope}
+    }
+  }
+}
 %% Local Variables:
 %%   mode: LaTeX
 %% End:

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.beach.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.city.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.light_woods.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.mountains.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.rough.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.swamp.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.town.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.village.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wargame.woods.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wgexport.cls
===================================================================
--- trunk/Master/texmf-dist/tex/latex/wargame/wgexport.cls	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/tex/latex/wargame/wgexport.cls	2023-03-30 20:06:53 UTC (rev 66713)
@@ -4,7 +4,7 @@
 %%
 %% The original source files were:
 %%
-%% util/core.dtx  (with options: `exportcls')
+%% util/export.dtx  (with options: `exportcls')
 %% 
 %% Copyright (C) 2019 Christian Holm.
 %% 
@@ -82,32 +82,36 @@
   \let\natoapp at report\do at natoapp@report%
   \chit at dbg{2}{chits to make images of `#2'}%
   \foreach[count=\ti from 0] \t/\x in #2{%
+    \chit at dbg{2}{^^JRow: `\t' (`\x')}
     \ifx\t\empty\else% Ignore empty rows
       \chit at dbg{5}{^^JSubcategory: `\x' (default `#1')}
-      \ifx\t\x\def\x{#1}\fi% Take sub-category or default
+      % Take sub-category or default
+      \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi
       \foreach \u/\m in \t{%
         \ifx\u\empty\else% Ignore empty cells
-          \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
-          \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
-          \ifx\u\m\def\m{1}\fi% If the same as unit
-          \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
-          %% We only make one copy of the chit, since we can duplicate
-          %% it in VASSAL
-          \info*{\u}{counter}{\x}
-          \begin{tikzpicture}
-            \chit[\u=\ti]%
-          \end{tikzpicture}
-          \end at info%
-          %% \foreach \n in {1,...,\m}{% Make a number of copies
-          %%   \ifx\u\chit at blank%
-          %%     \chit at dbg{3}{Ignoring blank chit:\u}%
-          %%   \else%
-          %%     \info{\u}{counter}{#2}
-          %%     \begin{tikzpicture}
-          %%       \chit[\u=\ti](\c,\r)%
-          %%     \end{tikzpicture}
-          %%   \fi%
-          %% }%
+          \ifx\u\chit at blank\else%
+            \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+            \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
+            \ifx\u\m\def\m{1}\fi% If the same as unit
+            \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
+            %% We only make one copy of the chit, since we can duplicate
+            %% it in VASSAL
+            \info*{\u}{counter}{\x}
+            \begin{tikzpicture}
+              \chit[\u=\ti]%
+            \end{tikzpicture}
+            \end at info%
+            %% \foreach \n in {1,...,\m}{% Make a number of copies
+            %%   \ifx\u\chit at blank%
+            %%     \chit at dbg{3}{Ignoring blank chit:\u}%
+            %%   \else%
+            %%     \info{\u}{counter}{#2}
+            %%     \begin{tikzpicture}
+            %%       \chit[\u=\ti](\c,\r)%
+            %%     \end{tikzpicture}
+            %%   \fi%
+            %% }%
+          \fi%
         \fi%
       }%
       \chit at dbg{2}{End of inner loop}%
@@ -123,37 +127,40 @@
   \foreach[count=\ti from 0] \t/\x in #2{%
     \ifx\t\empty\else% Ignore empty rows
       \chit at dbg{5}{^^JSubcategory: `\x' (default `#1')}
-      \ifx\t\x\def\x{#1}\fi% Take sub-category or default
+      % Take sub-category or default
+      \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi
       \foreach \u/\m in \t{%
         \ifx\u\empty\else% Ignore empty cells
-          \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
-          \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
-          \ifx\u\m\def\m{1}\fi% If the same as unit
-          \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
-          %% Flipped chit
-          \edef\s{\u\space flipped}%
-          %% We only make one copy of the chit, since we can duplicate
-          %% it in VASSAL
-          \info*{\u}{counter}{\x}%
-          \begin{tikzpicture}%
-            \chit[\u=\ti]%
-          \end{tikzpicture}%
-          \end at info%
-          \info*{\s}{counter}{\x}%
-          \begin{tikzpicture}%
-            \chit[\s=\ti]%
-          \end{tikzpicture}%
-          \end at info%
-          %% \foreach \n in {1,...,\m}{% Make a number of copies
-          %%   \ifx\u\chit at blank%
-          %%     \chit at dbg{3}{Ignoring blank chit:\u}%
-          %%   \else%
-          %%     \info{\u}{counter}{#2}
-          %%     \begin{tikzpicture}
-          %%       \chit[\u=\ti](\c,\r)%
-          %%     \end{tikzpicture}
-          %%   \fi%
-          %% }%
+          \ifx\u\chit at blank\else%
+            \chit at dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+            \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
+            \ifx\u\m\def\m{1}\fi% If the same as unit
+            \chit at dbg{2}{Next chit `\u' multiplicity `\m'}%
+            %% Flipped chit
+            \edef\s{\u\space flipped}%
+            %% We only make one copy of the chit, since we can duplicate
+            %% it in VASSAL
+            \info*{\u}{counter}{\x}%
+            \begin{tikzpicture}%
+              \chit[\u=\ti]%
+            \end{tikzpicture}%
+            \end at info%
+            \info*{\s}{counter}{\x}%
+            \begin{tikzpicture}%
+              \chit[\s=\ti]%
+            \end{tikzpicture}%
+            \end at info%
+            %% \foreach \n in {1,...,\m}{% Make a number of copies
+            %%   \ifx\u\chit at blank%
+            %%     \chit at dbg{3}{Ignoring blank chit:\u}%
+            %%   \else%
+            %%     \info{\u}{counter}{#2}
+            %%     \begin{tikzpicture}
+            %%       \chit[\u=\ti](\c,\r)%
+            %%     \end{tikzpicture}
+            %%   \fi%
+            %% }%
+          \fi%
         \fi%
       }%
     \fi%
@@ -160,45 +167,10 @@
   }%
   \endgroup%
 }
-\def\pt at to@cm#1{\pgfmathparse{#1 * 0.0351367}}
-\def\ptpoint at to@cm#1#2{%
-  \pt at to@cm{#1}\edef\x{\pgfmathresult}%
-  \pt at to@cm{#2}\edef\y{\pgfmathresult}}
-\def\mk at get@anchor#1#2{%
-  \pgfpointanchor{#1}{#2}%
-  \pgfgetlastxy\tmp at x\tmp at y%
-  \pt at to@cm{\tmp at x}\edef\tmp at x{\pgfmathresult}
-  \pt at to@cm{\tmp at y}\edef\tmp at y{\pgfmathresult}
-}
-\def\mk at get@global at anchor#1#2{%
-  \pgfpointanchor{#1}{#2}%
-  \pgfgetlastxy\tmp at x\tmp at y%
-  \pgfpointtransformed{\pgfpoint{\tmp at x}{\tmp at y}}
-  \pgf at xa=\pgf at x
-  \pgf at ya=\pgf at y
-  \pt at to@cm{\the\pgf at xa}\edef\tmp at x{\pgfmathresult}
-  \pt at to@cm{\the\pgf at ya}\edef\tmp at y{\pgfmathresult}
-}
-\def\get at bb#1{%
-  % \pgfpointanchor{#1}{south west}%
-  % \pgfgetlastxy\tmp at llx\tmp at lly%
-  % \pgfpointanchor{#1}{north east}%
-  % \pgfgetlastxy\tmp at urx\tmp at ury%
-  % \pt at to@cm{\tmp at llx}\edef\llx{\pgfmathresult}
-  % \pt at to@cm{\tmp at lly}\edef\lly{\pgfmathresult}
-  % \pt at to@cm{\tmp at urx}\edef\urx{\pgfmathresult}
-  % \pt at to@cm{\tmp at ury}\edef\ury{\pgfmathresult}
-  \mk at get@anchor{#1}{south west}
-  \edef\llx{\tmp at x}
-  \edef\lly{\tmp at y}
-  \mk at get@anchor{#1}{north east}
-  \edef\urx{\tmp at x}
-  \edef\ury{\tmp at y}
-}
 \def\mk at transform{%
   \pgfgettransformentries{\mxx}{\mxy}{\myx}{\myy}{\ptdx}{\ptdy}
-  \pt at to@cm{\ptdx}\edef\dx{\pgfmathresult}
-  \pt at to@cm{\ptdy}\edef\dy{\pgfmathresult}
+  \wg at pt@to at cm{\ptdx}\edef\dx{\pgfmathresult}
+  \wg at pt@to at cm{\ptdy}\edef\dy{\pgfmathresult}
   \mk at w{ \mk at i "xx": \mxx,}
   \mk at w{ \mk at i "xy": \mxy,}
   \mk at w{ \mk at i "yx": \myx,}
@@ -207,38 +179,21 @@
   \mk at w{ \mk at i "dy": \dy,}
 }
 \def\mk at bb#1{%
-  \get at bb{#1}
+  \wg at get@bb{#1}
   \mk at w{ \mk at i "lower left":  [\llx,\lly],}
   \mk at w{ \mk at i "upper right": [\urx,\ury],}
   \begingroup
-   % \pgftransforminvert
-   % \pgfpointanchor{#1}{south west}%
-   % \pgfgetlastxy\tmp at llx\tmp at lly%
-   % \pgfpointtransformed{\pgfpoint{\tmp at llx}{\tmp at lly}}
-   % \pgf at xa=\pgf at x
-   % \pgf at ya=\pgf at y
-   % %
-   % \pgfpointanchor{#1}{north east}%
-   % \pgfgetlastxy\tmp at urx\tmp at ury%
-   % \pgfgetlastxy\tmp at llx\tmp at lly%
-   % \pgfpointtransformed{\pgfpoint{\tmp at urx}{\tmp at ury}}
-   % \pgf at xb=\pgf at x
-   % \pgf at yb=\pgf at y
-   % \pt at to@cm{\the\pgf at xa}\edef\llx{\pgfmathresult}
-   % \pt at to@cm{\the\pgf at ya}\edef\lly{\pgfmathresult}
-   % \pt at to@cm{\the\pgf at xb}\edef\urx{\pgfmathresult}
-   % \pt at to@cm{\the\pgf at yb}\edef\ury{\pgfmathresult}x
-   \mk at get@global at anchor{#1}{south west}
+   \wg at get@global at nchor{#1}{south west}
    \mk at w{ \mk at i "global lower left":  [\tmp at x,\tmp at y],}
-   \mk at get@global at anchor{#1}{north east}
+   \wg at get@global at nchor{#1}{north east}
    \mk at w{ \mk at i "global upper right": [\tmp at x,\tmp at y]}
  \endgroup
 }
 \def\mk at pos#1(#2){%
-  \hex at dbg{10}{^^JMarking `#2' with `#1' - start}
+  \wg at dbg{10}{^^JMarking `#2' with `#1' - start}
   \coordinate[transform shape] (tmp) at (#2) {};
-  \mk at get@anchor{tmp}{center}
-  \hex at dbg{3}{^^JMarking `#2' with `#1' - `\tmp at x',\tmp at y'}
+  \wg at get@nchor{tmp}{center}
+  \wg at dbg{3}{^^JMarking `#2' with `#1' - `\tmp at x',\tmp at y'}
   \tikzset{zone point={#1}{\tmp at x}{\tmp at y}}
 }
 \pgfdeclaredecoration{record path construction}{initial}{%
@@ -274,8 +229,13 @@
   \let\markpos\mk at pos%
   \info{dummy}{<<dummy>>}{}%
   %\tikz{}%
-  \tikz{\scoped[every hex/.try,every hex node/.try]{%
-      \node[inner sep=0,outer sep=0]{%
+  \tikz{\scoped[%
+    every hex/.try,every hex node/.try,
+    ]{%
+      \def\hex at col{0}%
+      \def\hex at row{0}%
+      \node[hex,inner sep=0,outer sep=0]{%
+        \message{^^JHex label: `\meaning\hex at label'}%
         \global\let\mk at label\hex at label}}}%
   \info*{#2}{#1}{#3}%
   \mk at w{ \mk at i "zones": \@lbchar}%
@@ -282,6 +242,7 @@
   \edef\mk at i{\mk at i\space}
   %% Everything is made into centimeters
   \mk at w{ \mk at i "units": "cm",}
+  \hex at dbg{0}{Label: `\meaning\mk at label'}
   \@ifundefined{mk at label}{}{\mk at w{ \mk at i "labels": "\mk at label",}}
   %% Write out coordinate options as "coords" object
   \mk at w{ \mk at i"coords": \@lbchar}%
@@ -338,8 +299,8 @@
       % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
       \pgf at xa=\pgf at x
       \pgf at ya=\pgf at y
-      \pt at to@cm{\the\pgf at xa}\edef\px{\pgfmathresult}
-      \pt at to@cm{\the\pgf at ya}\edef\py{\pgfmathresult}
+      \wg at pt@to at cm{\the\pgf at xa}\edef\px{\pgfmathresult}
+      \wg at pt@to at cm{\the\pgf at ya}\edef\py{\pgfmathresult}
       \advance\mk at point1
       \global\mk at point=\mk at point
       \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1", "type": "point", "coords":  [\px,\py]
@@ -355,11 +316,12 @@
       % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
       \pgf at xa=\pgf at x
       \pgf at ya=\pgf at y
-      \pt at to@cm{\the\pgf at xa}\edef\px{\pgfmathresult}
-      \pt at to@cm{\the\pgf at ya}\edef\py{\pgfmathresult}
+      \wg at pt@to at cm{\the\pgf at xa}\edef\px{\pgfmathresult}
+      \wg at pt@to at cm{\the\pgf at ya}\edef\py{\pgfmathresult}
       \advance\mk at point1
       \global\mk at point=\mk at point
-      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1", "type": "point", "coords":  [\px,\py]
+      \mk at w{ \mk at i "point\the\mk at point": \@lbchar "name": "##1",
+        "parent": "\wg at export@box", "type": "point", "coords":  [\px,\py]
         \@rbchar, }
       %\message{^^JZone point \the\mk at point\space ##1: ##2,##3 -> \px,\py}
     },
@@ -383,22 +345,140 @@
   \mk at w{ \mk at i\@rbchar}%
   \end at info%
 }
+\def\wg at gennumberm@rkers#1#2#3{
+  \message{^^JNumbered markers: Type=`#1' Max=`#2' Category=`#3'}
+  \def\markers{}
+  \def\keys{}
+  \foreach \i in {1,...,#2}{%
+    \xdef\keys{/tikz/#1 \i/.style={/tikz/#1=\i},\keys}
+    \xdef\markers{\markers,#1 \i}}
+  {%
+    \nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}%
 \tikzset{
+  wg hidden unit/.pic={},
+  wg hidden unit/.style={
+    chit={
+      frame={draw=none,fill=none},
+      full=wg hidden unit}}}
+\DeclareRobustCommand\battlemarkers[2][BattleMarkers]{%
+  \wg at gennumberm@rkers{battle marker}{#2}{#1}%
+  \message{^^JMake a hidden unit and add to Markers category}
+  {%
+    \nopagecolor%
+    \chitimages[Markers]{{wg hidden unit}}%
+    %
+    \info{battle-marker-icon}{icon}{}%
+    \tikz[scale=.7,transform shape]{\pic{battle marker=0};}%
+    \info{clear-battles-icon}{icon}{}
+    \tikz[scale=.4,transform shape]{%
+      \pic{eliminate icon};
+      \pic[scale=.7,transform shape] at (-.3,0) {battle marker=0};}%
+  }%
+}
+\def\wg at gencolorm@rkers#1#2#3{%
+  \def\markers{}
+  \def\keys{}
+  \foreach \o/\f in {#2}{%
+    \ifx\o\f\def\f{white}\fi%
+    \message{^^JOdds marker `#1 \o' w/fill `\f'}%
+    \xdef\keys{/tikz/#1 \o/.style={/tikz/#1={\o,\f}},\keys}
+    \xdef\markers{\markers,#1 \o}}
+  {\nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}%
+\DeclareRobustCommand\oddsmarkers[2][OddsMarkers]{%
+  \wg at gencolorm@rkers{odds marker}{#2}{#1}%
+  \info{odds-battles-icon}{icon}{}
+  \tikz[scale=.5,transform shape]{\pic{odds marker={?:?,white}}}
+  \info{resolve-battles-icon}{icon}{}
+  \tikz[scale=.3,transform shape]{%
+    \pic{dice};
+    \pic[scale=1.2,transform shape] at (-.2,-.2) {battle marker=0};}%
+}
+\DeclareRobustCommand\resultmarkers[2][ResultMarkers]{%
+  \wg at gencolorm@rkers{result marker}{#2}{#1}}
+\DeclareRobustCommand\commonicons[2]{%
+  \begingroup%
+  \nopagecolor%
+  \tikzset{icon/.style={scale=.4,transform shape}}%
+  %
+  \info{pool-icon}{icon}{}
+  \tikz[icon]{\pic{pool icon};}
+  %
+  \info{oob-icon}{icon}{}%
+  \tikz[icon]{\pic{oob icon={#1}{#2}};}%
+  %
+  \info{flip-icon}{icon}{}%
+  \tikz[icon]{\pic{flip icon};}%
+  %
+  \info{eliminate-icon}{icon}{}%
+  \tikz[icon]{\pic{eliminate icon};}%
+  %
+  \info{restore-icon}{icon}{}%
+  \tikz[icon]{\pic{restore icon};}%
+  %
+  \info{dice-icon}{icon}{}%
+  \tikz[icon,scale=.9]{\pic{dice};}%
+  %
+  \info{unit-icon}{icon}{}%
+  \tikz[icon,scale=.7]{%
+    \chit[fill=#1,
+      symbol={[
+        scale line widths,
+        line width=1pt,
+        faction=friend,
+        command=land,
+        main=infantry,
+        scale=1.3](0,-.15)}]}%
+    \endgroup%
+}
+\def\dice{%
+  \@ifnextchar[{\wg at dice}{\wg at dice[]}%]
+}
+\def\wg at dice[#1]{%
+  \@ifnextchar[{\wg@@dice{#1}}{\wg@@dice{#1}[]} %]
+}
+\def\wg@@dice#1[#2]#3#4#5{%
+  \foreach \v/\p in {#5}{%
+    \info{#3-\v}{die-roll}{#3}
+    \tikz[#1]{
+      \node[shape=#4,transform shape,draw=none,fill=black,opacity=.5]
+      at (.05,-.03){};
+      \node[shape=#4,#2,transform shape]{\p};}}}
+\tikzset{
   zone turn/.store in=\zone at turn,
   zone mult/.store in=\zone at mult
 }
+\def\@chit at rep@line#1#2{%
+  \@ifundefined{#2}{}{
+    \edef\wg at chit@tmp{\csname #2\endcsname}
+    {\escapechar=`/
+      \xdef\tmp{\detokenize\expandafter{\wg at chit@tmp} \@empty}}
+    % \message{^^J\meaning\@tmp -> \meaning\tmp}
+    \mk at w{ \mk at i\space "#1": "\tmp",}}}
+
 \def\do at chit@report{%
+  \chit at dbg{3}{Start of Chit Report}
   \mk at w{ \mk at i "chit": \@lbchar}
+  \chit at dbg{3}{Report - ID}
   \@ifundefined{id}{}         {\mk at w{ \mk at i\space "id":      "\id", }}%
-  \@ifundefined{chit at symbol}{}     {\mk at w{ \mk at i\space "symbol":  "true", }}%
-  \@ifundefined{chit at full}{}       {\mk at w{ \mk at i\space "full":    "\chit at full", }}
-  \@ifundefined{chit at factors}{}    {\mk at w{ \mk at i\space "factors": "\chit at factors", }}%
-  \@ifundefined{chit at left}{}       {\mk at w{ \mk at i\space "left":    "\chit at left", }}%
-  \@ifundefined{chit at right}{}      {\mk at w{ \mk at i\space "right":   "\chit at right", }}%
-  \@ifundefined{chit at upper@left}{} {\mk at w{ \mk at i\space "upper left":  "\chit at upper@left", }}%
-  \@ifundefined{chit at lower@left}{} {\mk at w{ \mk at i\space "lower left":  "\chit at lower@left", }}%
-  \@ifundefined{chit at upper@right}{}{\mk at w{ \mk at i\space "upper right": "\chit at upper@right", }}%
-  \@ifundefined{chit at lower@right}{}{\mk at w{ \mk at i\space "lower right": "\chit at lower@right}", }%
+  \chit at dbg{3}{Report - Symbol: `\meaning\chit at symbol'}
+  \@ifundefined{chit at symbol}{}{\mk at w{ \mk at i\space "symbol":  "true", }}%
+  \chit at dbg{3}{Report - Full: `\meaning\chit at full'}
+  \@chit at rep@line{full}{chit at full}
+  \chit at dbg{3}{Report - Factors: `\meaning\chit at factors'}
+  \@chit at rep@line{factors}{chit at factors}%
+  \chit at dbg{3}{Report - Left: `\meaning\chit at left'}
+  \@chit at rep@line{left}{chit at left}%
+  \chit at dbg{3}{Report - Right: : `\meaning\chit at right'}
+  \@chit at rep@line{right}{chit at right}%
+  \chit at dbg{3}{Report - Upper left: `\meaning\chit at upper@left'}
+  \@chit at rep@line{upper left}{chit at upper@left}%
+  \chit at dbg{3}{Report - Lower left: `\meaning\chit at lower@left'}
+  \@chit at rep@line{lower left}{chit at lower@left}%
+  \chit at dbg{3}{Report - Upper right: `\meaning\chit at upper@right}
+  \@chit at rep@line{upper right}{chit at upper@right}%
+  \chit at dbg{3}{Report - Lower right: `\meaning\chit at lower@right'}
+  \@chit at rep@line{lower right}{chit at lower@right}%
+  \chit at dbg{3}{Report - End comma}
   \mk at w{ \mk at i\space "end": 0}
   \@ifundefined{chit at symbol}{
     \mk at w{ \mk at i \@rbchar }
@@ -405,19 +485,20 @@
   }{
     \mk at w{ \mk at i \@rbchar, }% NATOAPP6c will follow
   }%
+  \chit at dbg{3}{End of Chit Report}
 }
 \def\do at natoapp@report{%
   \mk at w{ \mk at i "natoapp6c": \@lbchar}
-  \@ifundefined{id}{}{\mk at w{ \mk at i\space "id": "\id", }}
-  \@ifundefined{natoapp at fac}{}{\mk at w{ \mk at i\space "faction": "\natoapp at fac", }}
-  \@ifundefined{natoapp at cmd}{}{\mk at w{ \mk at i\space "command": "\natoapp at cmd", }}
-  \@ifundefined{natoapp at ech}{}{\mk at w{ \mk at i\space "echelon": "\natoapp at ech", }}
-  \@ifundefined{natoapp at main}{}{\mk at w{ \mk at i\space "main": "\natoapp at main", }}
-  \@ifundefined{natoapp at left}{}{\mk at w{ \mk at i\space "left": "\natoapp at left", }}
-  \@ifundefined{natoapp at right}{}{\mk at w{ \mk at i\space "right": "\natoapp at right", }}
-  \@ifundefined{natoapp at upper}{}{\mk at w{ \mk at i\space "upper": "\natoapp at upper", }}
-  \@ifundefined{natoapp at lower}{}{\mk at w{ \mk at i\space "lower": "\natoapp at lower", }}
-  \@ifundefined{natoapp at below}{}{\mk at w{ \mk at i\space "below": "\natoapp at below", }}
+  \@chit at rep@line{id}{\id}
+  \@chit at rep@line{faction}{natoapp at fac}
+  \@chit at rep@line{command}{natoapp at cmd}
+  \@chit at rep@line{echelon}{natoapp at ech}
+  \@chit at rep@line{main}{natoapp at main}
+  \@chit at rep@line{left}{natoapp at left}
+  \@chit at rep@line{right}{natoapp at right}
+  \@chit at rep@line{upper}{natoapp at upper}
+  \@chit at rep@line{lower}{natoapp at lower}
+  \@chit at rep@line{below}{natoapp at below}
   \mk at w{ \mk at i\space "end": 0}
   \mk at w{ \mk at i \@rbchar}
 }

Modified: trunk/Master/texmf-dist/tex/latex/wargame/wgexport.py
===================================================================
--- trunk/Master/texmf-dist/tex/latex/wargame/wgexport.py	2023-03-30 20:06:04 UTC (rev 66712)
+++ trunk/Master/texmf-dist/tex/latex/wargame/wgexport.py	2023-03-30 20:06:53 UTC (rev 66713)
@@ -1,1885 +1,6192 @@
 #!/usr/bin/env python
+# Script collected from other scripts
 #
-from pprint import pprint
+#   ../vassal/vassal.py
+#   latexexporter.py
+#   main.py
+#
+# ====================================================================
+# From ../vassal/vassal.py
+# Script collected from other scripts
+#
+#   ../common/singleton.py
+#   ../common/verbose.py
+#   ../common/verboseguard.py
+#   base.py
+#   element.py
+#   globalkey.py
+#   gameelements.py
+#   mapelements.py
+#   globalproperty.py
+#   turn.py
+#   documentation.py
+#   player.py
+#   chessclock.py
+#   widget.py
+#   grid.py
+#   zone.py
+#   board.py
+#   map.py
+#   chart.py
+#   command.py
+#   trait.py
+#   withtraits.py
+#   traits/dynamicproperty.py
+#   traits/globalproperty.py
+#   traits/prototype.py
+#   traits/place.py
+#   traits/report.py
+#   traits/calculatedproperty.py
+#   traits/restrictcommand.py
+#   traits/label.py
+#   traits/layer.py
+#   traits/globalcommand.py
+#   traits/globalhotkey.py
+#   traits/nostack.py
+#   traits/deselect.py
+#   traits/restrictaccess.py
+#   traits/rotate.py
+#   traits/stack.py
+#   traits/mark.py
+#   traits/mask.py
+#   traits/trail.py
+#   traits/delete.py
+#   traits/sendto.py
+#   traits/moved.py
+#   traits/skel.py
+#   traits/submenu.py
+#   traits/basic.py
+#   traits/trigger.py
+#   traits/nonrect.py
+#   traits/click.py
+#   game.py
+#   buildfile.py
+#   moduledata.py
+#   save.py
+#   vsav.py
+#   vmod.py
+#   exporter.py
+#
+# ====================================================================
+# From ../common/singleton.py
+# ====================================================================
+class Singleton(type):
+    '''Meta base class for singletons'''
+    _instances = {}
+    def __call__(cls, *args, **kwargs):
+        '''Create the singleton object or returned existing
 
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''
+        if cls not in cls._instances:
+            cls._instances[cls] = \
+                super(Singleton, cls).__call__(*args, **kwargs)
+        return cls._instances[cls]
+
+#
+# EOF
+#
 # ====================================================================
-def add_pws(opw=None,upw=None):
-    '''Add password options to the poppler commands
+# From ../common/verbose.py
+# --------------------------------------------------------------------
 
-    Parameters
-    ----------
-    opw : str
-    	Owner password
-    upw : str
-    	User password
+class Verbose(metaclass=Singleton):
+    def __init__(self,verbose=False):
+        '''Singleton for writing message to screen, contigent on setting
 
-    Returns
-    -------
-    opts : list
-    	List of arguments 
-    '''
-    args = []
-    if  upw is not None:
-        args.extend(['-upw',upw])
+        Parameters
+        ----------
+        verbose : bool
+             Whether to show messages or not
+        '''
+        self._indent  = ''
+        self._verbose = verbose
 
-    if  opw is not None:
-        args.extend(['-opw',opw])
+    def setVerbose(self,verbose):
+        '''Set whether to print or not
 
-    return args
+        Parameters
+        ----------
+        verbose : bool
+             Whether to show messages or not
+        '''
+        self._verbose = verbose
 
-# --------------------------------------------------------------------
-def create_proc(args):
-    '''Create a process
+    @property
+    def verbose(self):
+        '''Test if this is verbose'''
+        return self._verbose
 
-    Parameters
-    ----------
-    args : list
-    	List of commmand line arguments
+    def message(self,*args,**kwargs):
+        '''Write messsage if verbose
 
-    Returns
-    -------
-    proc : subprocess.Process
-    	Process handle 
-    '''
-    from os import environ
-    from subprocess import Popen, PIPE, TimeoutExpired
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''        
+        if not self._verbose: return
+        if not kwargs.pop('noindent', False):
+            print(self._indent,end='')
+        print(*args,**kwargs)
 
-    return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
-    
+    def incr(self):
+        '''Increment indention'''
+        self._indent += ' '
+
+    def decr(self):
+        '''Decrement indention'''
+        if len(self._indent) > 0:
+            self._indent = self._indent[:-1]
+
+#
+# EOF
+#
+# ====================================================================
+# From ../common/verboseguard.py
 # --------------------------------------------------------------------
-def get_pdf_info(pdfname='export.pdf',upw=None,opw=None,timeout=None):
-    '''Get dictionary of PDF information
 
-    Parameters
-    ----------
-    pdfname : str
-    	File name
-    opw : str
-    	Owner password
-    upw : str
-    	User password
-    timeout : int
-    	Timeout in seconds or None
-    
-    Returns
-    -------
-    info : dict
-        PDF information
-    '''
-    from subprocess import TimeoutExpired
-   
-    args = ['pdfinfo', pdfname]
-    args.extend(add_pws(opw=opw,upw=upw))
+class VerboseGuard:
+    def __init__(self,*args,**kwargs):
+        '''A guard pattern that increases verbose indention
 
-    proc = create_proc(args)
+        This is a context manager.  The arguments passed are used for
+        an initial message, before increasinig indention.
 
-    try:
-        out, err = proc.communicate(timeout=timeout)
-    except Exception as e:
-        proc.kill()
-        proc.communicate()
-        raise RuntimeError(f'Failed to get PDF info: {e}')
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''                
+        Verbose().message(*args,**kwargs)
 
-    d = {}
-    for field in out.decode("utf8", "ignore").split("\n"):
-        sf         = field.split(":")
-        key, value = sf[0], ":".join(sf[1:])
-        if key != "":
-            d[key] = (int(value.strip()) if key == 'Pages' else value.strip())
+    def __bool_(self):
+        '''Test if verbose'''
+        return Verbose().verbose
+    
+    def __enter__(self):
+        '''Enter context'''
+        Verbose().incr()
+        return self
 
-    if "Pages" not in d:
-        raise ValueError
+    def __exit__(self,*args):
+        '''Exit context'''        
+        Verbose().decr()
 
-    return d
+    def __call__(self,*args,**kwargs):
+        '''Write a message at current indention level
         
-# --------------------------------------------------------------------
-def get_images_info(infofile):
-    '''Read in image information from file
+        Parameters
+        ----------
+        args : tuple
+            Arguments
+        kwargs : dict
+            Keyword arguments
+        '''                
+        Verbose().message(*args,**kwargs)
 
+#
+# EOF
+#
+# ====================================================================
+# From base.py
+# ====================================================================
+# Key encoding
+SHIFT = 65
+CTRL = 130
+ALT   = 520
+CTRL_SHIFT = CTRL+SHIFT
+ALT_SHIFT = ALT+SHIFT
+NONE = '\ue004'
+NONE_MOD = 0
+def key(let,mod=CTRL):
+
+    '''Encode a key sequence
+
     Parameters
     ----------
-    infofile : str
-         file name
+    let : str
+        Key code (Letter)
+    mod : int
+        Modifier mask
+    '''
+    return f'{ord(let)},{mod}'
+#
+def hexcolor(s):
+    if isinstance(s,str):
+        s = s.replace('0x','')
+        if len(s) == 3:
+            r, g, b = [int(si,16)/16 for si in s]
+        elif len(s) == 6:
+            r = int(s[0:2],16) / 256
+            g = int(s[2:4],16) / 256
+            b = int(s[4:6],16) / 256
+        else:
+            raise RuntimeError('3 or 6 hexadecimal digits for color string')
+    elif isinstance(s,int):
+        r = ((s >> 16) & 0xFF) / 256
+        g = ((s >>  8) & 0xFF) / 256
+        b = ((s >>  0) & 0xFF) / 256
+    else:
+        raise RuntimeError('Hex colour must be string or integer')
 
+    return rgb(int(r*256),int(g*256),int(b*256))
+    
+# Colour encoding 
+def rgb(r,g,b):
+    '''Encode RGB colour
+
+    Parameters
+    ----------
+    r : int
+        Red channel
+    g : int
+        Green channel
+    b : int
+        Blue channel
+
     Returns
     -------
-    images : list
-         List of image information 
+    colour : str
+        RGB colour as a string
     '''
-    #from csv import DictReader
-    from json import load 
+    return ','.join([str(r),str(g),str(b)])
 
-    with open(infofile,'r') as file:
-        info = load(file)
-        #reader = DictReader(file,fieldnames=['page','name','type','sub type'])
-        #return [l for l in reader]
+def rgba(r,g,b,a):
+    '''Encode RGBA colour
 
-    return info
-    
-
-# --------------------------------------------------------------------
-def convert_page(pdfname,page,opw=None,upw=None,timeout=None):
-    '''Convert a single PDF page to an image
-    
     Parameters
     ----------
-    pdfname : str
-    	File name
-    page : int
-        Page number 
-    opw : str
-    	Owner password
-    upw : str
-    	User password
-    timeout : int
-    	Timeout in seconds or None
+    r : int
+        Red channel
+    g : int
+        Green channel
+    b : int
+        Blue channel
+    a : int
+        Alpha channel
     
     Returns
     -------
-    img : bytes
-        The bytes of the image 
+    colour : str
+        RGBA colour as a string
     '''
-    args = ['pdftocairo',
-            '-singlefile',
-            '-f', str(page),
-            '-l', str(page),
-            '-png']
-    args.extend(add_pws(opw=opw,upw=upw))
-    args.append(pdfname)
-    args.append('-')
+    return ','.join([str(r),str(g),str(b),str(a)])
 
-    proc = create_proc(args)
+def dumpTree(node,ind=''):
+    '''Dump the tree of nodes
 
-    try:
-        out, err = proc.communicate(timeout=timeout)
-    except Exception as e:
-        proc.kill()
-        proc.communicate()
-        raise RuntimeError(f'Failed to convert page {page} of {pdfname}: {e}')
+    Parameters
+    ----------
+    node : xml.dom.Node
+        Node to dump
+    ind : str
+        Current indent 
+    '''
+    print(f'{ind}{node}')
+    for c in node.childNodes:
+        dumpTree(c,ind+' ')
 
-    from io import BytesIO
-    from PIL import Image
+#
+# EOF
+#
+# ====================================================================
+# From element.py
+
+# ====================================================================
+class Element:
+    BUILD  = 'VASSAL.build.'
+    MODULE = BUILD  + 'module.'
+    WIDGET = BUILD  + 'widget.'
+    MAP    = MODULE + 'map.'    
+    PICKER = MAP    + 'boardPicker.'
+    BOARD  = PICKER + 'board.'
+    def __init__(self,parent,tag,node=None,**kwargs):
+        '''Create a new element
+
+        Parameters
+        ----------
+        parent : Element
+            Parent element to add this element to
+        tag : str
+            Element tag
+        node : xml.dom.Node
+            If not None, then read attributes from that. Otherwise
+            set elements according to kwargs
+        kwargs : dict
+            Attribute keys and values.  Only used if node is None
+        '''
+        if parent is not None:
+            self._root = parent._root
+            self._node = (node if node is not None else
+                          parent.addNode(tag,**kwargs))
+        else:
+            self._root = None
+            self._node = None
+
+    # ----------------------------------------------------------------
+    # Attributes
+    def __contains__(self,key):
+        '''Check if element has attribute key'''        
+        return self.hasAttribute(key)
     
-    img = out #Image.open(BytesIO(out))
+    def __getitem__(self,key):
+        '''Get attribute key value'''
+        return self.getAttribute(key)
 
-    return img
+    def __setitem__(self,key,value):
+        '''Set attribute key value'''
+        self.setAttribute(key,value)
 
-# --------------------------------------------------------------------
-def ignore_entry(entry):
-    return entry['category'] in ['<<dummy>>','<<eol>>']
+    def hasAttribute(self,k):
+        '''Check if element has attribute '''
+        return self._node.hasAttribute(k)
 
-# --------------------------------------------------------------------
-def convert_pages(pdfname,infoname,
-                  opw=None,upw=None,timeout=None):
-    '''Convert pages in a PDF to images
+    def getAttribute(self,k):
+        '''Get attribute key value'''
+        return self._node.getAttribute(k)
+        
+    def setAttribute(self,k,v):
+        '''Set attribute key value'''
+        self._node.setAttribute(k,str(v).lower()
+                                if isinstance(v,bool) else str(v))
+        
+    def setAttributes(self,**kwargs):
+        '''Set attributes to dictionary key and value'''
+        for k,v in kwargs.items():
+            self.setAttribute(k,v)
 
-    This creates a list of dictionaries with the image name and data
-    (as bytes)
+    def getAttributes(self):
+        '''Get attributes as dict'''
+        return self._node.attributes
 
-    Parameters
-    ----------
-    pdfname : str
-    	File name of PDF to read images from 
-    infoname : str
-        File name of CSV to read image info from 
-    opw : str
-    	Owner password
-    upw : str
-    	User password
-    timeout : int
-    	Timeout in seconds or None
+    # ----------------------------------------------------------------
+    # Plain nodes
+    def getChildren(self):
+        '''Get child nodes (xml.dom.Node)'''
+        return self._node.childNodes
+
+    # ----------------------------------------------------------------
+    # Getters
+    #
+    # First generics 
+    def getAsDict(self,tag='',key=None,enable=True):
+        '''Get elements with a specific tag as a dictionary
+        where the key is given by attribute key'''
+        cont = self._node.getElementsByTagName(tag)
+        if not enable or key is None:
+            return cont
+
+        return {e.getAttribute(key): e for e in cont}
+
+    def getAsOne(self,tag='',single=True):
+        '''Get elements with a specific tag, as a list.
+        If single is true, then assume we only have one such
+        child element, or fail.'''
+        cont = self._node.getElementsByTagName(tag)
+        if single and len(cont) != 1:
+            return None
+        return cont
     
-    Returns
-    -------
-    imgs : list
-        List of images as dicts, info and the bytes of the images 
-    '''
+    def getElementsByKey(self,cls,key='',asdict=True):
+        '''Get elments of a specific class as a dictionary,
+        where the key is set by the key attribute.'''
+        cont = self.getAsDict(cls.TAG,key,asdict)
+        if cont is None: return None
+        
+        if not asdict: return [cls(self,node=n) for n in cont]
 
-    oargs   = {'opw':opw,'upw':upw,'timeout':timeout}
-    docinfo = get_pdf_info(pdfname,**oargs)
-    info    = get_images_info(infoname)
+        return {k : cls(self,node=n) for k, n in cont.items()}
 
-    if len(info)-1 != docinfo['Pages']:
-        raise RuntimeError(f'Number of pages in {pdfname} '
-                           f'({docinfo["Pages"]}) not {len(info)-1}')
+    def getAllElements(self,cls,single=True):
+        '''Get elements with a specific tag, as a list.  If single is
+        true, then assume we only have one such child element, or
+        fail.
 
-    for i in info:
-        if ignore_entry(i): continue
-        i['img'] = convert_page(pdfname,i['number'],**oargs)
+        '''
+        cont = self.getAsOne(cls.TAG,single=single)
+        if cont is None: return None
+        return [cls(self,node=n) for n in cont]
 
-    return info
+    def getSpecificElements(self,cls,key,*names,asdict=True):
+        '''Get all elements of specific class and that has the
+        attribute key, and the attribute value is in names
 
-# ====================================================================
-def remove_and_get(zipfname,*filenames):
-    '''Open zip file, and make a copy of it without the files listed
-    in entries.  Return the data of files listed in entry.'''
-    from tempfile import mkdtemp
-    from zipfile import ZipFile
-    from shutil import move, rmtree 
-    from os import path
+        '''
+        cont = self.getAsOne(cls.TAG,single=False)
+        cand = [cls(self,node=n) for n in cont
+                if n.getAttribute(key) in names]
+        if asdict:
+            return {c[key] : c for c in cand}
+        return cand
+    
+    def getParent(self,cls,checkTag=True):
+        if self._node.parentNode is None:
+            return None
+        if checkTag and self._node.parentNode.tagName != cls.TAG:
+            return None
+        return cls(self,node=self._node.parentNode)
 
-    tempdir = mkdtemp()
-    ret     = {}
-    try:
-        tempname = path.join(tempdir, 'new.zip')
-        with ZipFile(zipfname, 'r') as zipread:
+    def getParentOfClass(self,cls):
+        '''Searches back until we find the parent with the right
+        class, or none
+        '''
+        t = {c.TAG: c for c in cls}
+        p = self._node.parentNode
+        while p is not None:
+            c = t.get(p.tagName,None)
+            if c is not None: return c(self,node=p)
+            p = p.parentNode
+        return None
+        
+    # ----------------------------------------------------------------
+    # Adders
+    def addNode(self,tag,**attr):
+        '''Add a note to this element
 
-            with ZipFile(tempname, 'w') as zipwrite:
+        Parameters
+        ----------
+        tag : str
+            Node tag name
+        attr : dict
+            Attributes to set
+        '''
+        e = self._root.createElement(tag)
+        if self._node: self._node.appendChild(e)
 
-                for item in zipread.infolist():
-                    data = zipread.read(item.filename)
+        for k, v in attr.items():
+            e.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
 
-                    if item.filename not in filenames:
-                        zipwrite.writestr(item, data)
-                    else:
-                        ret[item.filename] = data
-                        
-        move(tempname, zipfname)
-    finally:
-        rmtree(tempdir)
+        return e
 
-    return ret
+    def addText(self,text):
+        '''Add a text child node to an element'''
+        t = self._root.createTextNode(text)
+        self._node.appendChild(t)
+        return t
 
+    def getText(self):
+        '''Get contained text node content'''
+        if self._node.firstChild is None or \
+           self._node.firstChild.nodeType != self._node.firstChild.TEXT_NODE:
+            return ''
+        return self._node.firstChild.nodeValue
+        
+
+    def add(self,cls,**kwargs):
+        '''Add an element and return wrapped in cls object'''
+        return cls(self,node=None,**kwargs)
+
+    def append(self,elem):
+        '''Append and element'''
+        if self._node.appendChild(elem._node):
+            return elem
+        return False
+
+    # ----------------------------------------------------------------
+    def remove(self,elem):
+        '''Remove an element'''
+        try:
+            self._node.removeChild(elem._node)
+        except:
+            return None
+        return elem
+    # ----------------------------------------------------------------
+    def insertBefore(self,toadd,ref):
+        '''Insert an element before another element'''
+        try:
+            self._node.insertBefore(toadd._node,ref._node)
+        except:
+            return None
+        return toadd
+
 # --------------------------------------------------------------------
-def add_back(zipfname,files):
-    '''Open a zip file for appending and add data in files
+class DummyElement(Element):
+    def __init__(self,parent,node=None,**kwargs):
+        '''A dummy element we can use to select elements of different
+        classes
 
-    Files is a dictionary of filenames mapping to file content,
-    as returned by remove_and_get
-    '''
-    from zipfile import ZipFile
-    
-    with ZipFile(zipfname, 'a') as zipadd:
-        for filename,data in files.items():
-            zipadd.writestr(filename,data)
+        '''  
+        super(DummyElement,self).__init__(parent,'Dummy',node=node)
 
+
+#
+# EOF
+#
 # ====================================================================
-# Contants used for tag names
-BUILD  = 'VASSAL.build.'
-MODULE = BUILD + 'module.'
-WIDGET = BUILD + 'widget.'
-MAP    = MODULE + 'map.'    
-PICKER = MAP + 'boardPicker.'
+# From globalkey.py
 
+# --------------------------------------------------------------------
+class GlobalKey(Element):
+    SELECTED = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS'
+    def __init__(self,
+                 parent,
+                 tag,
+                 node                 = None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = SELECTED,
+                 tooltip              = '',
+                 icon                 = ''):
+        '''
+        Parameters
+        ----------
+        - tag          The XML tag to use
+        - parent       Parent node
+        - node         Optionally existing node
+        - name         Name of key
+        - buttonHotkey Key in "global" scope
+        - hotkey       Key to send to targeted pieces
+        - buttonText   Text on button
+        - canDisable   If true, disabled when propertyGate is true
+        - deckCount    Number of decks (-1 is all)
+        - filter       Which units to target
+        - propertyGate When true, disable
+        - reportFormat Chat message
+        - reportSingle Also show single piece reports
+        - singleMap    Only originating map if True
+        - target       Preselection filter (default selected pieces)
+        - tooltip      Hover-over message
+        - icon         Image to use as icon
+
+        Default targets are selected units
+        '''
+        nme = (name    if len(name)    > 0 else
+               tooltip if len(tooltip) > 0 else '')
+        tip = (tooltip if len(tooltip) > 0 else
+               name    if len(name)    > 0 else '') 
+        super(GlobalKey,self).\
+            __init__(parent,
+                     tag,
+                     node                 = node,
+                     name                 = nme,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tip,
+                     icon                 = icon)
+#
+# EOF
+# 
 # ====================================================================
-def get_asdict(elem,tag,key,enable=True):
-    cont = elem.getElementsByTagName(tag)
-    if not enable:
-        return cont
+# From gameelements.py
 
-    return {e.getAttribute(key): e for e in cont}
+# --------------------------------------------------------------------
+class GameElementService:
+    def getGame(self):
+        return self.getParent(Game)
 
 # --------------------------------------------------------------------
-def get_asone(elem,tag,enable=True):
-    cont = elem.getElementsByTagName(tag)
-    if enable and len(cont) != 1:
-        return None
-    return cont
+class GameElement(Element,GameElementService):
+    def __init__(self,game,tag,node=None,**kwargs):
+        super(GameElement,self).__init__(game,tag,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class Notes(GameElement):
+    TAG = Element.MODULE+'NotesWindow'
+    def __init__(self,elem,node=None,
+                 name             = 'Notes',
+                 buttonText       = '',
+                 hotkey           = key('N',ALT),
+                 icon             = '/images/notes.gif',
+                 tooltip          = 'Show notes window'):
+        super(Notes,self).__init__(elem,self.TAG,node=node,
+                                   name       = name,
+                                   buttonText = buttonText,
+                                   hotkey     = hotkey,
+                                   icon       = icon,
+                                   tooltip    = tooltip)
+    def encode(self):
+        return ['NOTES\t\\','PNOTES']
+
+# --------------------------------------------------------------------
+class PredefinedSetup(GameElement):
+    TAG = Element.MODULE+'PredefinedSetup'
+    def __init__(self,elem,node=None,
+                 name             = '',
+                 file             = '',
+                 useFile          = False,
+                 isMenu           = False,
+                 description      = ''):
+        useFile = ((useFile or not isMenu) and
+                   (file is not None and len(file) > 0))
+        if file is None: file = ''
+        super(PredefinedSetup,self).__init__(elem,self.TAG,node=node,
+                                             name        = name,
+                                             file        = file,
+                                             useFile     = useFile,
+                                             isMenu      = isMenu,
+                                             description = description)
+    def addPredefinedSetup(self,**kwargs):
+        '''Add a `PredefinedSetup` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PredefinedSetup
+            The added element
+        '''
+        return self.add(PredefinedSetup,**kwargs)
+    def getPredefinedSetups(self,asdict=True):
+        '''Get all PredefinedSetup element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `PredefinedSetup` elements.  If `False`, return a list of all PredefinedSetup` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `PredefinedSetup` children
+        '''
+        return self.getElementsByKey(PredefinedSetup,'name',asdict)
+        
     
+        
+                   
+                  
 # --------------------------------------------------------------------
-def get_doc(string):
-    from xml.dom.minidom import parseString
+class GlobalTranslatableMessages(GameElement):
+    TAG=Element.MODULE+'properties.GlobalTranslatableMessages'
+    def __init__(self,elem,node=None):
+        '''Translations
 
-    return parseString(string)
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        '''
+        super(GlobalTranslatableMessages,self).\
+            __init__(elem,self.TAG,node=node)
 
 # --------------------------------------------------------------------
-def get_game(doc):
-    return doc.getElementsByTagName('VASSAL.build.GameModule')[0]
+class Language(GameElement):
+    TAG = 'VASSAL.i18n.Language'
+    def __init__(self,elem,node=None,**kwargs):
+        super(Languate,self).__init__(sele,self.TAG,node=none,**kwargs)
+        
+# --------------------------------------------------------------------
+class Chatter(GameElement):
+    TAG=Element.MODULE+'Chatter'
+    def __init__(self,elem,node=None,**kwargs):
+        '''Chat
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        kwargs : dict
+            Attributes
+        '''
+        super(Chatter,self).__init__(elem,self.TAG,node=node,**kwargs)
+        
 # --------------------------------------------------------------------
-def get_globalproperties(doc,one=True):
-    return get_asone(doc,MODULE+'properties.GlobalProperties',one)
+class KeyNamer(GameElement):
+    TAG=Element.MODULE+'KeyNamer'
+    def __init__(self,elem,node=None,**kwargs):
+        '''Key namer (or help menu)
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        kwargs : dict
+            Attributes
+        '''
+        super(KeyNamer,self).__init__(elem,self.TAG,node=node,**kwargs)
+        
+
 # --------------------------------------------------------------------
-def get_globalproperty(doc,asdict=True):
-    '''A little confusing - this returns a dictionary'''
-    return get_asdict(doc,MODULE+'properties.GlobalProperty','name',asdict)
+#    <VASSAL.build.module.GlobalOptions
+#      autoReport="Always"
+#      centerOnMove="Use Preferences Setting"
+#      chatterHTMLSupport="Always"
+#      hotKeysOnClosedWindows="Always"
+#      inventoryForAll="Never"
+#      nonOwnerUnmaskable="Always"
+#      playerIdFormat="$PlayerName$"
+#      promptString="Opponents can unmask pieces"
+#      sendToLocationMoveTrails="Always"
+#      storeLeadingZeroIntegersAsStrings="true">
+#        <option name="stepIcon">/images/StepForward16.gif</option>
+#        <option name="stepHotKey">39,130</option>
+#        <option name="undoIcon">/images/Undo16.gif</option>
+#        <option name="undoHotKey">90,130</option>
+#        <option name="serverControlsIcon">/images/connect.gif</option>
+#        <option name="serverControlsHotKey">65,195</option>
+#        <option name="debugControlsIcon"/>
+#        <option name="debugControlsHotKey">68,195</option>
+#    </VASSAL.build.module.GlobalOptions>
+class GlobalOptions(GameElement):
+    NEVER  = 'Never'
+    ALWAYS = 'Always'
+    PROMPT = 'Use Preferences Setting'
+    TAG    = Element.MODULE+'GlobalOptions'
+    def __init__(self,doc,node=None,
+                 autoReport               = PROMPT,
+                 centerOnMove             = PROMPT,
+                 chatterHTMLSupport       = ALWAYS,
+                 hotKeysOnClosedWindows   = NEVER,
+                 inventoryForAll          = ALWAYS,
+                 nonOwnerUnmaskable       = PROMPT,
+                 playerIdFormat           = "$playerName$",
+                 promptString             = "Opponents can unmask pieces",
+                 sendToLocationMoveTrails = NEVER,
+                 storeLeadingZeroIntegersAsStrings = False,
+                 description                       = 'Global options',
+                 dragThreshold                     = 10):
+        '''Set global options on the module
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        
+        autoReport                        : str='always'
+        centerOnMove                      : str Option
+        chatterHTMLSupport                : str='never'
+        hotKeysOnClosedWindows            : str='never'
+        inventoryForAll                   : str='always' 
+        nonOwnerUnmaskable                : str='never'
+        playerIdFormat                    : str='$PlayerName$'
+        promptString                      : str=?
+        sendToLocationMoveTrails          : bool=false
+        storeLeadingZeroIntegersAsStrings : bool=False
+        '''
+        super(GlobalOptions,self).\
+            __init__(doc,self.TAG,node=node,
+                     autoReport               = autoReport,
+                     centerOnMove             = centerOnMove,
+                     chatterHTMLSupport       = chatterHTMLSupport,
+                     hotKeysOnClosedWindows   = hotKeysOnClosedWindows,
+                     inventoryForAll          = inventoryForAll,
+                     nonOwnerUnmaskable       = nonOwnerUnmaskable,
+                     playerIdFormat           = playerIdFormat,
+                     promptString             = promptString,
+                     sendToLocationMoveTrails = sendToLocationMoveTrails,
+                     storeLeadingZeroIntegersAsStrings = storeLeadingZeroIntegersAsStrings,
+                     dragThreshold            = dragThreshold,
+                     description              = description)
+
+    def addOption(self,**kwargs):
+        '''Add a `Option` element to this
+
+        Options known
+        - stepIcon - image file name (/images/StepForward16.gif)
+        - stepHotKey - key 
+        - undoIcon - image file name (/images/Undo16.gif)
+        - undoHotKey - key
+        - serverControlsIcon - image file name (/images/connect.gif)
+        - serverControlsHotKey - key
+        - debugControlsIcon - image file name 
+        - debugControlsHotKey - key 
+        
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Option
+            The added element
+        '''
+        return self.add(Option,**kwargs)
+    def getOptions(self):
+        return self.getElementsByKey(Option,'name')
+
+    def addPreference(self,cls,**kwargs):
+        return self.add(cls,**kwargs)
+
+    def addIntPreference(self,**kwargs):
+        return self.add(IntPreference,**kwargs)
+
+    def addFloatPreference(self,**kwargs):
+        return self.add(FloatPreference,**kwargs)
+    
+    def addBoolPreference(self,**kwargs):
+        return self.add(BoolPreference,**kwargs)
+    
+    def addStrPreference(self,**kwargs):
+        return self.add(StrPreference,**kwargs)
+    
+    def addTextPreference(self,**kwargs):
+        return self.add(TextPreference,**kwargs)
+    
+    def addEnumPreference(self,**kwargs):
+        return self.add(EnumPreference,**kwargs)
+    
+    def getIntPreferences(self):
+        return self.getElementsByKey(IntPreference,'name')
+
+    def getFloatPreferences(self):
+        return self.getElementsByKey(FloatPreference,'name')
+
+    def getBoolPreferences(self):
+        return self.getElementsByKey(BoolPreference,'name')
+
+    def getStrPreferences(self):
+        return self.getElementsByKey(StrPreference,'name')
+
+    def getTextPreferences(self):
+        return self.getElementsByKey(TextPreference,'name')
+
+    def getEnumPreferences(self):
+        return self.getElementsByKey(EnumPreference,'name')
+
+    def getPreferences(self):
+        retd = {}
+        for cls in [IntPreference,
+                    FloatPreference,
+                    BoolPreference,
+                    StrPreference,
+                    TextPreference,
+                    EnumPreference]:
+            retd.update(self.getElementsByKey(cls,'name'))
+
+        return retd
+    
+
 # --------------------------------------------------------------------
-def get_turntrack(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.TurnTracker','name',asdict)
+class Option(Element):
+    TAG = 'option'
+    def __init__(self,doc,node=None,name='',value=''):
+        super(Option,self).__init__(doc,tag=self.TAG,node=node,name=name)
+        self.addText(value)
 
+    def getGlobalOptions(self):
+        return self.getParent(GlobalOptions)
+
 # --------------------------------------------------------------------
-def get_turncounter(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.CounterTurnLevel','property',asdict)
+class Preference(Element):
+    PREFS = 'VASSAL.preferences.'
+    def __init__(self,
+                 doc,
+                 tag,
+                 node    = None,
+                 name    = '',
+                 default = '',
+                 desc    = '',
+                 tab     = '',
+                 **kwargs):
+        '''Add a preference
 
+        Parameters
+        ----------
+        name : str
+            Name of property
+        default : str
+            Default value
+        desc : str
+            Description
+        tab : str
+            Preference tab to put in to
+        '''
+        super(Preference,self).__init__(doc,
+                                        tag     = tag,
+                                        node    = node,
+                                        name    = name,
+                                        default = default,
+                                        desc    = desc,
+                                        tab     = tab)
+
+    def getGlobalOptions(self):
+        return self.getParent(GlobalOptions)
+    
 # --------------------------------------------------------------------
-def get_turnlist(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.ListTurnLevel','property',asdict)
+class IntPreference(Preference):
+    TAG = Preference.PREFS+'IntegerPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = 0,
+                 desc    = '',
+                 tab     = ''):
+        super(IntPreference,self).__init__(doc,
+                                           tag     = self.TAG,
+                                           node    = node,
+                                           name    = name,
+                                           default = str(default),
+                                           desc    = desc,
+                                           tab     = tab)
+# --------------------------------------------------------------------
+class FloatPreference(Preference):
+    TAG = Preference.PREFS+'DoublePreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = 0.,
+                 desc    = '',
+                 tab     = ''):
+        super(FloatPreference,self).__init__(doc,
+                                             tag     = self.TAG,
+                                             node    = node,
+                                             name    = name,
+                                             default = str(default),
+                                             desc    = desc,
+                                             tab     = tab)
+# --------------------------------------------------------------------
+class BoolPreference(Preference):
+    TAG = Preference.PREFS+'BooleanPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = False,
+                 desc    = '',
+                 tab     = ''):
+        super(BoolPreference,self).__init__(doc,
+                                            tag     = self.TAG,
+                                            node    = node,
+                                            name    = name,
+                                            default = ('true' if default
+                                                       else 'false'),
+                                            desc    = desc,
+                                            tab     = tab)
+# --------------------------------------------------------------------
+class StrPreference(Preference):
+    TAG = Preference.PREFS+'StringPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = '',
+                 desc    = '',
+                 tab     = ''):
+        super(StrPreference,self).__init__(doc,
+                                           tag     = self.TAG,
+                                           node    = node,
+                                           name    = name,
+                                           default = default,
+                                           desc    = desc,
+                                           tab     = tab)
+# --------------------------------------------------------------------
+class TextPreference(Preference):
+    TAG = Preference.PREFS+'TextPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 default = '',
+                 desc    = '',
+                 tab     = ''):
+        super(TextPreference,self).__init__(doc,
+                                            tag     = self.TAG,
+                                            node    = node,
+                                            name    = name,
+                                            default = (default
+                                                       .replace('\n','
')),
+                                            desc    = desc,
+                                            tab     = tab)
+# --------------------------------------------------------------------
+class EnumPreference(Preference):
+    TAG = Preference.PREFS+'EnumPreference'
+    def __init__(self,
+                 doc,
+                 node    = None,
+                 name    = '',
+                 values  = [],
+                 default = '',
+                 desc    = '',
+                 tab     = ''):
+        ce = lambda v : str(v).replace(',',r'\,')
+        sl = [ce(v) for v in values]
+        df = ce(v)
+        assert df in sl, \
+            f'Default value "{default}" not in list {":".join(values)}'
+        super(EnumPreference,self).__init__(doc,
+                                            tag     = self.TAG,
+                                            node    = node,
+                                            name    = name,
+                                            default = df,
+                                            desc    = desc,
+                                            tab     = tab,
+                                            list    = sl)
 
+    
 # --------------------------------------------------------------------
-def get_turnhotkey(doc,asdict=True):
-    return get_asdict(doc,MODULE+'turn.TurnGlobalHotkey','hotkey',asdict)
+# CurrentMap == "Board"
+class Inventory(GameElement):
+    TAG = Element.MODULE+'Inventory'
+    def __init__(self,doc,node=None,
+                  canDisable          = False,
+                  centerOnPiece       = True,
+                  disabledIcon        = '',
+                  drawPieces          = True,
+                  foldersOnly         = False,
+                  forwardKeystroke    = True,
+                  groupBy             = '',
+                  hotkey              = key('I',ALT),
+                  icon                = '/images/inventory.gif',
+                  include             = '{}',
+                  launchFunction      = 'functionHide',
+                  leafFormat          = '$PieceName$',
+                  name                = '',
+                  nonLeafFormat       = '$PropertyValue$',
+                  pieceZoom           = '0.33',
+                  pieceZoom2          = '0.5',
+                  pieceZoom3          = '0.6',
+                  propertyGate        = '',
+                  refreshHotkey       = key('I',ALT_SHIFT),
+                  showMenu            = True,
+                  sides               = '',
+                  sortFormat          = '$PieceName$',
+                  sortPieces          = True,
+                  sorting             = 'alpha',
+                  text                = '',
+                  tooltip             = 'Show inventory of all pieces',
+                  zoomOn              = False):
+        super(Inventory,self).__init__(doc,self.TAG,node=node,
+                                       canDisable          = canDisable,
+                                       centerOnPiece       = centerOnPiece,
+                                       disabledIcon        = disabledIcon,
+                                       drawPieces          = drawPieces,
+                                       foldersOnly         = foldersOnly,
+                                       forwardKeystroke    = forwardKeystroke,
+                                       groupBy             = groupBy,
+                                       hotkey              = hotkey,
+                                       icon                = icon,
+                                       include             = include,
+                                       launchFunction      = launchFunction,
+                                       leafFormat          = leafFormat,
+                                       name                = name,
+                                       nonLeafFormat       = nonLeafFormat,
+                                       pieceZoom           = pieceZoom,
+                                       pieceZoom2          = pieceZoom2,
+                                       pieceZoom3          = pieceZoom3,
+                                       propertyGate        = propertyGate,
+                                       refreshHotkey       = refreshHotkey,
+                                       showMenu            = showMenu,
+                                       sides               = sides,
+                                       sortFormat          = sortFormat,
+                                       sortPieces          = sortPieces,
+                                       sorting             = sorting,
+                                       text                = text,
+                                       tooltip             = tooltip,
+                                       zoomOn              = zoomOn)
+                  
+    
+        
 
+    
+    
+
+
+
+
+
+
 # --------------------------------------------------------------------
-def get_documentation(doc,one=True):
-    return get_asone(doc,MODULE+'Documentation',one)
+class Prototypes(GameElement):
+    TAG = Element.MODULE+'PrototypesContainer'
+    def __init__(self,game,node=None,**kwargs):
+        super(Prototypes,self).\
+            __init__(game,self.TAG,node=node,**kwargs)
 
+    def addPrototype(self,**kwargs):
+        '''Add a `Prototype` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Prototype
+            The added element
+        '''
+        return self.add(Prototype,**kwargs)
+    def getPrototypes(self,asdict=True):
+        '''Get all Prototype element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Prototype` elements.  If `False`, return a list of all Prototype` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Prototype` children
+        '''
+        return self.getElementsByKey(Prototype,'name',asdict=asdict)
+        
+    
+
+
 # --------------------------------------------------------------------
-def get_helpfiles(doc,key='fileName',asdict=True):
-    return get_asdict(doc,MODULE+'documentation.HelpFile',key,asdict)
+class DiceButton(GameElement):
+    TAG=Element.MODULE+'DiceButton'
+    def __init__(self,elem,node=None,
+                 addToTotal           = 0,
+                 canDisable           = False,
+                 hotkey               = key('6',ALT),
+                 icon                 = '/images/die.gif',
+                 keepCount            = 1,
+                 keepDice             = False,
+                 keepOption           = '>',
+                 lockAdd              = False,
+                 lockDice             = False,
+                 lockPlus             = False,
+                 lockSides            = False,
+                 nDice                = 1,
+                 nSides               = 6,
+                 name                 = '1d6',
+                 plus                 = 0,
+                 prompt               = False,
+                 propertyGate         = '',
+                 reportFormat         = '** $name$ = $result$ *** <$PlayerName$>;',
+                 reportTotal          = False,
+                 sortDice             = False,
+                 text                 = '1d6',
+                 tooltip              = 'Roll a 1d6'):
+        super(DiceButton,self).\
+            __init__(elem,self.TAG,node=node,
+                     addToTotal           = addToTotal,
+                     canDisable           = canDisable,
+                     hotkey               = hotkey,
+                     icon                 = icon,
+                     keepCount            = keepCount,
+                     keepDice             = keepDice,
+                     keepOption           = keepOption,
+                     lockAdd              = lockAdd,
+                     lockDice             = lockDice,
+                     lockPlus             = lockPlus,
+                     lockSides            = lockSides,
+                     nDice                = nDice,
+                     nSides               = nSides,
+                     name                 = name,
+                     plus                 = plus,
+                     prompt               = prompt,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportTotal          = reportTotal,
+                     sortDice             = sortDice,
+                     text                 = text,
+                     tooltip              = tooltip)
 
 # --------------------------------------------------------------------
-def get_tutorials(doc,key='name',asdict=True):
-    return get_asdict(doc,MODULE+'documentation.Tutorial',key,asdict)
+class GameMassKey(GlobalKey,GameElementService):
+    TAG = Element.MODULE+'GlobalKeyCommand'
+    def __init__(self,map,node=None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = GlobalKey.SELECTED,
+                 tooltip              = '',
+                 icon                 = ''):
+        '''Default targets are selected units'''
+        super(GameMassKey,self).\
+            __init__(map,
+                     self.TAG,
+                     node                 = node,
+                     name                 = name,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tooltip,
+                     icon                 = icon)
+        
+# --------------------------------------------------------------------
+class StartupMassKey(GlobalKey,GameElementService):
+    TAG = Element.MODULE+'StartupGlobalKeyCommand'
+    FIRST_LAUNCH = 'firstLaunchOfSession'
+    EVERY_LAUNCH = 'everyLaunchOfSession'
+    START_GAME   = 'startOfGameOnly'
+    def __init__(self,
+                 map,
+                 node                 = None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = GlobalKey.SELECTED,
+                 tooltip              = '',
+                 icon                 = '',
+                 whenToApply          = EVERY_LAUNCH):
+        '''Default targets are selected units'''
+        super(StartupMassKey,self).\
+            __init__(map,
+                     self.TAG,
+                     node                 = node,
+                     name                 = name,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tooltip,
+                     icon                 = icon)
+        if node is None:
+            self['whenToApply'] = whenToApply
 
 # --------------------------------------------------------------------
-def get_inventories(doc,key='name',asdict=False):
-    return get_asdict(doc,MODULE+'Inventory',key,asdict)
+class Menu(GameElement):
+    TAG = Element.MODULE+'ToolbarMenu'
+    def __init__(self,
+                 game,
+                 node                 = None,
+                 canDisable           = False,
+                 description          = '',
+                 disabledIcon         = '',
+                 hotkey               = '',
+                 icon                 = '',
+                 menuItems            = [],# Button texts
+                 propertyGate         = '',
+                 text                 = '',# Menu name
+                 tooltip              = ''):
+        if len(description) <= 0 and len(tooltip) > 0:
+            description = tooltip
+        if len(tooltip) <= 0 and len(description) > 0:
+            tooltip = description 
+        super(Menu,self).\
+            __init__(game,
+                     self.TAG,
+                     node                 = node,
+                     canDisable           = canDisable,
+                     description          = description,
+                     disabledIcon         = disabledIcon,
+                     hotkey               = hotkey,
+                     icon                 = icon,
+                     menuItems            = ','.join(menuItems),
+                     propertyGate         = propertyGate,
+                     text                 = text,
+                     tooltip              = tooltip)
+                     
+        
+# --------------------------------------------------------------------
+class SymbolicDice(GameElement):
+    TAG = Element.MODULE+'SpecialDiceButton'
+    def __init__(self,
+                 game,
+                 node                    = None,
+                 canDisable	         = False,
+                 disabledIcon            = '',
+                 hotkey                  = key('6',ALT),
+                 name                    = "Dice",  # GP prefix
+                 text                    = '', # Text on button
+                 icon                    = '/images/die.gif', # Icon on button
+                 format                  = '{name+": "+result1}', # Report 
+                 tooltip                 = 'Die roll', # Help
+                 propertyGate            = '', # Property to disable when T
+                 resultButton            = False, # Result on button?
+                 resultChatter           = True,  # Result in Chatter?
+                 resultWindow            = False, # Result window?
+                 backgroundColor	 = rgb(0xdd,0xdd,0xdd),  # Window background
+                 windowTitleResultFormat = "$name$", # Window title
+                 windowX                 = '67', # Window size
+                 windowY                 = '65'):
+        super(SymbolicDice,self).\
+            __init__(game,
+                     self.TAG,
+                     node                    = node,
+                     canDisable	             = canDisable,
+                     disabledIcon            = disabledIcon,
+                     hotkey                  = hotkey,
+                     name                    = name,
+                     text                    = text,
+                     icon                    = icon,
+                     format                  = format,
+                     tooltip                 = tooltip,
+                     propertyGate            = propertyGate,
+                     resultButton            = resultButton,
+                     resultChatter           = resultChatter,
+                     resultWindow            = resultWindow,
+                     backgroundColor	     = backgroundColor,
+                     windowTitleResultFormat = windowTitleResultFormat,
+                     windowX                 = windowX,
+                     windowY                 = windowY)
 
+    def addDie(self,**kwargs):
+        return self.add(SpecialDie,**kwargs)
+
+    def getSymbolicDice(self):
+        return self.getParent(SymbolicDice)
+        
+        
 # --------------------------------------------------------------------
-def get_maps(doc,asdict=True):
-    return get_asdict(doc,MODULE+'Map','mapName',asdict)
+class SpecialDie(GameElement):
+    TAG = Element.MODULE+'SpecialDie'
+    def __init__(self,
+                 symbolic,               # Symblic dice 
+                 node                    = None,
+                 name                    = '', # Name of dice (no GP)
+                 report                  = '{name+": "+result}',
+                 faces                   = None):
+        super(SpecialDie,self).\
+            __init__(symbolic,
+                     self.TAG,
+                     node = node,
+                     name = name,
+                     report = report)
+        if node is not None or faces is None:
+            return
+        if isinstance(faces,list):
+            faces = {i+1: f for i,f in enumerate(faces)}
+        for v,f in faces:
+            self.addFace(text = str(v), value = v, icon = f)
 
+    def addFace(self,**kwargs):
+        self.add(DieFace,**kwargs)
+
+    def getSymbolicDice(self):
+        return self.getParent(SymbolicDice)
+        
+
+        
+                     
 # --------------------------------------------------------------------
-def get_widgetmap(doc,asdict=True):
-    return get_asdict(doc,WIDGET+'WidgetMap','mapName',asdict)
+class DieFace(GameElement):
+    TAG = Element.MODULE+'SpecialDieFace'
+    def __init__(self,
+                 special,               # Special dice
+                 node,                  # existing node
+                 icon      = '',        # graphical representation
+                 text      = '',        # Text representation
+                 value     = 0):        # Value representation
+        super(DieFace,self).\
+            __init__(special,
+                     self.TAG,
+                     node      = node,
+                     icon      = icon,
+                     text      = text,
+                     value     = value)
+                     
+    def getSpecialDie(self):
+        return self.getParent(SpecialDie)
+#
+# EOF
+#
+# ====================================================================
+# From mapelements.py
 
 # --------------------------------------------------------------------
-def get_chartwindows(doc,asdict=True):
-    return get_asdict(doc,MODULE+'ChartWindow','name',asdict)
+class MapElementService:
+    def getMap(self):
+        '''Get map - either a Map or WidgetMap'''
+        return self.getParentOfClass([WidgetMap,Map])
+        # if self._parent is None:
+        #     return None
+        # 
+        # if 'WidgetMap' in self._parent.tagName:
+        #     return self.getParent(WidgetMap)
+        #     
+        # return self.getParent(Map)
+    def getGame(self):
+        m = self.getMap()
+        if m is not None: return m.getGame()
+        return None
 
 # --------------------------------------------------------------------
-def get_map(doc,name):
-    maps = get_maps(doc,True)
-    return maps.get(name, None) if maps else None
+class MapElement(Element,MapElementService):
+    def __init__(self,map,tag,node=None,**kwargs):
+        super(MapElement,self).__init__(map,tag,node=node,**kwargs)
 
+
 # --------------------------------------------------------------------
-def get_boards(doc,asdict=True):
-    return get_asdict(doc,PICKER+'Board','name',asdict)
+class StackMetrics(MapElement):
+    TAG=Element.MAP+'StackMetrics'
+    def __init__(self,map,node=None,
+                 bottom               = key('(',0),
+                 down                 = key('%',0),
+                 top                  = key('&',0),
+                 up                   = key("'",0),
+                 disabled             = False,
+                 exSepX               = 6,   # Expanded (after double click)
+                 exSepY               = 18,  # Expanded (after double click)
+                 unexSepX             = 8,   # Compact
+                 unexSepY             = 16): # Compact
+        super(StackMetrics,self).__init__(map,self.TAG,node=node,
+                                          bottom               = bottom,
+                                          disabled             = disabled,
+                                          down                 = down,
+                                          exSepX               = exSepX,
+                                          exSepY               = exSepY,
+                                          top                  = top,
+                                          unexSepX             = unexSepX,
+                                          unexSepY             = unexSepY,
+                                          up                   = up)
 
 # --------------------------------------------------------------------
-def get_zoned(doc,one=True):
-    return get_asone(doc,PICKER+'board.ZonedGrid',one)
+class ImageSaver(MapElement):
+    TAG=Element.MAP+'ImageSaver'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 canDisable           = False,
+                 hotkey               = '',
+                 icon                 = '/images/camera.gif',
+                 propertyGate         = '',
+                 tooltip              = 'Save map as PNG image'):
+        super(ImageSaver,self).__init__(map,self.TAG,node=node,
+                                        buttonText           = buttonText,
+                                        canDisable           = canDisable,
+                                        hotkey               = hotkey,
+                                        icon                 = icon,
+                                        propertyGate         = propertyGate,
+                                        tooltip              = tooltip)
+# --------------------------------------------------------------------
+class TextSaver(MapElement):
+    TAG=Element.MAP+'TextSaver'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 canDisable           = False,
+                 hotkey               = '',
+                 icon                 = '/images/camera.gif',
+                 propertyGate         = '',
+                 tooltip              = 'Save map as text'):
+        super(TextSaver,self).__init__(map,self.TAG,node=node,
+                                        buttonText           = buttonText,
+                                        canDisable           = canDisable,
+                                        hotkey               = hotkey,
+                                        icon                 = icon,
+                                        propertyGate         = propertyGate,
+                                        tooltip              = tooltip)
 
+
 # --------------------------------------------------------------------
-def get_zone(doc,asdict=True):
-    return get_asdict(doc,PICKER+'board.mapgrid.Zone','name',asdict)
+class ForwardToChatter(MapElement):
+    TAG=Element.MAP+'ForwardToChatter'
+    def __init__(self,map,node=None,**kwargs):
+        super(ForwardToChatter,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_hexgrid(doc,one=True):
-    return get_asone(doc,PICKER+'board.HexGrid',one)
+class MenuDisplayer(MapElement):
+    TAG=Element.MAP+'MenuDisplayer'
+    def __init__(self,map,node=None,**kwargs):
+        super(MenuDisplayer,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_squaregrid(doc,one=True):
-    return get_asone(doc,PICKER+'board.SquareGrid',one)
+class MapCenterer(MapElement):
+    TAG=Element.MAP+'MapCenterer'
+    def __init__(self,map,node=None,**kwargs):
+        super(MapCenterer,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_regiongrid(doc,one=True):
-    return get_asone(doc,PICKER+'board.RegionGrid',one)
+class StackExpander(MapElement):
+    TAG=Element.MAP+'StackExpander'
+    def __init__(self,map,node=None,**kwargs):
+        super(StackExpander,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_hexnumbering(doc,one=True):
-    return get_asone(doc,PICKER+'board.mapgrid.HexGridNumbering',one)
-        
+class PieceMover(MapElement):
+    TAG=Element.MAP+'PieceMover'
+    def __init__(self,map,node=None,**kwargs):
+        super(PieceMover,self).__init__(map,self.TAG,node=node,**kwargs)
+
 # --------------------------------------------------------------------
-def get_regions(doc,asdict=True):
-    return get_asdict(doc,PICKER+'board.Region', 'name', asdict)
+class SelectionHighlighters(MapElement):
+    TAG=Element.MAP+'SelectionHighlighters'
+    def __init__(self,map,node=None,**kwargs):
+        super(SelectionHighlighters,self).\
+            __init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_squarenumbering(doc,one=True):
-    return get_asone(doc,PICKER+'board.mapgrid.SquareGridNumbering', one)
+class KeyBufferer(MapElement):
+    TAG=Element.MAP+'KeyBufferer'
+    def __init__(self,map,node=None,**kwargs):
+        super(KeyBufferer,self).__init__(map,self.TAG,node=node,**kwargs)
 
 # --------------------------------------------------------------------
-def get_pieces(doc,asdict=True):
-    return get_asdict(doc,WIDGET+'PieceSlot','entryName',asdict)
+class HighlightLastMoved(MapElement):
+    TAG=Element.MAP+'HighlightLastMoved'
+    def __init__(self,map,node=None,
+                 color     = rgb(255,0,0),
+                 enabled   = True,
+                 thickness = 2):
+        super(HighlightLastMoved,self).__init__(map,self.TAG,node=node,
+                                                color     = color,
+                                                enabled   = enabled,
+                                                thickness = thickness)
 
 # --------------------------------------------------------------------
-def get_prototypecontainer(doc,one=True):
-    return get_asone(doc,MODULE+'PrototypesContainer',one)
+class CounterDetailViewer(MapElement):
+    TAG=Element.MAP+'CounterDetailViewer'
+    TOP_LAYER  = 'from top-most layer only'
+    ALL_LAYERS = 'from all layers'
+    INC_LAYERS = 'from listed layers only'
+    EXC_LAYERS = 'from layers other than those listed'
+    FILTER = 'by using a property filter'
+    def __init__(self,map,node=None,
+                 borderWidth          = 0,
+                 centerAll            = False,
+                 centerText           = False,
+                 combineCounterSummary = False,
+                 counterReportFormat  = '',
+                 delay                = 700,
+                 description          = '',
+                 display              = TOP_LAYER,
+                 emptyHexReportForma  = '$LocationName$',
+                 enableHTML           = True,
+                 extraTextPadding     = 0,
+                 fgColor              = rgb(0,0,0),
+                 fontSize             = 9,
+                 graphicsZoom         = 1.0,
+                 hotkey               = key('\n',0),
+                 layerList            = '',
+                 minDisplayPieces     = 2,
+                 propertyFilter       = '',
+                 showDeck             = False,
+                 showDeckDepth        = 1,
+                 showDeckMasked       = False,
+                 showMoveSelectde     = False,
+                 showNoStack          = False,
+                 showNonMovable       = False,
+                 showOverlap          = False,
+                 showgraph            = True,
+                 showgraphsingle      = False,
+                 showtext             = False,
+                 showtextsingle       = False,
+                 stretchWidthSummary  = False,
+                 summaryReportFormat  = '$LocationName$',
+                 unrotatePieces       = False,
+                 version              = 3,
+                 verticalOffset       = 0,
+                 verticalTopText      = 5,
+                 zoomlevel            = 1.0): # showTerrain attributes
+        super(CounterDetailViewer,self)\
+            .__init__(map,self.TAG,node=node,
+                      borderWidth           = borderWidth,
+                      centerAll             = centerAll,
+                      centerText            = centerText,
+                      combineCounterSummary = combineCounterSummary,
+                      counterReportFormat   = counterReportFormat,
+                      delay                 = delay,
+                      description           = description,
+                      display               = display,
+                      emptyHexReportForma   = emptyHexReportForma,
+                      enableHTML            = enableHTML,
+                      extraTextPadding      = extraTextPadding,
+                      fgColor               = fgColor,
+                      fontSize              = fontSize,
+                      graphicsZoom          = graphicsZoom,
+                      hotkey                = hotkey,
+                      layerList             = layerList,
+                      minDisplayPieces      = minDisplayPieces,
+                      propertyFilter        = propertyFilter,
+                      showDeck              = showDeck,
+                      showDeckDepth         = showDeckDepth,
+                      showDeckMasked        = showDeckMasked,
+                      showMoveSelectde      = showMoveSelectde,
+                      showNoStack           = showNoStack,
+                      showNonMovable        = showNonMovable,
+                      showOverlap           = showOverlap,
+                      showgraph             = showgraph,
+                      showgraphsingle       = showgraphsingle,
+                      showtext              = showtext,
+                      showtextsingle        = showtextsingle,
+                      stretchWidthSummary   = stretchWidthSummary,
+                      summaryReportFormat   = summaryReportFormat,
+                      unrotatePieces        = unrotatePieces,
+                      version               = version,
+                      verticalOffset        = verticalOffset,
+                      verticalTopText       = verticalTopText,
+                      zoomlevel             = zoomlevel)
 
 # --------------------------------------------------------------------
-def get_prototypes(doc,asdict=True):
-    return get_asdict(doc,MODULE+'PrototypeDefinition', 'name',asdict)
+class GlobalMap(MapElement):
+    TAG=Element.MAP+'GlobalMap'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 color                = rgb(255,0,0),
+                 hotkey               = key('O',CTRL_SHIFT),
+                 icon                 = '/images/overview.gif',
+                 scale                = 0.2,
+                 tooltip              = 'Show/Hide overview window'):
+        super(GlobalMap,self).\
+            __init__(map,self.TAG,node=node,
+                     buttonText           = buttonText,
+                     color                = color,
+                     hotkey               = hotkey,
+                     icon                 = icon,
+                     scale                = scale,
+                     tooltip              = 'Show/Hide overview window')
 
 # --------------------------------------------------------------------
-def get_masskey(map,asdict=True):
-    return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+class Zoomer(MapElement):
+    TAG = Element.MAP+'Zoomer'
+    def __init__(self,map,node=None,
+                 inButtonText         = '',
+                 inIconName           = '/images/zoomIn.gif',
+                 inTooltip            = 'Zoom in',
+                 outButtonText        = '',
+                 outIconName          = '/images/zoomOut.gif',
+                 outTooltip           = 'Zoom out',
+                 pickButtonText       = '',
+                 pickIconName         = '/images/zoom.png',
+                 pickTooltip          = 'Select Zoom',
+                 zoomInKey            = key('=',CTRL_SHIFT),
+                 zoomLevels           = [0.2,0.25,0.333,0.4,0.5,
+                                         0.555,0.625,0.75,1.0,1.25,1.6],
+                 zoomOutKey           = key('-'),
+                 zoomPickKey          = key('='),
+                 zoomStart            = 3):
+        '''Zoom start is counting from the back (with default zoom levels,
+        and zoom start, the default zoom is 1'''
+        lvls = ','.join([str(z) for z in zoomLevels])
+        super(Zoomer,self).\
+            __init__(map,self.TAG,node=node,
+                     inButtonText         = inButtonText,
+                     inIconName           = inIconName,
+                     inTooltip            = inTooltip,
+                     outButtonText        = outButtonText,
+                     outIconName          = outIconName,
+                     outTooltip           = outTooltip,
+                     pickButtonText       = pickButtonText,
+                     pickIconName         = pickIconName,
+                     pickTooltip          = pickTooltip,
+                     zoomInKey            = zoomInKey,
+                     zoomLevels           = lvls,
+                     zoomOutKey           = zoomOutKey,
+                     zoomPickKey          = zoomPickKey,
+                     zoomStart            = zoomStart)
 
 # --------------------------------------------------------------------
-def get_masskey(map,asdict=True):
-    return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+class HidePiecesButton(MapElement):
+    TAG=Element.MAP+'HidePiecesButton'
+    def __init__(self,map,node=None,
+                 buttonText           = '',
+                 hiddenIcon           = '/images/globe_selected.gif',
+                 hotkey               = key('O'),
+                 showingIcon          = '/images/globe_unselected.gif',
+                 tooltip              = 'Hide all pieces on this map'):
+        super(HidePiecesButton,self).\
+            __init__(map,self.TAG,node=node,
+                     buttonText           = buttonText,
+                     hiddenIcon           = hiddenIcon,
+                     hotkey               = hotkey,
+                     showingIcon          = showingIcon,
+                     tooltip              = tooltip)
+        
+# --------------------------------------------------------------------
+class MassKey(GlobalKey,MapElementService):
+    TAG = Element.MAP+'MassKeyCommand'
+    def __init__(self,map,node=None,
+                 name                 = '',                
+                 buttonHotkey         = '',
+                 hotkey               = '',
+                 buttonText           = '',
+                 canDisable           = False,
+                 deckCount            = '-1',
+                 filter               = '',
+                 propertyGate         = '',
+                 reportFormat         = '',
+                 reportSingle         = False,
+                 singleMap            = True,
+                 target               = GlobalKey.SELECTED,
+                 tooltip              = '',
+                 icon                 = ''):
+        '''Default targets are selected units'''
+        super(MassKey,self).\
+            __init__(map,self.TAG,node=node,
+                     name                 = name,                
+                     buttonHotkey         = buttonHotkey, # This hot key
+                     hotkey               = hotkey,       # Target hot key
+                     buttonText           = buttonText,
+                     canDisable           = canDisable,
+                     deckCount            = deckCount,
+                     filter               = filter,
+                     propertyGate         = propertyGate,
+                     reportFormat         = reportFormat,
+                     reportSingle         = reportSingle,
+                     singleMap            = singleMap,
+                     target               = target,
+                     tooltip              = tooltip,
+                     icon                 = icon)
 
 
 # --------------------------------------------------------------------
-def get_piece_parts(code):
-    '''Takes the code part of a pieceslot (the text node) and decodes
-    it into traits.
+class Flare(MapElement):
+    TAG=Element.MAP+'Flare'
+    def __init__(self,map,node=None,
+                 circleColor          = rgb(255,0,0),
+                 circleScale          = True,
+                 circleSize           = 100,
+                 flareKey             = 'keyAlt',
+                 flareName            = 'Map Flare',
+                 flarePulses          = 6,
+                 flarePulsesPerSec    = 3,
+                 reportFormat         = ''):
+        super(Flare,self).__init__(map,self.TAG,node=node,
+                                   circleColor          = circleColor,
+                                   circleScale          = circleScale,
+                                   circleSize           = circleSize,
+                                   flareKey             = flareKey,
+                                   flareName            = flareName,
+                                   flarePulses          = flarePulses,
+                                   flarePulsesPerSec    = flarePulsesPerSec,
+                                   reportFormat         = '')
 
-    One can use this to modify the pieces.  One could for example
-    remove a trait by calling this function, and remove the element
-    corresponding to the trait from the returned list. Then one must
-    collect up the definitions and states separately and pass each of
-    them to `enc_parts', which gives us two strings that can be passed
-    to `add_piece' (or `add_proto') function to get the new text node
-    content of the piece slot or prototype definition.  For example
+# --------------------------------------------------------------------
+class AtStart(MapElement):
+    TAG = Element.MODULE+'map.SetupStack'
+    def __init__(self,map,node=None,
+                 name            = '',
+                 location        = '',
+                 useGridLocation = True,
+                 owningBoard     = '',
+                 x               = 0,
+                 y               = 0):
+        '''Pieces are existing PieceSlot elements'''
+        super(AtStart,self).\
+            __init__(map,self.TAG,node=node,
+                     name            = name,
+                     location        = location,
+                     owningBoard     = owningBoard,
+                     useGridLocation = useGridLocation,
+                     x               = x,
+                     y               = y)
 
-        piece  = get_pieceslots(root,pieces)['foo']
-        code   = piece.childNodes[0].nodeValue
-        traits = get_piece_parts(code)
-        ... # Modify the list
-    
-        # Separate out definitions and states 
-        defs   = [enc_def(e['def'])   for e in traits]
-        states = [enc_def(e['state']) for e in traits]
+    def addPieces(self,*pieces):
+        '''Add a `Pieces` element to this
 
-        # Encode definitions and states as a single string, and then form body 
-        def    = enc_parts(*defs)
-        state  = enc_parts(*states)
-        body   = add_piece(def,state)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Pieces
+            The added element
+        '''
+        # copy pieces here
+        copies = []
+        for p in pieces:
+            c = self.addPiece(p)
+            if c is not None:
+                copies.append(c)
+        return copies
+        
+    def addPiece(self,piece):
+        '''Add a `Piece` element to this
 
-        # Set new definition 
-        piece.childNodes[0].nodeValue = body 
-        
-    See also dicts_to_piece
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Piece
+            The added element
+        '''
+        if not isinstance(piece,PieceSlot):
+            print(f'Trying to add {type(piece)} to AtStart')
+            return None
+            
+        p = piece.clone(self)
+        # self._node.appendChild(p._node)
+        return p
     
-    Parameters
-    ----------
-    code : str
-        Text node content of <PieceSlot> or <PrototypeDefinition> tags
-    
-    Returns
-    -------
-    traits : list of dict
-        All the traits in a list.  Each element of the list is a
-        dictionary with the keys 'def' and 'status'.
+    def getPieces(self,asdict=True):
+        '''Get all Piece element(s) from this
 
-        The key 'def' points to a list of the trait attributes used to
-        define the trait type.  The first element of the 'def' list is
-        the trait identifier.  The rest are the arguments for that
-        trait type.  Note, all fields are encoded as strings. 
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Piece`
+            elements.  If `False`, return a list of all Piece`
+            children.
 
-        The key 'status' encodes the status of the trait.  The format
-        of this depends on the trait.Note, all fields are encoded as
-        strings.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Piece` children
 
-    '''
-    cmd,iden,typ,sta = code.split('/')
-    defs = typ.split(r'	')
-    stas = sta.split(r'	')
-    defs = [d.rstrip("\\").split(';') for d in defs]
-    stas = [s.rstrip("\\").split(';') for s in stas]
-    ret  = [{'def': d, 'state': s} for d,s in zip(defs,stas)]
-    return ret
+        '''
+        return self.getElementsWithKey(PieceSlot,'entryName',asdict)
 
+#
+# EOF
+#
+# ====================================================================
+# From globalproperty.py
+
 # --------------------------------------------------------------------
-def dicts_to_piece(traits):
-    # Separate out definitions and states 
-    defs   = [enc_def(e['def'])   for e in traits]
-    states = [enc_def(e['state']) for e in traits]
+class GlobalProperties(Element):
+    TAG = Element.MODULE+'properties.GlobalProperties'
+    def __init__(self,elem,node=None,**named):
+        super(GlobalProperties,self).__init__(elem,self.TAG,node=node)
+        
+        for n, p in named:
+            self.addProperty(n, **p)
 
-    # Encode definitions and states as a single string, and then form body 
-    df     = enc_parts(*defs)
-    state  = enc_parts(*states)
-    body   = add_piece(df,state)
+    def getGame(self):
+        return self.getParent(Game)
+    def addProperty(self,**kwargs):
+        '''Add a `Property` element to this
 
-    return body
-    
-    
-# ====================================================================
-# Key encoding
-SHIFT = 65
-CTRL = 130
-ALT   = 520
-CTRL_SHIFT = CTRL+SHIFT
-ALT_SHIFT = ALT+SHIFT
-NONE = '\ue004'
-NONE_MOD = 0
-def key(let,mod=CTRL):
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Property
+            The added element
+        '''
+        return GlobalProperty(self,node=None,**kwargs)
+    def getProperties(self):
+        return getElementsByKey(GlobalProperty,'name')
+        
 
-    '''Encode a key sequence
+# --------------------------------------------------------------------
+class GlobalProperty(Element):
+    TAG = Element.MODULE+'properties.GlobalProperty'
+    def __init__(self,elem,node=None,
+                 name         = '',
+                 initialValue = '',
+                 isNumeric    = False,
+                 min          = "null",
+                 max          = "null",
+                 wrap         = False,
+                 description  = ""):
+        super(GlobalProperty,self).__init__(elem,self.TAG,
+                                            node         = node,
+                                            name         = name,
+                                            initialValue = initialValue,
+                                            isNumeric    = isNumeric,
+                                            min          = min,
+                                            max          = max,
+                                            wrap         = wrap,
+                                            description  = description)
 
-    Parameters
-    ----------
-    let : str
-        Key code (Letter)
-    mod : int
-        Modifier mask
-    '''
-    return f'{ord(let)},{mod}'
-# Colour encoding 
-def rgb(r,g,b):
-    return ','.join([str(r),str(g),str(b)])
+    def getGlobalProperties(self):
+        return self.getParent(GlobalProperties)
 
-def rgba(r,g,b,a):
-    return ','.join([str(r),str(g),str(b),str(a)])
 
+#
+# EOF
+#
+# ====================================================================
+# From turn.py
 
 # --------------------------------------------------------------------
-def set_node_attr(node,**attr):
-    for k,v in attr.items():
-        node.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
-    
-# --------------------------------------------------------------------
-def add_node(root,parent,tag,**attr):
-    e = root.createElement(tag)
-    if parent: parent.appendChild(e)
-    set_node_attr(e,**attr)
+class TurnLevel(Element):
+    def __init__(self,elem,tag,node=None,**kwargs):
+        super(TurnLevel,self).__init__(elem,tag,node=node,**kwargs)
 
-    return e
+    def addLevel(self,counter=None,phases=None):
+        '''Add a `Level` element to this
 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Level
+            The added element
+        '''
+        if counter is None and phases is None:
+            return self
+        
+        t = TurnCounter if counter is not None else TurnList
+        o = counter     if counter is not None else phases
 
-# --------------------------------------------------------------------
-def add_text(root,parent,text):
-    t = root.createTextNode(text)
-    parent.appendChild(t)
-    return t
+        subcounter = o.pop('counter',None)
+        subphases  = o.pop('phases',None)
 
-# --------------------------------------------------------------------
-def add_game(root,doc,
-             name,
-             version, 
-             ModuleOther1    = "",
-             ModuleOther2    = "",
-             VassalVersion   = "3.6.7",
-             description     = "",
-             nextPieceSlotId = 0):
-    return add_node(root,doc,BUILD+'GameModule',
-                    name            = name,
-                    version         = version,
-                    ModuleOther1    = ModuleOther1,
-                    ModuleOther2    = ModuleOther2,
-                    VassalVersion   = VassalVersion,
-                    description     = description,
-                    nextPieceSlotId = nextPieceSlotId)
+        s = t(self,node=None,**o)
 
-# --------------------------------------------------------------------
-def add_basiccommandencoder(root,doc):
-    return add_node(root,doc,MODULE+'BasicCommandEncoder')
+        return s.addLevel(subcounter, subphases)
 
-# --------------------------------------------------------------------
-def add_globalproperties(root,elem,*props,**named):
-    gp = add_node(root,elem,MODULE+'properties.GlobalProperties')
-    # Add elements where each is a dict with _at least_
-    # "{'name':NAME, 'initialValue':VALUE}"
-    for p in props:
-        add_globalproperty(root,gp,**p)
-    # Add elements where each is a named dict with _at least_
-    # "{'initialValue':VALUE}"
-    for n, p in named:
-        add_globalproperty(root,gp,name=n,**p)
+    def getUp(self):
+        return self.getParent(TurnLevel)
+    def addCounter(self,**kwargs):
+        '''Add a `Counter` element to this
 
-    return gp
-        
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Counter
+            The added element
+        '''
+        return self.add(self,TurnCounter,**kwargs)
+    def addList(self,**kwargs):
+        '''Add a `List` element to this
 
-# --------------------------------------------------------------------
-def add_globalproperty(root,elem,name,initialValue,
-                       isNumeric   = False,
-                       min         = "null",
-                       max         = "null",
-                       wrap        = False,
-                       description = ""):
-    return add_node(root,elem,MODULE+'properties.GlobalProperty',
-                    name         = name,
-                    initialValue = initialValue,
-                    isNumeric    = isNumeric,
-                    min          = min,
-                    max          = max,
-                    wrap         = wrap,
-                    description  = description)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : List
+            The added element
+        '''
+        return self.add(self,TurnList,**kwargs)
+    def getCounter(self):
+        return self.getAllElements(TurnCounter)
+    def getList(self):
+        return self.getAllElements(TurnList)
 
 # --------------------------------------------------------------------
-def add_turntrack(root, elem, name,
-                  buttonText       = 'Turn',
-                  hotkey           = '',
-                  icon             = '',
-                  length           = -1,
-                  lengthStyle      = 'Maximum',
-                  nexthotkey       = key('T',ALT),
-                  plusButtonSize   = 22,
-                  prevhotkey       = key('T',ALT_SHIFT),
-                  reportFormat     = '<$PlayerId$> Turn updated from $oldTurn$ to $newTurn$',
-                  turnButtonHeight = 22,
-                  turnFormat       = None,
-                  counter          = None,
-                  phases           = None):
-
-    levels = (counter if counter is not None else
-              phases if phases is not None else None)
-    if levels is not None:
-        lvl = 1
-        lvls = [f'$level{lvl}$']
-        sub  = levels
-        while True:
-            sub = sub.get('counter',sub.get('phases',None))
-            if sub is None:
-                break
-            lvl += 1
-            lvls.append(f'$level{lvl}$')
+class TurnTrack(TurnLevel):
+    TAG = Element.MODULE+'turn.TurnTracker'
+    def __init__(self,elem,node=None,
+                 name             = '',
+                 buttonText       = 'Turn',
+                 hotkey           = '',
+                 icon             = '',
+                 length           = -1,
+                 lengthStyle      = 'Maximum',
+                 nexthotkey       = key('T',ALT),
+                 plusButtonSize   = 22,
+                 prevhotkey       = key('T',ALT_SHIFT),
+                 reportFormat     = 'Turn updated from $oldTurn$ to $newTurn$',
+                 turnButtonHeight = 22,
+                 fwdOnly          = True,
+                 turnFormat       = None,
+                 counter          = None,
+                 phases           = None):
+        levels = (counter if counter is not None else
+                  phases if phases is not None else None)
+        if levels is not None:
+            lvl = 1
+            lvls = [f'$level{lvl}$']
+            sub  = levels
+            while True:
+                sub = sub.get('counter',sub.get('phases',None))
+                if sub is None:
+                    break
+                lvl += 1
+                lvls.append(f'$level{lvl}$')
             
-        turnFormat = ' '.join(lvls)
+            turnFormat = ' '.join(lvls)
         
-    if turnFormat is None:
-        turnFormat = '$level1$ $level2$ $level3$ $level4$'        
+        if turnFormat is None:
+            turnFormat = '$level1$ $level2$ $level3$ $level4$'        
         
-    t = add_node(root, elem, MODULE+'turn.TurnTracker',
-                 name             = name,
-                 buttonText       = buttonText,
-                 hotkey           = hotkey,
-                 icon             = icon,
-                 length           = length,
-                 lengthStyle      = lengthStyle,
-                 nexthotkey       = nexthotkey,
-                 plusButtonSize   = plusButtonSize,
-                 prevhotkey       = prevhotkey,
-                 reportFormat     = reportFormat,
-                 turnButtonHeight = turnButtonHeight,
-                 turnFormat       = turnFormat)
+        super(TurnTrack,self).__init__(elem, self.TAG,
+                                       node             = node,
+                                       name             = name,
+                                       buttonText       = buttonText,
+                                       hotkey           = hotkey,
+                                       icon             = icon,
+                                       length           = length,
+                                       lengthStyle      = lengthStyle,
+                                       nexthotkey       = nexthotkey,
+                                       plusButtonSize   = plusButtonSize,
+                                       prevhotkey       = prevhotkey,
+                                       reportFormat     = reportFormat,
+                                       turnButtonHeight = turnButtonHeight,
+                                       turnFormat       = turnFormat)
 
-    add_turnlevel(root, t, counter=counter, phases=phases)
-    
-    return t
+        self.addLevel(counter=counter, phases=phases)
 
-# --------------------------------------------------------------------
-def add_turnlevel(root,parent,counter=None,phases=None):
-    f,s = ((add_turncounter,counter) if counter is not None else
-           (add_turnlist,phases)     if phases  is not None else (None,None))
-    if f is None: return parent
+    def getGame(self):
+        return self.getParent(Game)
+    def getLists(self,asdict=True):
+        '''Get all List element(s) from this
 
-    subcounter = s.pop('counter',None)
-    subphases  = s.pop('phases',None)
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `List`
+            elements.  If `False`, return a list of all List`
+            children.
+        
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `List` children
 
-    sub = f(root,parent,**s)
+        '''
+        return self.getElementsByKey(TurnList,'property',asdict=asdict)
+    def getCounters(self,asdict=True):
+        '''Get all Counter element(s) from this
 
-    return add_turnlevel(root,sub,counter=subcounter,phases=subphases)
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Counter`
+            elements.  If `False`, return a list of all Counter`
+            children.
 
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Counter` children
+
+        '''
+        return self.getElementsByKey(TurnCounter,'property',asdict=asdict)
+    def addHotkey(self,**kwargs):
+        '''Add a `Hotkey` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Hotkey
+            The added element
+        '''
+        return self.add(TurnGlobalHotkey,**kwargs)
+    def getHotkeys(self,asdict):
+        return self.getElementsByKey(TurnGlobalHotkey,'name',asdict=asdict)
+    def encode(self):
+        ret = f'TURN{self["name"]}\t'
+        
+        return []
+
+        
 # --------------------------------------------------------------------
-def add_turncounter(root,elem,property,
-                    incr          = 1,
-                    loop          = False,
-                    loopLimit     = -1,
-                    turnFormat    = "$value$"):
-    c = add_node(root,elem,MODULE+"turn.CounterTurnLevel",
-                 property       = property,
-                 incr           = incr,
-                 loop           = loop,
-                 loopLimit      = loopLimit,
-                 turnFormat     = turnFormat)
-    return c
+class TurnCounter(TurnLevel):
+    TAG = Element.MODULE+"turn.CounterTurnLevel"
+    def __init__(self,elem,node=None,
+                 property      = '',
+                 start         = 1,
+                 incr          = 1,
+                 loop          = False,
+                 loopLimit     = -1,
+                 turnFormat    = "$value$"):
+        super(TurnCounter,self).__init__(elem,self.TAG,node=node,
+                                         property       = property,
+                                         start          = start,
+                                         incr           = incr,
+                                         loop           = loop,
+                                         loopLimit      = loopLimit,
+                                         turnFormat     = turnFormat)
                     
 # --------------------------------------------------------------------
-def add_turnlist(root,elem,property,names,
+class TurnList(TurnLevel):
+    TAG = Element.MODULE+"turn.ListTurnLevel"
+    def __init__(self,elem,node=None,
+                 property      = '',
+                 names         = [],
                  configFirst   = False,
                  configList    = False,
                  turnFormat    = '$value$'):
-    c = add_node(root,elem,MODULE+"turn.ListTurnLevel",
-                 property       = property,
-                 list           = ','.join([str(p) for p in names]),
-                 configFirst    = configFirst,
-                 configList     = configList,
-                 turnFormat     = turnFormat)
-    return c
+        super(TurnList,self).\
+            __init__(elem,self.TAG,node=node,
+                     property       = property,
+                     list           = ','.join([str(p) for p in names]),
+                     configFirst    = configFirst,
+                     configList     = configList,
+                     turnFormat     = turnFormat)
                   
 # --------------------------------------------------------------------
-def add_turnhotkey(root,elem,
-                   hotkey,
-                   match        = '{phase=="first"}',
-                   reportFormat = '{PlayerName}',
-                   name         = ''):
-    return add_node(root,elem,MODULE+'turn.TurnGlobalHotkey',
-                    hotkey       = hotkey,
-                    match        = match,
-                    reportFormat = reportFormat,
-                    name         = name)
+class TurnGlobalHotkey(Element):
+    TAG = Element.MODULE+'turn.TurnGlobalHotkey'
+    def __init__(self,elem,
+                 node         = None,
+                 hotkey       = '',
+                 match        = '{true}',
+                 reportFormat = '',
+                 name         = ''):
+        '''Global key activated by turn change
 
-                   
-                  
-# --------------------------------------------------------------------
-def add_translatable(root,elem):
-    return add_node(root,elem,MODULE+'properties.GlobalTranslatableMessages')
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        hotkey : str
+            What to send (global command)
+        match : str
+            When to send
+        reportFormat : str
+            What to what
+        name : str
+            A free form name
+        '''
+        super(TurnGlobalHotkey,self).__init__(elem,self.TAG,
+                                              node         = node,
+                                              hotkey       = hotkey,
+                                              match        = match,
+                                              reportFormat = reportFormat,
+                                              name         = name)
 
-# --------------------------------------------------------------------
-def add_language(root,elem):
-    return add_node(root,elem,'VASSAL.i18n.Language')
+    def getTurnTrack(self):
+        '''Get the turn track'''
+        return self.getParent(TurnTrack)
 
+#
+# EOF
+#
+# ====================================================================
+# From documentation.py
+
+# ====================================================================
+def createKeyHelp(*args,**kwargs):
+    '''Creates a help file with key-bindings
+
+    See Documentation.createKeyHelp
+    '''
+    return Documentation.createKeyHelp(*args,**kwargs)
+
 # --------------------------------------------------------------------
-def add_chatter(root,elem):
-    return add_node(root,elem,MODULE+'Chatter')
+class Documentation(GameElement):
+    TAG=Element.MODULE+'Documentation'
+    def __init__(self,doc,node=None,**kwargs):
+        '''Documentation (or help menu)
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        kwargs : dict
+            Attributes
+        '''
+        super(Documentation,self).__init__(doc,self.TAG,node=node,**kwargs)
+
+    def addAboutScreen(self,**kwargs):
+        '''Add a `AboutScreen` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : AboutScreen
+            The added element
+        '''
+        return self.add(AboutScreen,**kwargs)
+    def addHelpFile(self,**kwargs):
+        '''Add a `HelpFile` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HelpFile
+            The added element
+        '''
+        return self.add(HelpFile,**kwargs)
+    def addBrowserHelpFile(self,**kwargs):
+        '''Add a `BrowserHelpFile` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BrowserHelpFile
+            The added element
+        '''
+        return self.add(HelpBrowserFile,**kwargs)
+    def addBrowserPDFFile(self,**kwargs):
+        '''Add a `BrowserPDFFile` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BrowserPDFFile
+            The added element
+        '''
+        return self.add(BrowserPDFFile,**kwargs)
+    def addTutorial(self,**kwargs):
+        '''Add a `Tutorial` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Tutorial
+            The added element
+        '''
+        return self.add(Tutorial,**kwargs)
+    def getAboutScreens(self):
+        return self.getElementsByKey(AboutScreen,'title')
+    def getHelpFiles(self):
+        return self.getElementsByKey(HelpFile,'title')
+    def getBrowserHelpFiles(self):
+        return self.getElementsByKey(BrowserHelpFile,'title')
+    def getBrowserPDFFiles(self):
+        return self.getElementsByKey(BrowserPDFFile,'title')
+    def getTutorials(self):
+        return self.getElementsByKey(Tutorial,'name')
+
+    @classmethod
+    def createKeyHelp(cls,keys,title='',version=''):
+        '''Creates a help file with key-bindings
+        
+        Parameters
+        ----------
+        keys : list of list of str
+             List of key-binding documentation
+        title : str
+             Title of help file
+        version : str
+             Version number
+        
+        Returns
+        -------
+        txt : str
+            File content
+        '''
+        txt = f'''
+        <html>
+         <body>
+          <h1>{title} (Version {version}) Key bindings</h1>
+          <table>
+          <tr><th>Key</th><th>Where</th><th>Effect</th></tr>'''
+        
+        for key, where, description in keys:
+            txt += (f'<tr><td>{key}</td>'
+                    f'<td>{where}</td>'
+                    f'<td>{description}</td></tr>')
+        
+        txt += '''
+          </table>
+         </body>
+        </html>'''
+        
+        return txt 
+
+
 # --------------------------------------------------------------------
-def add_keynamer(root,elem):
-    return add_node(root,elem,MODULE+'KeyNamer')
+class AboutScreen(Element):
+    TAG = Element.MODULE+'documentation.AboutScreen'
+    def __init__(self,doc,node=None,title='',fileName=""):
+        '''Create an about screen element that shows image
 
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        fileName : str
+            Internal file name        
+        '''
+        super(AboutScreen,self).__init__(doc,
+                                         self.TAG,
+                                         node     = node,
+                                         fileName = fileName,
+                                         title    = title)
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+
 # --------------------------------------------------------------------
-def add_documentation(root,doc):
-    return add_node(root,doc,MODULE+'Documentation')
+class BrowserPDFFile(Element):
+    TAG = Element.MODULE+'documentation.BrowserPDFFile'
+    def __init__(self,doc,node=None,title='',pdfFile=''):
+        '''Create help menu item that opens an embedded PDF
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        pdfFile : str
+            Internal file name
+        '''
+        super(BrowserPDFFile,self).__init__(doc,self.TAG,
+                                            node    = node,
+                                            pdfFile = pdfFile,
+                                            title   = title)
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
 
 # --------------------------------------------------------------------
-def add_about(root,doc,title,fileName=""):
-    return add_node(root,doc,MODULE+'documentation.AboutScreen',
-                    fileName = fileName,
-                    title = title)
-# --------------------------------------------------------------------
-def add_pdffile(root,doc,title,pdfFile):
-    return add_node(root,doc,MODULE+'documentation.BrowserPDFFile',
-                    pdfFile = pdfFile, title=title)
-# --------------------------------------------------------------------
-def add_helpfile(root,doc,title,fileName,fileType="archive"):
-    return add_node(root,doc,MODULE+'documentation.HelpFile',
-                    fileName = fileName,
-                    fileType = fileType,
-                    title    = title)
+class HelpFile(Element):
+    TAG = Element.MODULE+'documentation.HelpFile'
+    ARCHIVE = 'archive'
+    def __init__(self,doc,node=None,
+                 title='',
+                 fileName='',
+                 fileType=ARCHIVE):
+        '''Create a help menu entry that opens an embeddded file
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        fileName : str
+            Internal file name
+        fileType : str
+            How to find the file 
+        '''
+        super(HelpFile,self).__init__(doc,self.TAG,node=node,
+                                      fileName = fileName,
+                                      fileType = fileType,
+                                      title    = title)
 
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+
 # --------------------------------------------------------------------
-def add_htmlfile(root,doc,title,startingPage='index.html'):
-    return add_node(root,doc,MODULE+'documentation.BrowserHelpFile',
-                    startingPage=startingPage,
-                    title=title)
+class BrowserHelpFile(Element):
+    TAG = Element.MODULE+'documentation.BrowserHelpFile'
+    def __init__(self,doc,node=None,
+                 title='',
+                 startingPage='index.html'):
+        '''Create a help menu entry that opens an embeddded HTML
+        page (with possible sub-pages) file
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        title : str
+            Entry title
+        startingPage : str
+            which file to start at
+        '''
+        super(BrowserHelpFile,self).__init__(doc,self.TAG,node=node,
+                                             startingPage=startingPage,
+                                             title=title)
 
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+        
 # --------------------------------------------------------------------
-def add_tutorial(root,doc,
+class Tutorial(Element):
+    TAG = Element.MODULE+'documentation.Tutorial'
+    def __init__(self,doc,node=None,
                  name            = 'Tutorial',
                  logfile         = 'tutorial.vlog',
                  promptMessage   = 'Load the tutorial?',
                  welcomeMessage  = 'Press "Step forward" (PnDn) to step through the tutorial',
                  launchOnStartup = True):
-    return add_node(root,doc,MODULE+'documentation.Tutorial',
-                    name            = name,
-                    logfile         = logfile,
-                    promptMessage   = promptMessage,
-                    welcomeMessage  = welcomeMessage,
-                    launchOnStartup = launchOnStartup)
+        '''Add a help menu item that loads the tutorial
 
+        Also adds the start-up option to run the tutorial
+
+
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        name : str
+            Name of entry
+        logfile : str
+            Internal file name
+        promptMessage : str
+            What to show
+        launchOnStartup : bool
+            By default, launch tutorial first time running module
+        '''
+        super(Tutorial,self).__init__(doc,self.TAG,node=node,
+                                      name            = name,
+                                      logfile         = logfile,
+                                      promptMessage   = promptMessage,
+                                      welcomeMessage  = welcomeMessage,
+                                      launchOnStartup = launchOnStartup)
+
+    def getDocumentation(self):
+        '''Get Parent element'''
+        return self.getParent(Documentation)
+
+
+#
+# EOF
+#
+# ====================================================================
+# From player.py
+
 # --------------------------------------------------------------------
-def add_roster(root,doc,buttonKeyStroke='',
+class PlayerRoster(GameElement):
+    TAG = Element.MODULE+'PlayerRoster'
+    def __init__(self,doc,node=None,buttonKeyStroke='',
                buttonText='Retire',
                buttonToolTip='Switch sides, become observer, or release faction'):
-    return add_node(root,doc,MODULE+'PlayerRoster',
-                    buttonKeyStroke = buttonKeyStroke,
-                    buttonText      = buttonText,
-                    buttonToolTip   = buttonToolTip)
+        '''Add a player roster to the module
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        buttonText : str
+            Text on button
+        buttonTooltip : str
+            Tool tip to show when hovering over button
+        '''
+        super(PlayerRoster,self).__init__(doc,self.TAG,node=node,
+                                          buttonKeyStroke = buttonKeyStroke,
+                                          buttonText      = buttonText,
+                                          buttonToolTip   = buttonToolTip)
+    def addSide(self,name):
+        '''Add a `Side` element to this
 
-# --------------------------------------------------------------------
-def add_entry(root,doc,text):
-    n = add_node(root,doc,'entry')
-    add_text(root,n,text)
-    return n
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Side
+            The added element
+        '''
+        return self.add(PlayerSide,name=name)
+    def getSides(self):
+        '''Get all sides'''
+        return self.getAllElements(PlayerSide,False)
+    def encode(self):
+        '''Encode for save'''
+        return ['PLAYER\ta\ta\t<observer>']
 
 # --------------------------------------------------------------------
-def add_globaloptions(root,doc,
-                      autoReport               = "Use Preferences Setting",
-                      centerOnMove             = "Use Preferences Setting",
-                      chatterHTMLSupport       = "Always",
-                      hotKeysOnClosedWindows   = "Never",
-                      inventoryForAll          = "Always",
-                      nonOwnerUnmaskable       = "Use Preferences Setting",
-                      playerIdFormat           = "$playerName$",
-                      promptString             = "Opponents can unmask pieces",
-                      sendToLocationMoveTrails = "Never"):
-    return add_node(root,doc,MODULE+'GlobalOptions',
-                      autoReport               = autoReport,
-                      centerOnMove             = centerOnMove,
-                      chatterHTMLSupport       = chatterHTMLSupport,
-                      hotKeysOnClosedWindows   = hotKeysOnClosedWindows,
-                      inventoryForAll          = inventoryForAll,
-                      nonOwnerUnmaskable       = nonOwnerUnmaskable,
-                      playerIdFormat           = playerIdFormat,
-                      promptString             = promptString,
-                      sendToLocationMoveTrails = sendToLocationMoveTrails)
+class PlayerSide(Element):
+    TAG = 'entry'
+    def __init__(self,doc,node=None,name=''):
+        '''Adds a side to the player roster
 
-# --------------------------------------------------------------------
-def add_option(root,doc,name,value):
-    n = add_node(root,doc,'option',name=name)
-    add_text(n,root,value)
-    return n
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        name : str
+            Name of side 
+        '''
+        super(PlayerSide,self).__init__(doc,self.TAG,node=node)
+        if node is None:
+            self.addText(name)
 
-# --------------------------------------------------------------------
-# CurrentMap == "Board"
-def add_inventory(root,doc,
-                  canDisable          = False,
-                  centerOnPiece       = True,
-                  disabledIcon        = '',
-                  drawPieces          = True,
-                  foldersOnly         = False,
-                  forwardKeystroke    = True,
-                  groupBy             = '',
-                  hotkey              = key('I',ALT),
-                  icon                = '/images/inventory.gif',
-                  include             = '{}',
-                  launchFunction      = 'functionHide',
-                  leafFormat          = '$PieceName$',
-                  name                = '',
-                  nonLeafFormat       = '$PropertyValue$',
-                  pieceZoom           = '0.33',
-                  pieceZoom2          = '0.5',
-                  pieceZoom3          = '0.6',
-                  propertyGate        = '',
-                  refreshHotkey       = key('I',ALT_SHIFT),
-                  showMenu            = True,
-                  sides               = '',
-                  sortFormat          = '$PieceName$',
-                  sortPieces          = True,
-                  sorting             = 'alpha',
-                  text                = '',
-                  tooltip             = 'Show inventory of all pieces',
-                  zoomOn              = False):
-    return add_node(root,doc,MODULE+'Inventory',
-                    canDisable          = canDisable,
-                    centerOnPiece       = centerOnPiece,
-                    disabledIcon        = disabledIcon,
-                    drawPieces          = drawPieces,
-                    foldersOnly         = foldersOnly,
-                    forwardKeystroke    = forwardKeystroke,
-                    groupBy             = groupBy,
-                    hotkey              = hotkey,
-                    icon                = icon,
-                    include             = include,
-                    launchFunction      = launchFunction,
-                    leafFormat          = leafFormat,
-                    name                = name,
-                    nonLeafFormat       = nonLeafFormat,
-                    pieceZoom           = pieceZoom,
-                    pieceZoom2          = pieceZoom2,
-                    pieceZoom3          = pieceZoom3,
-                    propertyGate        = propertyGate,
-                    refreshHotkey       = refreshHotkey,
-                    showMenu            = showMenu,
-                    sides               = sides,
-                    sortFormat          = sortFormat,
-                    sortPieces          = sortPieces,
-                    sorting             = sorting,
-                    text                = text,
-                    tooltip             = tooltip,
-                    zoomOn              = zoomOn)
+    def getPlayerRoster(self):
+        '''Get Parent element'''
+        return self.getParent(PlayerRoster)
+
+
+#
+# EOF
+#
+# ====================================================================
+# From chessclock.py
+
+# ====================================================================
+class ChessClock(Element):
+    TAG=Element.MODULE+'chessclockcontrol.ChessClock'
+    def __init__(self,
+                 doc,
+                 node                   = None,
+                 icon                   = '',
+                 description            = '',
+                 side                   = '',
+                 tooltip                = 'Individual clock control',
+                 buttonText             = '',
+                 startHotkey            = '',
+                 stopHotkey             = '',
+                 tickingBackgroundColor = rgb(255,255,0),
+                 tickingFontColor       = rgb(0,0,0),
+                 tockingFontColor       = rgb(51,51,51)):
+        '''Individual clock for a side
+
+        When the clock is running, the background colour may be
+        changed, and the colour of the numbers alternate between
+        `tickingFontColor` and `tockingFontColor`.
+        
+        Parameters
+        ----------
+        doc : Element
+            Parent element 
+        node : xml.dom.Element 
+            Read from this node
+        icon : str
+            File name of button icon
+        description : str
+            Note on this clock
+        side : str
+            Name of side this clock belongs to
+        tooltop : str
+            Hover help text
+        buttonText : str
+            Text on button
+        startHotkey : str (key code)
+            Key or command to start timer
+        stopHotkey : str (key code)
+            Key or command to stop timer
+        tickingBackgroundColor : str (color)
+            Background color of time display when clock is running
+        tickingFontColor : str (color)
+            First color of numbers in display when clock is running.
+        tockingFontColor : str (color)
+            Second color of numbers in display when clock is running.
+        '''
+        super(ChessClock,self).__init__(#ChessClock
+                 doc,
+                 self.TAG,
+                 node                   = node,
+                 icon                   = icon,
+                 description            = description,
+                 side                   = side,
+                 tooltip                = tooltip,
+                 buttonText             = buttonText,
+                 startHotkey            = startHotkey,
+                 stopHotkey             = stopHotkey,
+                 tickingBackgroundColor = tickingBackgroundColor,
+                 tickingFontColor       = tickingFontColor,
+                 tockingFontColor       = tockingFontColor)
+            
+    def getControl(self):
+        '''Get Parent element'''
+        return self.getParent(ChessClockControl)
+                 
+# ====================================================================
+class ChessClockControl(GameElement):
+    TAG=Element.MODULE+'ChessClockControl'
+    ALWAYS = 'Always'
+    AUTO   = 'Auto'
+    NEVER  = 'Never'
+    def __init__(self,
+                 doc,
+                 node              = None,
+                 name              = 'Chess clock',
+                 description       = '',
+                 buttonIcon        = 'chess_clock.png',
+                 buttonText        = '',
+                 buttonTooltip     = 'Show/stop/hide chess clocks',
+                 showHotkey        = key('U',ALT),
+                 pauseHotkey       = key('U',CTRL_SHIFT),
+                 nextHotkey        = key('U'),
+                 startOpponentKey  = '',
+                 showTenths        = AUTO,
+                 showSeconds       = AUTO,
+                 showHours         = AUTO,
+                 showDays          = AUTO,
+                 allowReset        = False,
+                 addClocks         = True):
+        '''A set of chess clocs
+
+        Parameters
+        ----------
+        doc : Element
+            Parent
+        node : xml.dom.Element
+            Node to read state from
+        name : str
+            Name of clock control
+        description : str
+            Note on the chess clocks control
+        buttonIcon : str
+            Icon file name for button (chess_clock.png)
+        buttonText : str
+            Text on button 
+        buttonTooltip : str
+            Hower help
+        showHotkey : str (key code)
+            Show or hide interface hot key
+        nextHotkey : str (key code)
+            Start the next clock hot key
+        pauseHotkey : str (key code)
+            Pause all clocks hot key 
+        startOpponentKey : str (key code)
+            Start opponens clock 
+        showTenths : one of AUTO, ALWAYS, NEVER
+            Whether to show tenths of seconds
+        showSeconds : one of AUTO, ALWAYS, NEVER
+            Whether to show seconds in clock 
+        showHours : one of AUTO, ALWAYS, NEVER
+            Whether to show hours in clock
+        showDays : one of AUTO, ALWAYS, NEVER
+            Whether to show days in clock
+        allowReset : boolean
+            If true, allow manual reset of all clocks
+        '''
+        super(ChessClockControl,self).__init__(# ChessclockControl
+            doc,
+            self.TAG,
+            node              = node,
+            name              = name,
+            description       = description,
+            buttonIcon        = buttonIcon,
+            buttonText        = buttonText,
+            buttonTooltip     = buttonTooltip,
+            showHotkey        = showHotkey,
+            pauseHotkey       = pauseHotkey,
+            nextHotkey        = nextHotkey,
+            startOpponentKey  = startOpponentKey,
+            showTenths        = showTenths,
+            showSeconds       = showSeconds,
+            showHours         = showHours,
+            showDays          = showDays,
+            allowReset        = allowReset)
+        print(node,addClocks)
+        if node is not None or not addClocks:
+            return
+        
+        print('--- Will add clocks')
+        game   = self.getGame()
+        roster = game.getPlayerRoster()[0]
+        sides  = roster.getSides()
+        for side in sides:
+            name = side.getText()
+            self.addClock(side        = name,
+                          tooltip     = f'Clock for {name}',
+                          buttonText  = name,
+                          startHotkey = key('U'),
+                          stopHotkey  = key('U'))
     
-                  
+    def addClock(self,**kwargs):
+        '''Add a clock element to this
 
-# --------------------------------------------------------------------
-def add_map(root,doc,
-            mapName,
-            allowMultiple        = 'false',
-            backgroundcolor      = rgb(255,255,255),
-            buttonName           = '',
-            changeFormat         = '$message$',
-            color                = rgb(0,0,0),
-            createFormat         = '$pieceName$ created in $location$ *',
-            edgeHeight           = '0',
-            edgeWidth            = '0',
-            hideKey              = '',
-            hotkey               = key('M',ALT),
-            icon                 = '/images/map.gif',
-            launch               = 'false',
-            markMoved            = 'Always',
-            markUnmovedHotkey    = '',
-            markUnmovedIcon      = '/images/unmoved.gif',
-            markUnmovedReport    = '',
-            markUnmovedText      = '',
-            markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
-            moveKey              = '',
-            moveToFormat         = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
-            moveWithinFormat     = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
-            showKey              = '',
-            thickness            = '3',
-            cls                  = MODULE+'Map'):
-    return add_node(root,doc,cls,
-                    allowMultiple        = allowMultiple,
-                    backgroundcolor      = backgroundcolor,
-                    buttonName           = buttonName,
-                    changeFormat         = changeFormat,
-                    color                = color,
-                    createFormat         = createFormat,
-                    edgeHeight           = edgeHeight,
-                    edgeWidth            = edgeWidth,
-                    hideKey              = hideKey,
-                    hotkey               = hotkey,
-                    icon                 = icon,
-                    launch               = launch,
-                    mapName              = mapName,
-                    markMoved            = markMoved,
-                    markUnmovedHotkey    = markUnmovedHotkey,
-                    markUnmovedIcon      = markUnmovedIcon,
-                    markUnmovedReport    = markUnmovedReport,
-                    markUnmovedText      = markUnmovedText,
-                    markUnmovedTooltip   = markUnmovedTooltip,
-                    moveKey              = moveKey,
-                    moveToFormat         = moveToFormat,
-                    moveWithinFormat     = moveWithinFormat,
-                    showKey              = showKey,
-                    thickness            = thickness)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : AboutScreen
+            The added element
+        '''
+        return self.add(ChessClock,**kwargs)
+    def getClocks(self,asdict=True):
+        '''Return dictionary of clocs'''
+        return self.getElementsByKey(ChessClock,'side',asdict)
 
+
+#
+# EOF
+#
+# ====================================================================
+# From widget.py
+
 # --------------------------------------------------------------------
-def add_widgetmap(root,doc, mapName,**attr):
-    return add_map(root,doc,mapName,cls=WIDGET+'WidgetMap',**attr)
+class WidgetElement:
+    def __init__(self):
+        pass
 
+    def addTabs(self,**kwargs):
+        '''Add a `Tabs` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Tabs
+            The added element
+        '''
+        return self.add(TabWidget,**kwargs)
+    def addCombo(self,**kwargs):
+        '''Add a drop-down menu to this
+
+        Parameters
+        ----------
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Combo
+            The added element
+        '''
+        return self.add(ComboWidget,**kwargs)
+    def addPanel(self,**kwargs):
+        '''Add a `Panel` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Panel
+            The added element
+        '''
+        return self.add(PanelWidget,**kwargs)
+    def addList(self,**kwargs):
+        '''Add a `List` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : List
+            The added element
+        '''
+        return self.add(ListWidget,**kwargs)
+    def addMapWidget(self,**kwargs):
+        '''Add a `MapWidget` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MapWidget
+            The added element
+        '''
+        return self.add(MapWidget,**kwargs)
+    def addChart(self,**kwargs):
+        '''Add a `Chart` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chart
+            The added element
+        '''
+        return self.add(Chart,**kwargs)
+    def addPieceSlot(self,**kwargs):
+        '''Add a `PieceSlot` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PieceSlot
+            The added element
+        '''
+        return self.add(PieceSlot,**kwargs)
+    def addPiece(self,piece):
+        '''Add a `Piece` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Piece
+            The added element
+        '''
+        if not isinstance(piece,PieceSlot):
+            print(f'Trying to add {type(piece)} to ListWidget')
+            return None
+            
+        p = piece.clone(self)
+        return p
+    def getTabs(self,asdict=True):
+        '''Get all Tab element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Tab` elements.  If `False`, return a list of all Tab` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Tab` children
+        '''
+        return self.getElementsByKey(TabWidget,'entryName',asdict)
+    def getCombos(self,asdict=True):
+        '''Get all Combo element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Tab` elements.  If `False`, return a list of all Tab` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Tab` children
+        '''
+        return self.getElementsByKey(ComboWidget,'entryName',asdict)
+    def getLists(self,asdict=True):
+        '''Get all List element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `List` elements.  If `False`, return a list of all List` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `List` children
+        '''
+        return self.getElementsByKey(ListWidget,'entryName',asdict)
+    def getPanels(self,asdict=True):
+        '''Get all Panel element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Panel` elements.  If `False`, return a list of all Panel` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Panel` children
+        '''
+        return self.getElementsByKey(PanelWidget,'entryName',asdict)
+    def getMapWidgets(self,asdict=True):
+        '''Get all MapWidget element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `MapWidget` elements.  If `False`, return a list of all MapWidget` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `MapWidget` children
+        '''
+        return self.getElementsByKey(MapWidget,'entryName',asdict)
+    def getCharts(self,asdict=True):
+        '''Get all Chart element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Chart` elements.  If `False`, return a list of all Chart` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Chart` children
+        '''
+        return self.getElementsByKey(Chart,'chartName',asdict)
+    def getPieceSlots(self,asdict=True):
+        '''Get all PieceSlot element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `PieceSlot` elements.  If `False`, return a list of all PieceSlot` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `PieceSlot` children
+        '''
+        return self.getElementsByKey(PieceSlot,'entryName',asdict)
+
 # --------------------------------------------------------------------
-def add_boardpicker(root,doc,
-                    addColumnText        = 'Add column',
-                    addRowText           = 'Add row',
-                    boardPrompt          = 'Select board',
-                    slotHeight           = 125,
-                    slotScale            = 0.2,
-                    slotWidth            = 350,
-                    title                = 'Choose Boards'):
-    return add_node(root,doc,MAP+'BoardPicker',
-                    addColumnText        = addColumnText,
-                    addRowText           = addRowText,
-                    boardPrompt          = boardPrompt,
-                    slotHeight           = slotHeight,
-                    slotScale            = slotScale,
-                    slotWidth            = slotWidth,
-                    title                = title) 
+class PieceWindow(GameElement,WidgetElement):
+    TAG=Element.MODULE+'PieceWindow'
+    def __init__(self,elem,node=None,
+                 name         = '',
+                 defaultWidth = 0,
+                 hidden       = False,
+                 hotkey       = key('C',ALT),
+                 scale        = 1.,
+                 text         = '',
+                 tooltip      = 'Show/hide piece window',
+                 icon         = '/images/counter.gif'):
+        super(PieceWindow,self).__init__(elem,self.TAG,node=node,
+                                         name = name,
+                                         defaultWidth = defaultWidth,
+                                         hidden       = hidden,
+                                         hotkey       = hotkey,
+                                         scale        = scale,
+                                         text         = text,
+                                         tooltip      = tooltip,
+                                         icon         = icon)
 
 # --------------------------------------------------------------------
-def add_setup(root,picker,mapName,maxColumns,boardNames):
-    '''Add given boards in row-first up to maxColumns per row'''
-    s = add_node(root,picker,'setup')
-    col = 0
-    row = 0
-    lst = [f'{mapName}BoardPicker']
-    for bn in boardNames:
-        lst.extend([bn,str(col),str(row)])
-        col += 1
-        if col >= maxColumns:
-            col = 0
-            row += 1
-    txt = r'	'.join(lst)
-    add_text(root,s,txt)
-            
+class ComboWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'BoxWidget'
+    def __init__(self,elem,node=None,entryName='',width=0,height=0):
+        super(ComboWidget,self).__init__(elem,
+                                       self.TAG,
+                                       node = node,
+                                       entryName = entryName,
+                                       width     = width,
+                                       height    = height)
+        
 # --------------------------------------------------------------------
-def add_board(root,picker,name,
-              image      = '',
-              reversible = False,
-              color      = rgb(255,255,255),
-              width      = 0,
-              height     = 0):
-    return add_node(root,picker,PICKER+'Board',
-                    image      = image,
-                    name       = name,
-                    reversible = reversible,
-                    color      = color,
-                    width      = width,
-                    height     = height)
+class TabWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'TabWidget'
+    def __init__(self,elem,node=None,entryName=''):
+        super(TabWidget,self).__init__(elem,
+                                       self.TAG,
+                                       node      = node,
+                                       entryName = entryName)
 
 # --------------------------------------------------------------------
-def add_zoned(root,board):
-    return add_node(root,board,PICKER+'board.ZonedGrid')
+class ListWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'ListWidget'
+    def __init__(self,elem,node = None,
+                 entryName = '',
+                 height    = 0,
+                 width     = 300,
+                 scale     = 1.,
+                 divider   = 150):
+        super(ListWidget,self).__init__(elem,self.TAG,node=node,
+                                        entryName = entryName,
+                                        height    = height,
+                                        width     = width,
+                                        scale     = scale,
+                                        divider   = divider)
 
 # --------------------------------------------------------------------
-def add_zonehighlighter(root,zoned):
-    return add_node(root,zoned,PICKER+'board.mapgrid.ZonedGridHighlighter')
+class PanelWidget(Element,WidgetElement):
+    TAG=Element.WIDGET+'PanelWidget'
+    def __init__(self,elem,node=None,
+                 entryName = '',
+                 fixed     = False,
+                 nColumns  = 1,
+                 vert      = False):
+        super(PanelWidget,self).__init__(elem,self.TAG,node=node,
+                                         entryName = entryName,
+                                         fixed     = fixed,
+                                         nColumns  = nColumns,
+                                         vert      = vert)
 
 # --------------------------------------------------------------------
-def add_zone(root,zoned,
-             name              = "",
-             highlightProperty = "",
-             locationFormat    = "$gridLocation$",
-             path              = "0,0;976,0;976,976;0,976",
-             useHighlight      = False,
-             useParentGrid     = False):
-    return add_node(root,zoned,PICKER+'board.mapgrid.Zone',
-                    name              = name,
-                    highlightProperty = highlightProperty,
-                    locationFormat    = locationFormat,
-                    path              = path,
-                    useHighlight      = useHighlight,
-                    useParentGrid     = useParentGrid)
+class MapWidget(Element):
+    TAG=Element.WIDGET+'MapWidget'
+    def __init__(self,elem,node=None,entryName=''):
+        super(MapWidget,self).__init__(elem,self.TAG,
+                                       node      = node,
+                                       entryName = entryName)
 
+    def addWidgetMap(self,**kwargs):
+        '''Add a `WidgetMap` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : WidgetMap
+            The added element
+        '''
+        return self.add(WidgetMap,**kwargs)
+    def getWidgetMaps(self,asdict=True):
+        '''Get all WidgetMap element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `WidgetMap` elements.  If `False`, return a list of all WidgetMap` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `WidgetMap` children
+        '''
+        return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict)
+    
+#
+# EOF
+#
+# ====================================================================
+# From grid.py
+
 # --------------------------------------------------------------------
 HEX_WIDTH = 88.50779626676963
 HEX_HEIGHT = 102.2
-def add_hexgrid(root,zone,
-                color        = rgb(0,0,0),
-                cornersLegal = False,
-                dotsVisible  = False,
-                dx           = HEX_WIDTH,
-                dy           = HEX_HEIGHT,
-                edgesLegal   = False,
-                sideways     = False,
-                snapTo       = True,
-                visible      = True,
-                x0           = 0,
-                y0           = 32,
-                cls          = PICKER+'board.HexGrid'):
-    return add_node(root,zone,cls,
-                    color        = color,
-                    cornersLegal = cornersLegal,
-                    dotsVisible  = dotsVisible,
-                    dx           = dx,
-                    dy           = dy,
-                    edgesLegal   = edgesLegal,
-                    sideways     = sideways,
-                    snapTo       = snapTo,
-                    visible      = visible,
-                    x0           = x0,
-                    y0           = y0)
-
+RECT_WIDTH  = 80
+RECT_HEIGHT = 80
 # --------------------------------------------------------------------
-def add_hexnumbering(root,grid,
-                     color                = rgb(255,0,0),
-                     first                = 'H',
-                     fontSize             = 24,
-                     hDescend             = False,
-                     hDrawOff             = 0,
-                     hLeading             = 1,
-                     hOff                 = 0,
-                     hType                = 'A',
-                     locationFormat       = '$gridLocation$',
-                     rotateText           = 0,
-                     sep                  = '',
-                     stagger              = True,
-                     vDescend             = False,
-                     vDrawOff             = 32,
-                     vLeading             = 0,
-                     vOff                 = 0,
-                     vType                = 'N',
-                     visible              = True,
-                     cls                  = PICKER+'board.mapgrid.HexGridNumbering'):
-    return add_node(root,grid,cls,
-                    color                = color,
-                    first                = first,
-                    fontSize             = fontSize,
-                    hDescend             = hDescend,
-                    hDrawOff             = hDrawOff,
-                    hLeading             = hLeading,
-                    hOff                 = hOff,
-                    hType                = hType,
-                    locationFormat       = locationFormat,
-                    rotateText           = rotateText,
-                    sep                  = sep,
-                    stagger              = stagger,
-                    vDescend             = vDescend,
-                    vDrawOff             = vDrawOff,
-                    vLeading             = vLeading,
-                    vOff                 = vOff,
-                    vType                = vType,
-                    visible              = visible)
+class BaseGrid(Element):
+    def __init__(self,zone,tag,node=None,
+                 color        = rgb(0,0,0),
+                 cornersLegal = False,
+                 dotsVisible  = False,
+                 dx           = HEX_WIDTH,  # Meaning seems reversed!
+                 dy           = HEX_HEIGHT,
+                 edgesLegal   = False,
+                 sideways     = False,
+                 snapTo       = True,
+                 visible      = True,
+                 x0           = 0,
+                 y0           = 32):
+        super(BaseGrid,self).__init__(zone,tag,node=node,
+                                      color        = color,
+                                      cornersLegal = cornersLegal,
+                                      dotsVisible  = dotsVisible,
+                                      dx           = dx,
+                                      dy           = dy,
+                                      edgesLegal   = edgesLegal,
+                                      sideways     = sideways,
+                                      snapTo       = snapTo,
+                                      visible      = visible,
+                                      x0           = x0,
+                                      y0           = y0)
+    def getZone(self):
+        z = self.getParent(Zone)
+        return z
+    def getZonedGrid(self):
+        z = self.getZone()
+        if z is not None:
+            return z.getZonedGrid()
+        return None
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return self.getParent(Board)
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        b = self.getPicker()
+        if b is not None:
+            return b.getMap()
+        return None
+    def getNumbering(self):
+        pass 
+    def getLocation(self,loc):
+        numbering = self.getNumbering()
+        if numbering is None or len(numbering) < 1:
+            return None
 
+        return numbering[0].getLocation(loc)
+    
 # --------------------------------------------------------------------
-RECT_WIDTH  = 80
-RECT_HEIGHT = 80
-def add_squaregrid(root,zone,
-                   dx           = RECT_WIDTH,
-                   dy           = RECT_HEIGHT,
-                   edgesLegal   = False,
-                   x0           = 0,
-                   y0           = int(0.4*RECT_HEIGHT),
-                   **attr):
-    return add_hexgrid(root,zone,cls=PICKER+'board.SquareGrid',
-                       dx           = dx,
-                       dy           = dy,
-                       edgesLegal   = edgesLegal,
-                       x0           = x0,
-                       y0           = y0,
-                       **attr)
+class BaseNumbering(Element):
+    def __init__(self,grid,tag,node=None,
+                 color                = rgb(255,0,0),
+                 first                = 'H',
+                 fontSize             = 24,
+                 hDescend             = False,
+                 hDrawOff             = 0,
+                 hLeading             = 1,
+                 hOff                 = 0,
+                 hType                = 'A',
+                 locationFormat       = '$gridLocation$',
+                 rotateText           = 0,
+                 sep                  = '',
+                 stagger              = True,
+                 vDescend             = False,
+                 vDrawOff             = 32,
+                 vLeading             = 0,
+                 vOff                 = 0,
+                 vType                = 'N',
+                 visible              = True):
+        super(BaseNumbering,self).__init__(grid,tag,node=node,
+                                           color           = color,
+                                           first           = first,
+                                           fontSize        = fontSize,
+                                           hDescend        = hDescend,
+                                           hDrawOff        = hDrawOff,
+                                           hLeading        = hLeading,
+                                           hOff            = hOff,
+                                           hType           = hType,
+                                           locationFormat  = locationFormat,
+                                           rotateText      = rotateText,
+                                           sep             = sep,
+                                           stagger         = stagger,
+                                           vDescend        = vDescend,
+                                           vDrawOff        = vDrawOff,
+                                           vLeading        = vLeading,
+                                           vOff            = vOff,
+                                           vType           = vType,
+                                           visible         = visible)
+    def getGrid(self): return getParent(BaseGrid)
 
+    def _getMatcher(self,tpe,leading):
+        if tpe == 'A':
+            return \
+                '-?(?:A+|B+|C+|D+|E+|F+|G+|H+|I+|'  + \
+                'J+|K+|L+|M+|N+|O+|P+|Q+|R+|S+|T+|' + \
+                'U+|V+|W+|X+|Y+|Z+)'
+
+        return f'-?[0-9]{{{int(leading)+1},}}'
+
+    def _getIndex(self,name,tpe):
+        if tpe == 'A':
+            negative = name.startswith('-')
+            if negative:
+                name = name[1:]
+
+            value = 0
+            for num,let in enumerate(name):
+                if not let.isupper():
+                    continue
+                if num < len(name) - 1:
+                    value += 26
+                else:
+                    value += ord(let)-ord('A')
+
+            if negative:
+                value *= -1
+
+            return value
+
+        return int(name)
+
+    def _getCenter(self,col,row):
+        '''Convert col and row index to picture coordinates'''
+        print('Dummy GetCenter')
+        pass
+    
+    def getLocation(self,loc):
+        '''Get picture coordinates from grid location'''
+        from re import match
+
+        first   = self['first']
+        vType   = self['vType']
+        hType   = self['hType']
+        vOff    = int(self['vOff'])
+        hOff    = int(self['hOff'])
+        colPat  = self._getMatcher(hType,self['hLeading'])
+        rowPat  = self._getMatcher(vType,self['vLeading'])
+        patts   = ((colPat,rowPat) if first == 'H' else (rowPat,colPat))
+        colGrp  = 1 if first == 'H' else 2
+        rowGrp  = 2 if first == 'H' else 1
+        patt    = ''.join([f'({p})' for p in patts])
+        matched = match(patt,loc)
+        if not matched:
+            return None
+
+        rowStr  = matched[rowGrp]
+        colStr  = matched[colGrp]
+        rowNum  = self._getIndex(rowStr,vType)
+        colNum  = self._getIndex(colStr,hType)
+
+        return self._getCenter(colNum-hOff, rowNum-vOff);
+       
+    
 # --------------------------------------------------------------------
-def add_squarenumbering(root,grid,
-                        hType                = 'N',
-                        **attr):
-    return add_hexnumbering(root,grid,
-                            cls=PICKER+'board.mapgrid.SquareGridNumbering',
-                            hType                = hType,
-                            **attr)
+class HexGrid(BaseGrid):
+    TAG = Element.BOARD+'HexGrid'
+    def __init__(self,zone,node=None,**kwargs):
+        super(HexGrid,self).__init__(zone,self.TAG,node=node,**kwargs)
 
+    def addNumbering(self,**kwargs):
+        '''Add a `Numbering` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Numbering
+            The added element
+        '''
+        return self.add(HexNumbering,**kwargs)
+    def getNumbering(self):
+        return self.getAllElements(HexNumbering)
+    def getDeltaX(self):
+        return float(self['dx'])
+    def getDeltaY(self):
+        return float(self['dy'])
+    def getXOffset(self):
+        return int(self['x0'])
+    def getYOffset(self):
+        return int(self['y0'])
+    def getMaxRows(self):
+        from math import floor
+        height    = self.getZone().getHeight()
+        return floor(height / self.getDeltaX() + .5)
+    def getMaxCols(self):
+        from math import floor
+        width    = self.getZone().getWidth()
+        return floor(width / self.getDeltaY()  + .5)
+
 # --------------------------------------------------------------------
-def add_regiongrid(root,zone,snapto=True,fontsize=9,visible=True):
-    # print(f'Make region grid visible: {visible}')
-    return add_node(root,zone,PICKER+'board.RegionGrid',
-                    fontsize = fontsize,
-                    snapto   = snapto,
-                    visble   = visible)
+class SquareGrid(BaseGrid):
+    TAG = Element.BOARD+'SquareGrid'
+    def __init__(self,zone,node=None,
+                 dx           = RECT_WIDTH,
+                 dy           = RECT_HEIGHT,
+                 edgesLegal   = False,
+                 x0           = 0,
+                 y0           = int(0.4*RECT_HEIGHT),
+                 **kwargs):
+        super(SquareGrid,self).__init__(zone,self.TAG,node=node,
+                                        dx         = dx,
+                                        dy         = dy,
+                                        edgesLegal = edgesLegal,
+                                        x0         = 0,
+                                        y0         = int(0.4*RECT_HEIGHT),
+                                        **kwargs)
+    def addNumbering(self,**kwargs):
+        '''Add a `Numbering` element to this
 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Numbering
+            The added element
+        '''
+        return self.add(SquareNumbering,**kwargs)
+    def getNumbering(self):
+        return self.getAllElements(SquareNumbering)
+    def getDeltaX(self):
+        return float(self['dx'])
+    def getDeltaY(self):
+        return float(self['dy'])
+    def getXOffset(self):
+        return int(self['x0'])
+    def getYOffset(self):
+        return int(self['y0'])
+    def getMaxRows(self):
+        from math import floor
+        height    = self.getZone().getHeight()
+        return floor(height / self.getDeltaY() + .5)
+    def getMaxCols(self):
+        from math import floor
+        width    = self.getZone().getWidth()
+        return floor(width / self.getDeltaX()  + .5)
+
 # --------------------------------------------------------------------
-def get_region_piece(root,grid,name,alsoPiece=True,piece=None,verbose=False):
-    if not alsoPiece:
-        return None, None
+class HexNumbering(BaseNumbering):
+    TAG = Element.BOARD+'mapgrid.HexGridNumbering'
+    def __init__(self,grid,node=None,**kwargs):
+        super(HexNumbering,self).__init__(grid,self.TAG,node=node,**kwargs)
+    def getGrid(self):
+        g = self.getParent(HexGrid)
+        return g
 
-    # Find the parent map element by stepping up the tree 
-    from functools import reduce
+    def _getCenter(self,col,row):
+        '''Convert col and row index to picture coordinates'''
+        from math import floor
+        
+        stagger  = self['stagger'] == 'true'
+        sideways = self.getGrid()['sideways'] == 'true'
+        hDesc    = self['hDescend'] == 'true'
+        vDesc    = self['vDescend'] == 'true'
+        xOff     = self.getGrid().getXOffset()
+        yOff     = self.getGrid().getYOffset()
+        hexW     = self.getGrid().getDeltaX()
+        hexH     = self.getGrid().getDeltaY()
+        zxOff    = self.getGrid().getZone().getXOffset()
+        zyOff    = self.getGrid().getZone().getYOffset()
+        maxRows  = self.getGrid().getMaxRows()
+        maxCols  = self.getGrid().getMaxCols()
+        # print(f'  Col:         {col}')
+        # print(f'  Row:         {row}')
+        # print(f'  Stagger:     {stagger}')
+        # print(f'  Sideways:    {sideways}')
+        # print(f'  hDesc:       {hDesc}')
+        # print(f'  vDesc:       {vDesc}')
+        # print(f'  maxRows:     {maxRows}')
+        # print(f'  maxCols:     {maxCols}')
 
-    map = reduce(lambda nn, _: None if nn is None else nn.parentNode,
-                 range(5), grid)
-    if map is None or \
-       map.tagName not in [MODULE+'Map',WIDGET+'WidgetMap']:
-        if map is None:
-            print(f'      = Map not found from grid')
-        return None, None
+        if sideways:
+            maxRows, maxCols = maxCols, maxRows
+            
+        if stagger:
+            if sideways:
+                if col % 2 != 0:
+                    row += 1 if hDesc else -1
+            else:
+                if col % 2 != 0:
+                    row += 1 if vDesc else -1
 
-    if piece is not None:
-        print(f'      Using supplied piece')
-        return piece, map
+        if hDesc:
+            col = maxCols - col
+        if vDesc:
+            row = maxRows - row
 
-    # Find the piece by searching from the top of the tree
-    piece = get_pieces(root,root).get(name,None)
-    # if piece is None:
-    #     print(list(get_pieces(root,root).keys()))
-    if verbose:
-        print(f'       Possible piece for region {name}: {piece}')
-    return piece, map
+        x = col * hexW + xOff
+        y = row * hexH + yOff + (hexH/2 if col % 2 != 0 else 0)
 
+        x = int(floor(x + .5))
+        y = int(floor(y + .5))
+        if sideways:
+            # print(f'Swap coordinates because {sideways}')
+            x, y = y, x
+
+        return x,y
+
 # --------------------------------------------------------------------
-def check_region_name(grid,name):
-    post = ''
-    targ = name
-    same = 0
-    regs = list(get_regions(grid).keys())
-    poss = len([e for e in regs if e.startswith(name)])
-    if poss == 0:
-        return name
+class SquareNumbering(BaseNumbering):
+    TAG = Element.BOARD+'mapgrid.SquareGridNumbering'
+    def __init__(self,grid,node=None,hType='N',**kwargs):
+        super(SquareNumbering,self).__init__(grid,self.TAG,node=node,
+                                             hType=hType,**kwargs)
+    def getGrid(self):
+        return self.getParent(SquareGrid)
 
-    return name + f' {poss}'
-    # Loop over keys, sorted
-    # regs.sort()
-    # for r in regs:
-    #     if targ == r:
-    #         same += 1
-    #         targ =  name + f' {same:02d}'
-    #         print(f'        Found region named {r} -> {targ}')
+    def getCenter(self,col,row):
+        hDesc    = self['hDescend'] == 'true'
+        vDesc    = self['vDescend'] == 'true'
+        xOff     = self.getGrid().getXOffset()
+        yOff     = self.getGrid().getYOffset()
+        squareW  = self.getGrid().getDeltaX()
+        squareH  = self.getGrid().getDeltaY()
+        maxRows  = self.getGrid().getMaxRows()
+        maxCols  = self.getGrid().getMaxCols()
 
-    return targ
+        if vDesc:  row = maxRows - row
+        if hDesc:  col = maxCols - col
+
+        x = col * squareW + xOff
+        y = row * squareH + yOff
+
+        return x,y
+        
     
 # --------------------------------------------------------------------
-def add_region(root,grid,name,originx,originy,gpid,
-               alsoPiece=True,piece=None,verbose=True):
-    # First thing is to check if we have a piece
-    piece, map = get_region_piece(root,grid,name,alsoPiece,piece,verbose)
-    if verbose:
-        print(f'       Region {name} -> piece {piece}')
-    
-            
-    nam = check_region_name(grid,name)
-    if verbose:
-        print(f'       Region {name} -> real name {nam}')
-    n   = add_node(root,grid,PICKER+'board.Region',
-                   name     = nam,
-                   originx  = originx,
-                   originy  = originy)
-    if piece is None:
-        return n, gpid
+class RegionGrid(Element):
+    TAG = Element.BOARD+'RegionGrid'
+    def __init__(self,zone,node=None,snapto=True,fontsize=9,visible=True):
+        super(RegionGrid,self).__init__(zone,self.TAG,node=node,
+                                        snapto   = snapto,
+                                        fontsize = fontsize,
+                                        visible  = visible)
 
-    if verbose:
-        print(f'       Add piece {name} to region')
-    _, gpid = add_atstart(root,map,name,piece,
-                          location        = nam,
-                          useGridLocation = True,
-                          owningBoard     = map.getAttribute('mapName'),
-                          x               = 0,
-                          y               = 0,
-                          gpid            = gpid)
-    return n, gpid
-    
+    def getZone(self):
+        return self.getParent(Zone)
+    def getZoneGrid(self):
+        z = self.getZone()
+        if z is not None:
+            return z.getBoard()
+        return None
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return self.getParent(Board)
+    def getMap(self):
+        b = self.getBoard()
+        if b is not None:
+            return b.getMap()
+        return None
+    def getRegions(self):
+        return self.getElementsByKey(Region,'name')    
+    def checkName(self,name):
+        '''Get unique name'''
+        poss = len([e for e in self.getRegions()
+                    if e == name or e.startswith(name+'_')])
+        if poss == 0:
+            return name
 
-# --------------------------------------------------------------------
-def add_stacking(root,map,
-                 bottom               = '40,0',
-                 disabled             = False,
-                 down                 = '37,0',
-                 exSepX               = 6,
-                 exSepY               = 18,
-                 top                  = '38,0',
-                 unexSepX             = 8,
-                 unexSepY             = 16,
-                 up                   = '39,0'):
-    return add_node(root,map,MAP+'StackMetrics',
-                    bottom               = bottom,
-                    disabled             = disabled,
-                    down                 = down,
-                    exSepX               = exSepX,
-                    exSepY               = exSepY,
-                    top                  = top,
-                    unexSepX             = unexSepX,
-                    unexSepY             = unexSepY,
-                    up                   = up)
+        return name + f'_{poss}'
+    def addRegion(self,**kwargs):
+        '''Add a `Region` element to this
 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Region
+            The added element
+        '''
+        return self.add(Region,**kwargs)
+    def getLocation(self,loc):
+        for r in self.getRegions().values():
+            if loc == r['name']:
+                return int(r['originx']),int(r['originy'])
+
+        return None
+        
 # --------------------------------------------------------------------
-def add_camera(root,map,
-               buttonText           = '',
-               canDisable           = False,
-               hotkey               = '',
-               icon                 = '/images/camera.gif',
-               propertyGate         = '',
-               tooltip              = 'Save map as PNG image'):
-    return add_node(root,map,MAP+'ImageSaver',
-               buttonText           = buttonText,
-               canDisable           = canDisable,
-               hotkey               = hotkey,
-               icon                 = icon,
-               propertyGate         = propertyGate,
-               tooltip              = tooltip)
+class Region(Element):
+    TAG = Element.BOARD+'Region'
+    def __init__(self,grid,node=None,
+                 name      = '',
+                 originx   = 0,
+                 originy   = 0,
+                 alsoPiece = True,
+                 piece     = None,
+                 prefix    = ''):
+        fullName = name + ("@"+prefix if len(prefix) else "")
+        realName = grid.checkName(fullName) if node is None else fullName
+        super(Region,self).__init__(grid,
+                                    self.TAG,
+                                    node    = node,
+                                    name    = realName,
+                                    originx = originx,
+                                    originy = originy)
 
+        if node is None and alsoPiece:
+            m = self.getMap()
+            b = self.getBoard()
+            if m is not None and b is not None:
+                if piece is None:
+                    g      = m.getGame()
+                    pieces = g.getSpecificPieces(name,asdict=False)
+                    piece  = pieces[0] if len(pieces) > 0 else None
+             
+                if piece is not None:
+                    # bname = m['mapName']
+                    bname = b['name']
+                    #print(f'Adding at-start name={name} location={realName} '
+                    #      f'owning board={bname}')
+                    a = m.addAtStart(name            = name,
+                                     location        = realName,
+                                     useGridLocation = True,
+                                     owningBoard     = bname,
+                                     x               = 0,
+                                     y               = 0)
+                    p = a.addPiece(piece)
+                    if p is None:
+                        print(f'EEE Failed to add piece {name} ({piece}) to add-start {a}')
+                    #if p is not None:
+                    #    print(f'Added piece {name} in region')
+                #else:
+                #    print(f'Could not find piece {name}')
+            
+    def getGrid(self):
+        return self.getParent(RegionGrid)
+    def getZone(self):
+        g = self.getGrid()
+        if g is not None:
+            return g.getZone()
+        return None
+    def getZonedGrid(self):
+        z = self.getZone()
+        if z is not None:
+            return z.getZonedGrid()
+        return None
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return self.getParent(Board)
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        b = self.getPicker()
+        if b is not None:
+            return b.getMap()
+        return None
+
+#
+# EOF
+#
+# ====================================================================
+# From zone.py
+
 # --------------------------------------------------------------------
-def add_forwardtochatter(root,map):
-    return add_node(root,map,MAP+'ForwardToChatter')
+class ZonedGrid(Element):
+    TAG=Element.BOARD+'ZonedGrid'
+    def __init__(self,board,node=None):
+        super(ZonedGrid,self).__init__(board,self.TAG,node=node)
 
+    def getBoard(self):
+        b = self.getParent(Board)
+        # print(f'Get Board of Zoned: {b}')        
+        return b
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        z = self.getPicker()
+        if z is not None:
+            return z.getMap()
+        return None
+    def addHighlighter(self,**kwargs):
+        '''Add a `Highlighter` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Highlighter
+            The added element
+        '''
+        return self.add(ZonedGridHighlighter,**kwargs)
+    def getHighlighters(self,single=True):
+        '''Get all or a sole `ZonedGridHighlighter` element(s) from this
+        
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Highligter` child, otherwise fail.
+            If `False` return all `Highligter` children in this element
+        Returns
+        -------
+        children : list
+            List of `Highligter` children (even if `single=True`)
+        '''
+        return self.getAllElements(ZonedGridHighliger,single=single)
+    def addZone(self,**kwargs):
+        '''Add a `Zone` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Zone
+            The added element
+        '''
+        return self.add(Zone,**kwargs)
+    def getZones(self,asdict=True):
+        '''Get all Zone element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Zone` elements.  If `False`, return a list of all Zone` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Zone` children
+        '''
+        return self.getElementsByKey(Zone,'name',asdict=asdict)
+
 # --------------------------------------------------------------------
-def add_menudisplayer(root,map):
-    return add_node(root,map,MAP+'MenuDisplayer')
+class ZonedGridHighlighter(Element):
+    TAG=Element.BOARD+'mapgrid.ZonedGridHighlighter'
+    def __init__(self,zoned,node=None):
+        super(ZonedGridHighlighter,self).__init__(zoned,self.TAG,node=node)
+    def getZonedGrid(self): return self.getParent(ZonedGrid)
 
+
 # --------------------------------------------------------------------
-def add_mapcenterer(root,map):
-    return add_node(root,map,MAP+'MapCenterer')
+class Zone(Element):
+    TAG = Element.BOARD+'mapgrid.Zone'
+    def __init__(self,zoned,node=None,
+                 name              = "",
+                 highlightProperty = "",
+                 locationFormat    = "$gridLocation$",
+                 path              = "0,0;976,0;976,976;0,976",
+                 useHighlight      = False,
+                 useParentGrid     = False):
+        super(Zone,self).\
+            __init__(zoned,self.TAG,node=node,
+                     name              = name,
+                     highlightProperty = highlightProperty,
+                     locationFormat    = locationFormat,
+                     path              = path,
+                     useHighlight      = useHighlight,
+                     useParentGrid     = useParentGrid)
 
+    def getZonedGrid(self):
+        z = self.getParent(ZonedGrid)
+        # print(f'Get Zoned of Zone {self["name"]}: {z}')        
+        return z
+    
+    def getBoard(self):
+        z = self.getZonedGrid()
+        if z is not None:
+            return z.getBoard()
+        return None
+    def getPicker(self):
+        z = self.getBoard()
+        if z is not None:
+            return z.getPicker()
+        return None
+    def getMap(self):
+        z = self.getPicker()
+        if z is not None:
+            return z.getMap()
+        return None    
+    def addHexGrid(self,**kwargs):
+        '''Add a `HexGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HexGrid
+            The added element
+        '''
+        return self.add(HexGrid,**kwargs)
+    def addSquareGrid(self,**kwargs):
+        '''Add a `SquareGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : SquareGrid
+            The added element
+        '''
+        return self.add(SquareGrid,**kwargs)
+    def addRegionGrid(self,**kwargs):
+        '''Add a `RegionGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : RegionGrid
+            The added element
+        '''
+        return self.add(RegionGrid,**kwargs)
+    def getHexGrids(self,single=True):
+        '''Get all or a sole `HexGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `HexGrid` child, otherwise fail.
+            If `False` return all `HexGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `HexGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(HexGrid,single=single)
+    def getSquareGrids(self,single=True):
+        '''Get all or a sole `SquareGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `SquareGrid` child, otherwise fail.
+            If `False` return all `SquareGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `SquareGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(SquareGrid,single=single)
+    def getRegionGrids(self,single=True):
+        '''Get all or a sole `RegionGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `RegionGrid` child, otherwise fail.
+            If `False` return all `RegionGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `RegionGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(RegionGrid,single=single)
+    def getGrids(self,single=True):
+        '''Get all or a sole `Grid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Grid` child, otherwise fail.
+            If `False` return all `Grid` children in this element
+        Returns
+        -------
+        children : list
+            List of `Grid` children (even if `single=True`)
+        '''
+        g = self.getHexGrids(single=single)
+        if g is not None: return g
+
+        g = self.getSquareGrids(single=single)
+        if g is not None: return g
+
+        g = self.getRegionGrids(single=single)
+        if g is not None: return g
+
+        return g
+    
+    def getPath(self):
+        p  = self['path'].split(';')
+        r  = []
+        for pp in p:
+            c = pp.split(',')
+            r.append([int(c[0]),int(c[1])])
+        return r
+    def getBB(self):
+        from functools import reduce
+        path = self.getPath()
+        llx  = reduce(lambda old,point:min(point[0],old),path,100000000000)
+        lly  = reduce(lambda old,point:min(point[1],old),path,100000000000)
+        urx  = reduce(lambda old,point:max(point[0],old),path,-1)
+        ury  = reduce(lambda old,point:max(point[1],old),path,-1)
+        return llx,lly,urx,ury
+    def getWidth(self):
+        llx,_,urx,_ = self.getBB()
+        return urx-llx
+    def getHeight(self):
+        _,lly,_,ury = self.getBB()
+        return ury-lly
+    def getXOffset(self):
+        return self.getBB()[0]
+    def getYOffset(self):
+        return self.getBB()[1]
+
+#
+# EOF
+#
+# ====================================================================
+# From board.py
+
 # --------------------------------------------------------------------
-def add_stackexpander(root,map):
-    return add_node(root,map,MAP+'StackExpander')
+class BoardPicker(MapElement):
+    TAG = Element.MAP+'BoardPicker'
+    def __init__(self,doc,node=None,
+                 addColumnText        = 'Add column',
+                 addRowText           = 'Add row',
+                 boardPrompt          = 'Select board',
+                 slotHeight           = 125,
+                 slotScale            = 0.2,
+                 slotWidth            = 350,
+                 title                = 'Choose Boards'):
+        super(BoardPicker,self).__init__(doc,self.TAG,node=node,
+                                         addColumnText        = addColumnText,
+                                         addRowText           = addRowText,
+                                         boardPrompt          = boardPrompt,
+                                         slotHeight           = slotHeight,
+                                         slotScale            = slotScale,
+                                         slotWidth            = slotWidth,
+                                         title                = title)
 
+    def addSetup(self,**kwargs):
+        '''Add a `Setup` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Setup
+            The added element
+        '''
+        if 'mapName' not in kwargs:
+            m = self.getMap()
+            kwargs['mapName'] = m.getAttribute('mapName')
+            
+        return self.add(Setup,**kwargs)
+    def getSetups(self,single=False):
+        '''Get all or a sole `Setup` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Setup` child, otherwise fail.
+            If `False` return all `Setup` children in this element
+        Returns
+        -------
+        children : list
+            List of `Setup` children (even if `single=True`)
+        '''
+        return self.getAllElements(Setup,single=single)
+    def addBoard(self,**kwargs):
+        '''Add a `Board` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Board
+            The added element
+        '''
+        return self.add(Board,**kwargs)
+    def getBoards(self,asdict=True):
+        '''Get all Board element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board` elements.  If `False`, return a list of all Board` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+        '''
+        return self.getElementsByKey(Board,'name',asdict=asdict)
+    def encode(self):
+        setups = self.getSetups()
+        if setups is not None and len(setups)>0:
+            return [setups[0]._node.childNodes[0].nodeValue]
+        
+        ret    = []
+        for bn in self.getBoards().keys():
+            ret.append(bn+'BoardPicker\t'+bn+'\t0\t0')
+
+        return ret
+
 # --------------------------------------------------------------------
-def add_piecemover(root,map):
-    return add_node(root,map,MAP+'PieceMover')
+class Setup(Element):
+    TAG = 'setup'
+    def __init__(self,picker,node=None,
+                 mapName = '',
+                 maxColumns = 1,
+                 boardNames = []):
+        super(Setup,self).__init__(picker,self.TAG,node=node)
+        col = 0
+        row = 0
+        lst = [f'{mapName}BoardPicker']
+        for bn in boardNames:
+            lst.extend([bn,str(col),str(row)])
+            col += 1
+            if col >= maxColumns:
+                col = 0
+                row += 1
+                
+        txt = r'	'.join(lst)
+        self.addText(txt)
 
+    def getPicker(self): return self.getParent(BoardPicker)
+            
 # --------------------------------------------------------------------
-def add_selectionhighlighters(root,map):
-    return add_node(root,map,MAP+'SelectionHighlighters')
+class Board(Element):
+    TAG = Element.PICKER+'Board'
+    def __init__(self,picker,node=None,
+                 name       = '',
+                 image      = '',
+                 reversible = False,
+                 color      = rgb(255,255,255),
+                 width      = 0,
+                 height     = 0):
+        super(Board,self).__init__(picker,self.TAG,node=node,
+                                   image      = image,
+                                   name       = name,
+                                   reversible = reversible,
+                                   color      = color,
+                                   width      = width,
+                                   height     = height)
 
+    def getPicker(self): return self.getParent(BoardPicker)
+    def getMap(self):
+        z = self.getPicker()
+        if z is not None:
+            return z.getMap()
+        return None
+    def addZonedGrid(self,**kwargs):
+        '''Add a `ZonedGrid` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ZonedGrid
+            The added element
+        '''
+        return self.add(ZonedGrid,**kwargs)
+    def getZonedGrids(self,single=True):
+        '''Get all or a sole `ZonedGrid` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `ZonedGrid` child, otherwise fail.
+            If `False` return all `ZonedGrid` children in this element
+        Returns
+        -------
+        children : list
+            List of `ZonedGrid` children (even if `single=True`)
+        '''
+        return self.getAllElements(ZonedGrid,single=single)
+    def getZones(self,asdict=True):
+        '''Get all Zone element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Zone` elements.  If `False`, return a list of all Zone` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Zone` children
+        '''
+        zoned = self.getZonedGrids(single=True)
+        if zoned is None: return None
+
+        return zoned[0].getZones(asdict=asdict)
+
+    def getWidth(self):
+        # print(f'Getting width of {self}: {self["width"]}')
+        if 'width' in self and int(self['width']) != 0:
+            return int(self['width'])
+        return 0
+
+    def getHeight(self):
+        # print(f'Getting height of {self}: {self["height"]}')
+        if 'height' in self and int(self['height']) != 0:
+            return int(self['height'])
+        return 0
+
+
+#
+# EOF
+#
+# ====================================================================
+# From map.py
+
 # --------------------------------------------------------------------
-def add_keybufferer(root,map):
-    return add_node(root,map,MAP+'KeyBufferer')
+class BaseMap(Element):
+    def __init__(self,doc,tag,node=None,
+                 mapName              = '',
+                 allowMultiple        = 'false',
+                 backgroundcolor      = rgb(255,255,255),
+                 buttonName           = '',
+                 changeFormat         = '$message$',
+                 color                = rgb(0,0,0), # Selected pieces
+                 createFormat         = '$pieceName$ created in $location$ *',
+                 edgeHeight           = '0',
+                 edgeWidth            = '0',
+                 hideKey              = '',
+                 hotkey               = key('M',ALT),
+                 icon                 = '/images/map.gif',
+                 launch               = 'false',
+                 markMoved            = 'Always',
+                 markUnmovedHotkey    = '',
+                 markUnmovedIcon      = '/images/unmoved.gif',
+                 markUnmovedReport    = '',
+                 markUnmovedText      = '',
+                 markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
+                 moveKey              = '',
+                 moveToFormat         = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 moveWithinFormat     = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 showKey              = '',
+                 thickness            = '3'):
+        super(BaseMap,self).__init__(doc,tag,node=node,
+                                     allowMultiple        = allowMultiple,
+                                     backgroundcolor      = backgroundcolor,
+                                     buttonName           = buttonName,
+                                     changeFormat         = changeFormat,
+                                     color                = color,
+                                     createFormat         = createFormat,
+                                     edgeHeight           = edgeHeight,
+                                     edgeWidth            = edgeWidth,
+                                     hideKey              = hideKey,
+                                     hotkey               = hotkey,
+                                     icon                 = icon,
+                                     launch               = launch,
+                                     mapName              = mapName,
+                                     markMoved            = markMoved,
+                                     markUnmovedHotkey    = markUnmovedHotkey,
+                                     markUnmovedIcon      = markUnmovedIcon,
+                                     markUnmovedReport    = markUnmovedReport,
+                                     markUnmovedText      = markUnmovedText,
+                                     markUnmovedTooltip   = markUnmovedTooltip,
+                                     moveKey              = moveKey,
+                                     moveToFormat         = moveToFormat,
+                                     moveWithinFormat     = moveWithinFormat,
+                                     showKey              = showKey,
+                                     thickness            = thickness)
 
+    def getGame(self):
+        '''Get the game'''
+        return self.getParentOfClass([Game])
+    def addPicker(self,**kwargs):
+        '''Add a `Picker` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Picker
+            The added element
+        '''
+        return self.add(BoardPicker,**kwargs)
+    def getBoardPicker(self,single=True):
+        '''Get all or a sole `BoardPicker` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `BoardPicker` child, otherwise fail.
+            If `False` return all `BoardPicker` children in this element
+        Returns
+        -------
+        children : list
+            List of `BoardPicker` children (even if `single=True`)
+        '''
+        return self.getAllElements(BoardPicker,single)
+    def getPicker(self,single=True):
+        '''Get all or a sole `BoardPicker` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `BoardPicker` child, otherwise fail.
+            If `False` return all `BoardPicker` children in this element
+        Returns
+        -------
+        children : list
+            List of `BoardPicker` children (even if `single=True`)
+        '''
+        return self.getAllElements(BoardPicker,single)
+    def getStackMetrics(self,single=True):
+        '''Get all or a sole `StackMetric` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `StackMetric` child, otherwise fail.
+            If `False` return all `StackMetric` children in this element
+        Returns
+        -------
+        children : list
+            List of `StackMetric` children (even if `single=True`)
+        '''
+        return self.getAllElements(StackMetrics,single)
+    def getImageSaver(self,single=True):
+        '''Get all or a sole `ImageSaver` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `ImageSaver` child, otherwise fail.
+            If `False` return all `ImageSaver` children in this element
+        Returns
+        -------
+        children : list
+            List of `ImageSaver` children (even if `single=True`)
+        '''
+        return self.getAllElements(ImageSaver,single)
+    def getTextSaver(self,single=True):
+        '''Get all or a sole `TextSaver` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `TextSaver` child, otherwise fail.
+            If `False` return all `TextSaver` children in this element
+        Returns
+        -------
+        children : list
+            List of `TextSaver` children (even if `single=True`)
+        '''
+        return self.getAllElements(TextSaver,single)
+    def getForwardToChatter(self,single=True):
+        '''Get all or a sole `ForwardToChatter` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `ForwardToChatter` child, otherwise fail.
+            If `False` return all `ForwardToChatter` children in this element
+        Returns
+        -------
+        children : list
+            List of `ForwardToChatter` children (even if `single=True`)
+        '''
+        return self.getAllElements(ForwardToChatter,single)
+    def getMenuDisplayer(self,single=True):
+        '''Get all or a sole `MenuDi` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `MenuDi` child, otherwise fail.
+            If `False` return all `MenuDi` children in this element
+        Returns
+        -------
+        children : list
+            List of `MenuDi` children (even if `single=True`)
+        '''
+        return self.getAllElements(MenuDisplayer,single)
+    def getMapCenterer(self,single=True):
+        '''Get all or a sole `MapCenterer` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `MapCenterer` child, otherwise fail.
+            If `False` return all `MapCenterer` children in this element
+        Returns
+        -------
+        children : list
+            List of `MapCenterer` children (even if `single=True`)
+        '''
+        return self.getAllElements(MapCenterer,single)
+    def getStackExpander(self,single=True):
+        '''Get all or a sole `StackExpander` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `StackExpander` child, otherwise fail.
+            If `False` return all `StackExpander` children in this element
+        Returns
+        -------
+        children : list
+            List of `StackExpander` children (even if `single=True`)
+        '''
+        return self.getAllElements(StackExpander,single)
+    def getPieceMover(self,single=True):
+        '''Get all or a sole `PieceMover` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `PieceMover` child, otherwise fail.
+            If `False` return all `PieceMover` children in this element
+        Returns
+        -------
+        children : list
+            List of `PieceMover` children (even if `single=True`)
+        '''
+        return self.getAllElements(PieceMover,single)
+    def getSelectionHighlighters(self,single=True):
+        '''Get all or a sole `SelectionHighlighter` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `SelectionHighlighter` child, otherwise fail.
+            If `False` return all `SelectionHighlighter` children in this element
+        Returns
+        -------
+        children : list
+            List of `SelectionHighlighter` children (even if `single=True`)
+        '''
+        return self.getAllElements(SelectionHighlighters,single)
+    def getKeyBufferer(self,single=True):
+        return self.getAllElements(KeyBufferer,single)
+    def getHighlightLastMoved(self,single=True):
+        '''Get all or a sole `HighlightLa` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `HighlightLa` child, otherwise fail.
+            If `False` return all `HighlightLa` children in this element
+        Returns
+        -------
+        children : list
+            List of `HighlightLa` children (even if `single=True`)
+        '''
+        return self.getAllElements(HighlightLastMoved,single)
+    def getCounterDetailViewer(self,single=True):
+        '''Get all or a sole `CounterDetailViewer` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `CounterDetailViewer` child, otherwise fail.
+            If `False` return all `CounterDetailViewer` children in this element
+        Returns
+        -------
+        children : list
+            List of `CounterDetailViewer` children (even if `single=True`)
+        '''
+        return self.getAllElements(CounterDetailViewer,single)
+    def getGlobalMap(self,single=True):
+        '''Get all or a sole `GlobalMap` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `GlobalMap` child, otherwise fail.
+            If `False` return all `GlobalMap` children in this element
+        Returns
+        -------
+        children : list
+            List of `GlobalMap` children (even if `single=True`)
+        '''
+        return self.getAllElements(GlobalMap,single)
+    def getZoomer(self,single=True):
+        '''Get all or a sole `Zoomer` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Zoomer` child, otherwise fail.
+            If `False` return all `Zoomer` children in this element
+        Returns
+        -------
+        children : list
+            List of `Zoomer` children (even if `single=True`)
+        '''
+        return self.getAllElements(Zoomer,single)
+    def getHidePiecesButton(self,single=True):
+        '''Get all or a sole `HidePiece` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `HidePiece` child, otherwise fail.
+            If `False` return all `HidePiece` children in this element
+        Returns
+        -------
+        children : list
+            List of `HidePiece` children (even if `single=True`)
+        '''
+        return self.getAllElements(HidePiecesButton,single)
+    def getMassKeys(self,asdict=True):
+        '''Get all MassKey element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `MassKey` elements.  If `False`, return a list of all MassKey` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `MassKey` children
+        '''
+        return self.getElementsByKey(MassKey,'name',asdict)
+    def getFlare(self,single=True):
+        '''Get all or a sole `Flare` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `Flare` child, otherwise fail.
+            If `False` return all `Flare` children in this element
+        Returns
+        -------
+        children : list
+            List of `Flare` children (even if `single=True`)
+        '''
+        return self.getAllElements(Flare,single)
+    def getAtStarts(self,single=True):
+        '''Get all or a sole `AtStart` element(s) from this
+
+        Parameters
+        ----------
+        single : bool
+            If `True`, there can be only one `AtStart` child, otherwise fail.
+            If `False` return all `AtStart` children in this element
+        Returns
+        -------
+        children : list
+            List of `AtStart` children (even if `single=True`)
+        '''
+        return self.getAllElements(AtStart,single)
+    def getBoards(self,asdict=True):
+        '''Get all Board element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Board` elements.  If `False`, return a list of all Board` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Board` children
+        '''
+        picker = self.getPicker()
+        if picker is None:  return None
+        return picker[0].getBoards(asdict=asdict)
+    def addBoardPicker(self,**kwargs):
+        '''Add a `BoardPicker` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BoardPicker
+            The added element
+        '''
+        return self.add(BoardPicker,**kwargs)
+    def addStackMetrics(self,**kwargs):
+        '''Add a `StackMetrics` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : StackMetrics
+            The added element
+        '''
+        return self.add(StackMetrics,**kwargs)
+    def addImageSaver(self,**kwargs):
+        '''Add a `ImageSaver` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ImageSaver
+            The added element
+        '''
+        return self.add(ImageSaver,**kwargs)
+    def addTextSaver(self,**kwargs):
+        '''Add a `TextSaver` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : TextSaver
+            The added element
+        '''
+        return self.add(TextSaver,**kwargs)
+    def addForwardToChatter(self,**kwargs):
+        '''Add a `ForwardToChatter` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : ForwardToChatter
+            The added element
+        '''
+        return self.add(ForwardToChatter,**kwargs)
+    def addMenuDisplayer(self,**kwargs):
+        '''Add a `MenuDisplayer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MenuDisplayer
+            The added element
+        '''
+        return self.add(MenuDisplayer,**kwargs)
+    def addMapCenterer(self,**kwargs):
+        '''Add a `MapCenterer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MapCenterer
+            The added element
+        '''
+        return self.add(MapCenterer,**kwargs)
+    def addStackExpander(self,**kwargs):
+        '''Add a `StackExpander` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : StackExpander
+            The added element
+        '''
+        return self.add(StackExpander,**kwargs)
+    def addPieceMover(self,**kwargs):
+        '''Add a `PieceMover` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PieceMover
+            The added element
+        '''
+        return self.add(PieceMover,**kwargs)
+    def addSelectionHighlighters(self,**kwargs):
+        '''Add a `SelectionHighlighters` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : SelectionHighlighters
+            The added element
+        '''
+        return self.add(SelectionHighlighters,**kwargs)
+    def addKeyBufferer(self,**kwargs):
+        '''Add a `KeyBufferer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : KeyBufferer
+            The added element
+        '''
+        return self.add(KeyBufferer,**kwargs)
+    def addHighlightLastMoved(self,**kwargs):
+        '''Add a `HighlightLastMoved` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HighlightLastMoved
+            The added element
+        '''
+        return self.add(HighlightLastMoved,**kwargs)
+    def addCounterDetailViewer(self,**kwargs):
+        '''Add a `CounterDetailViewer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : CounterDetailViewer
+            The added element
+        '''
+        return self.add(CounterDetailViewer,**kwargs)
+    def addGlobalMap(self,**kwargs):
+        '''Add a `GlobalMap` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : GlobalMap
+            The added element
+        '''
+        return self.add(GlobalMap,**kwargs)
+    def addZoomer(self,**kwargs):
+        '''Add a `Zoomer` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Zoomer
+            The added element
+        '''
+        return self.add(Zoomer,**kwargs)
+    def addHidePiecesButton(self,**kwargs):
+        '''Add a `HidePiecesButton` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : HidePiecesButton
+            The added element
+        '''
+        return self.add(HidePiecesButton,**kwargs)
+    def addMassKey(self,**kwargs):
+        '''Add a `MassKey` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : MassKey
+            The added element
+        '''
+        return self.add(MassKey,**kwargs)
+    def addStartupMassKey(self,**kwargs):
+        '''Add a `StartupMassKey` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : StartupMassKey
+            The added element
+        '''
+        return self.add(MassKey,**kwargs)
+    def addFlare(self,**kwargs):
+        '''Add a `Flare` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Flare
+            The added element
+        '''
+        return self.add(Flare,**kwargs)
+    def addAtStart(self,**kwargs):
+        '''Add a `AtStart` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : AtStart
+            The added element
+        '''
+        return self.add(AtStart,**kwargs)
+     
 # --------------------------------------------------------------------
-def add_highlightlastmoved(root,map,color=rgb(255,0,0),enabled=True,thickness=2):
-    return add_node(root,map,MAP+'HighlightLastMoved',
-                    color=color,enabled=enabled,thickness=thickness)
+class Map(BaseMap):
+    TAG = Element.MODULE+'Map'
+    def __init__(self,doc,node=None,
+                 mapName              = '',
+                 allowMultiple        = 'false',
+                 backgroundcolor      = rgb(255,255,255),
+                 buttonName           = '',
+                 changeFormat         = '$message$',
+                 color                = rgb(0,0,0),
+                 createFormat         = '$pieceName$ created in $location$ *',
+                 edgeHeight           = '0',
+                 edgeWidth            = '0',
+                 hideKey              = '',
+                 hotkey               = key('M',ALT),
+                 icon                 = '/images/map.gif',
+                 launch               = 'false',
+                 markMoved            = 'Always',
+                 markUnmovedHotkey    = '',
+                 markUnmovedIcon      = '/images/unmoved.gif',
+                 markUnmovedReport    = '',
+                 markUnmovedText      = '',
+                 markUnmovedTooltip   = 'Mark all pieces on this map as not moved',
+                 moveKey              = '',
+                 moveToFormat         = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 moveWithinFormat     = '$pieceName$ moves $previousLocation$ → $location$ *',
+                 showKey              = '',
+                 thickness            = '3'):
+        super(Map,self).__init__(doc,self.TAG,node=node,
+                                 allowMultiple        = allowMultiple,
+                                 backgroundcolor      = backgroundcolor,
+                                 buttonName           = buttonName,
+                                 changeFormat         = changeFormat,
+                                 color                = color,
+                                 createFormat         = createFormat,
+                                 edgeHeight           = edgeHeight,
+                                 edgeWidth            = edgeWidth,
+                                 hideKey              = hideKey,
+                                 hotkey               = hotkey,
+                                 icon                 = icon,
+                                 launch               = launch,
+                                 mapName              = mapName,
+                                 markMoved            = markMoved,
+                                 markUnmovedHotkey    = markUnmovedHotkey,
+                                 markUnmovedIcon      = markUnmovedIcon,
+                                 markUnmovedReport    = markUnmovedReport,
+                                 markUnmovedText      = markUnmovedText,
+                                 markUnmovedTooltip   = markUnmovedTooltip,
+                                 moveKey              = moveKey,
+                                 moveToFormat         = moveToFormat,
+                                 moveWithinFormat     = moveWithinFormat,
+                                 showKey              = showKey,
+                                 thickness            = thickness)
 
+    def getGame(self):
+        return self.getParent(Game)
+        
 # --------------------------------------------------------------------
-def add_counterviewer(root,map,
-                      borderWidth          = 0,
-                      centerAll            = False,
-                      centerText           = False,
-                      combineCounterSummary = False,
-                      counterReportFormat  = '',
-                      delay                = 700,
-                      description          = '',
-                      display              = 'from top-most layer only',
-                      emptyHexReportForma  = '$LocationName$',
-                      enableHTML           = True,
-                      extraTextPadding     = 0,
-                      fgColor              = rgb(0,0,0),
-                      fontSize             = 9,
-                      graphicsZoom         = 1.0,
-                      hotkey               = key('\n',0),
-                      layerList            = '',
-                      minDisplayPieces     = 2,
-                      propertyFilter       = '',
-                      showDeck             = False,
-                      showDeckDepth        = 1,
-                      showDeckMasked       = False,
-                      showMoveSelectde     = False,
-                      showNoStack          = False,
-                      showNonMovable       = False,
-                      showOverlap          = False,
-                      showgraph            = True,
-                      showgraphsingle      = False,
-                      showtext             = False,
-                      showtextsingle       = False,
-                      stretchWidthSummary  = False,
-                      summaryReportFormat  = '$LocationName$',
-                      unrotatePieces       = False,
-                      version              = 3,
-                      verticalOffset       = 0,
-                      verticalTopText      = 5,
-                      zoomlevel            = 1.0):
-    return add_node(root,map,MAP+'CounterDetailViewer',
-                    borderWidth          = borderWidth,
-                    centerAll            = centerAll,
-                    centerText           = centerText,
-                    combineCounterSummary = combineCounterSummary,
-                    counterReportFormat  = counterReportFormat,
-                    delay                = delay,
-                    description          = description,
-                    display              = display,
-                    emptyHexReportForma  = emptyHexReportForma,
-                    enableHTML           = enableHTML,
-                    extraTextPadding     = extraTextPadding,
-                    fgColor              = fgColor,
-                    fontSize             = fontSize,
-                    graphicsZoom         = graphicsZoom,
-                    hotkey               = hotkey,
-                    layerList            = layerList,
-                    minDisplayPieces     = minDisplayPieces,
-                    propertyFilter       = propertyFilter,
-                    showDeck             = showDeck,
-                    showDeckDepth        = showDeckDepth,
-                    showDeckMasked       = showDeckMasked,
-                    showMoveSelectde     = showMoveSelectde,
-                    showNoStack          = showNoStack,
-                    showNonMovable       = showNonMovable,
-                    showOverlap          = showOverlap,
-                    showgraph            = showgraph,
-                    showgraphsingle      = showgraphsingle,
-                    showtext             = showtext,
-                    showtextsingle       = showtextsingle,
-                    stretchWidthSummary  = stretchWidthSummary,
-                    summaryReportFormat  = summaryReportFormat,
-                    unrotatePieces       = unrotatePieces,
-                    version              = version,
-                    verticalOffset       = verticalOffset,
-                    verticalTopText      = verticalTopText,
-                    zoomlevel            = zoomlevel)
+class WidgetMap(BaseMap):
+    TAG = Element.WIDGET+'WidgetMap'
+    def __init__(self,doc,node=None,**attr):
+        super(WidgetMap,self).__init__(doc,self.TAG,node=node,**attr)
 
+    def getGame(self):
+        return self.getParentOfClass([Game])
+    def getMapWidget(self):
+        return self.getParent(MapWidget)
+
+
+#
+# EOF
+#
+# ====================================================================
+# From chart.py
+
 # --------------------------------------------------------------------
-def add_globalmap(root,map,
-                  buttonText           = '',
-                  color                = rgb(255,0,0),
-                  hotkey               = key('O',CTRL_SHIFT),
-                  icon                 = '/images/overview.gif',
-                  scale                = 0.2,
-                  tooltip              = 'Show/Hide overview window'):
-    return add_node(root,map,MAP+'GlobalMap',
-                    buttonText           = buttonText,
-                    color                = color,
-                    hotkey               = hotkey,
-                    icon                 = icon,
-                    scale                = scale,
-                    tooltip              = 'Show/Hide overview window')
+class ChartWindow(GameElement,WidgetElement):
+    TAG=Element.MODULE+'ChartWindow'
+    def __init__(self,elem,node=None,
+                 name        = '',
+                 hotkey      = key('A',ALT),
+                 description = '',
+                 text        = '',
+                 tooltip     = 'Show/hide Charts',
+                 icon        = '/images/chart.gif'):
+        super(ChartWindow,self).__init__(elem,self.TAG,node=node,
+                                         name        = name,
+                                         hotkey      = hotkey,
+                                         description = description,
+                                         text        = text,
+                                         tooltip     = tooltip,
+                                         icon        = icon)
 
+    def addChart(self,**kwargs):
+        '''Add a `Chart` element to this
+
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chart
+            The added element
+        '''
+        return self.add(Chart,**kwargs)
+    def getCharts(self,asdict=True):
+        '''Get all Chart element(s) from this
+
+        Parameters
+        ----------
+        asdict : bool
+            If `True`, return a dictonary that maps key to `Chart` elements.  If `False`, return a list of all Chart` children.
+        Returns
+        -------
+        children : dict or list
+            Dictionary or list of `Chart` children
+        '''
+        return self.getElementsById(Chart,'chartName',asdict=asdict)
+    
+    
+
 # --------------------------------------------------------------------
-def add_zoomer(root,map,
-               inButtonText         = '',
-               inIconName           = '/images/zoomIn.gif',
-               inTooltip            = 'Zoom in',
-               outButtonText        = '',
-               outIconName          = '/images/zoomOut.gif',
-               outTooltip           = 'Zoom out',
-               pickButtonText       = '',
-               pickIconName         = '/images/zoom.png',
-               pickTooltip          = 'Select Zoom',
-               zoomInKey            = key('=',CTRL_SHIFT),
-               zoomLevels           = [0.2,0.25,0.333,0.4,0.5,0.555,0.625,0.75,1.0,1.25,1.6],
-               zoomOutKey           = key('-'),
-               zoomPickKey          = key('='),
-               zoomStart            = 3):
-    '''Zoom start is counting from the back (with default zoom levels,
-    and zoom start, the default zoom is 1'''
-    lvls = ','.join([str(z) for z in zoomLevels])
-    return add_node(root,map,MAP+'Zoomer',
-                    inButtonText         = inButtonText,
-                    inIconName           = inIconName,
-                    inTooltip            = inTooltip,
-                    outButtonText        = outButtonText,
-                    outIconName          = outIconName,
-                    outTooltip           = outTooltip,
-                    pickButtonText       = pickButtonText,
-                    pickIconName         = pickIconName,
-                    pickTooltip          = pickTooltip,
-                    zoomInKey            = zoomInKey,
-                    zoomLevels           = lvls,
-                    zoomOutKey           = zoomOutKey,
-                    zoomPickKey          = zoomPickKey,
-                    zoomStart            = zoomStart)
+class Chart(Element):
+    TAG=Element.WIDGET+'Chart'
+    def __init__(self,elem,node=None,
+                 chartName   = '',
+                 fileName    = '',
+                 description = ''):
+        super(Chart,self).__init__(elem,self.TAG,node=node,
+                                   chartName   = chartName,
+                                   description = description,
+                                   fileName    = fileName)
 
+#
+# EOF
+#
+# ====================================================================
+# From command.py
+
 # --------------------------------------------------------------------
-def add_hidepieces(root,map,
-                   buttonText           = '',
-                   hiddenIcon           = '/images/globe_selected.gif',
-                   hotkey               = key('O'),
-                   showingIcon          = '/images/globe_unselected.gif',
-                   tooltip              = 'Hide all pieces on this map'):
-    return add_node(root,map,MAP+'HidePiecesButton',
-                    buttonText           = buttonText,
-                    hiddenIcon           = hiddenIcon,
-                    hotkey               = hotkey,
-                    showingIcon          = showingIcon,
-                    tooltip              = tooltip)
+class Command:
+    def __init__(self,what,iden,tpe,state):
+        self.cmd = '/'.join([what,iden,tpe,state])
+    
 # --------------------------------------------------------------------
-def add_masskey(root,map,
-                name,                
-                buttonHotkey,
-                hotkey,
-                buttonText           = '',
-                canDisable           = False,
-                deckCount            = '-1',
-                filter               = '',
-                propertyGate         = '',
-                reportFormat         = '',
-                reportSingle         = False,
-                singleMap            = True,
-                target               = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS',
-                tooltip              = '',
-                icon                 = ''):
-    '''Default targets are selected units'''
-    return add_node(root,map,MAP+'MassKeyCommand',
-                    name                 = name,                
-                    buttonHotkey         = buttonHotkey, # This hot key
-                    hotkey               = hotkey,       # Target hot key
-                    buttonText           = buttonText,
-                    canDisable           = canDisable,
-                    deckCount            = deckCount,
-                    filter               = filter,
-                    propertyGate         = propertyGate,
-                    reportFormat         = reportFormat,
-                    reportSingle         = reportSingle,
-                    singleMap            = singleMap,
-                    target               = target,
-                    tooltip              = tooltip,
-                    icon                 = icon)
+class AddCommand(Command):
+    ID = '+'
+    def __init__(self,iden,tpe,state):
+        super(AddCommand,self).__init__(self.ID,iden,tpe,state)
+        
 
+#
+# EOF
+#
+# ====================================================================
+# From trait.py
+# ====================================================================
+class Trait:
+    known_traits = []
+    def __init__(self):
+        '''Base class for trait capture.  Unlike the Element classes,
+        this actually holds state that isn't reflected elsewhere in
+        the DOM.  This means that the data here is local to the
+        object.   So when we do
+        
+            piece  = foo.getPieceSlots()[0]
+            traits = p.getTraits()
+            for trait in traits:
+                if trait.ID == 'piece': 
+                    trait["gpid"] = newPid
+                    trait["lpid"] = newPid
+        
+        we do not actually change anything in the DOM.  To do that, we
+        must add back _all_ the traits as
+        
+            piece.setTraits(traits)
+        
+        We can add traits to a piece, like
+        
+            piece.addTrait(MarkTrait('Hello','World'))
+        
+        But it is not particularly efficient.  Better to do
+        (continuing from above)
+        
+            traits.append(MarkTrait('Hello','World;)
+            piece.setTraits(traits)
+        
+        '''
+        self._type  = None
+        self._state = None
 
+    def setType(self,**kwargs):
+        '''Set types.  Dictionary of names and values.  Dictonary keys
+        defines how we access the fields, which is internal here.
+        What matters is the order of the values.
+
+        '''
+        self._type   = list(kwargs.values())
+        self._tnames = list(kwargs.keys())
+    def setState(self,**kwargs):
+        '''Set states.  Dictionary of names and values.  Dictonary keys
+        defines how we access the fields, which is internal here.
+        What matters is the order of the values.
+        '''
+        self._state  = list(kwargs.values())
+        self._snames = list(kwargs.keys())
+
+    
+    def __getitem__(self,key):
+        '''Look up item in either type or state'''
+        try:
+            return self._type[self._tnames.index(key)]
+        except:
+            pass
+        return self._state[self._snames.index(key)]
+
+    def __setitem__(self,key,value):
+        '''Set item in either type or state'''
+        try:
+            self._type[self._tnames.index(key)] = value
+            return
+        except:
+            pass
+        self._state[self._snames.index(key)] = value
+
+    def encode(self,term=False):
+        '''
+        returns type and state encoded'''
+        t = self.encodeFields(self.ID,*self._type,term=term)
+        s = self.encodeFields(*self._state,term=term)
+        return t,s
+
+    @classmethod
+    def findTrait(cls,traits,ID,key=None,value=None,verbose=False):
+        for trait in traits:
+            if trait.ID != ID:
+                continue
+            if verbose:
+                print(f' {trait.ID}')
+            if key is None or value is None:
+                if verbose:
+                    print(f' Return {trait.ID}')
+                return trait
+            if verbose:
+                print(f' Check {key}={value}: {trait[key]}')
+            if trait[key] == value:
+                return trait
+        if verbose:
+            print(f' Trait of type {ID} with {key}={value} not found')
+        return None
+        
+    @classmethod
+    def take(cls,iden,t,s):
+        '''If the first part of the string t matches the ID, then take it.
+
+        t and s are lists of strings.
+        ''' 
+        if iden != cls.ID: return None
+
+        ret = cls()
+        ret._type = t
+        ret._state = s
+        ret.check() # Check if we're reasonable, or raise
+        #print(f'Took {iden} {cls}\n'
+        #      f'  {ret._tnames}\n'
+        #      f'  {ret._snames}')
+        return ret
+
+    def check(self):
+        '''Implement if trait should check that all is OK when cloning'''
+        pass
+
+    @classmethod
+    def encodeFields(cls,*args,term=False):
+        return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
+                         for e in args])+(';' if term else '')
+
+    @classmethod
+    def decodeFields(cls,s):
+        from re import split
+        return split(r'(?<!\\);',s)
+        # return s.split(';') # Probably too simple-minded 
+
+    @classmethod
+    def encodeKeys(cls,keys,sep=','):
+        return sep.join([k.replace(',','\\'+f'{sep}') for k in keys])
+        
+    @classmethod
+    def decodeKeys(cls,keys,sep=','):
+        from re import split
+        ks = split(r'(?<!\\)'+f'{sep}',keys)
+        return [k.replace('\\'+f'{sep}',f'{sep}') for k in ks]
+
+    @classmethod
+    def flatten(cls,traits,game=None,prototypes=None):
+        if prototypes is None:
+            if game is None:
+                print(f'Warning: Game or prototypes not passed')
+                return None
+            prototypes = game.getPrototypes()[0].getPrototypes()
+
+        ret   = []        
+        return cls._flatten(traits,ret,prototypes,False)
+    
+    @classmethod
+    def _flatten(cls,traits,ret,prototypes,nobasic=True):
+        '''Expand all prototype traits in traits'''
+        for trait in traits:
+            # Ignore recursive basic traits
+            if nobasic and trait.ID == BasicTrait.ID:
+                continue
+            # Add normal traits
+            if trait.ID != PrototypeTrait.ID:
+                ret.append(trait)
+                continue
+
+            # Find prototype
+            proto = prototypes.get(trait['name'],None)
+            if proto is None:
+                print(f'Warning, prototype {trait["name"]} not found')
+                continue
+
+            # Recursive call to add prototype traits (and possibly
+            # more recursive calls 
+            cls._flatten(proto.getTraits(), ret, prototypes,not nobasic)
+
+        return ret
+#
+# EOF
+#
+# ====================================================================
+# From withtraits.py
+
 # --------------------------------------------------------------------
-def add_flare(root,map,
-              circleColor          = rgb(255,0,0),
-              circleScale          = True,
-              circleSize           = 100,
-              flareKey             = 'keyAlt',
-              flareName            = 'Map Flare',
-              flarePulses          = 6,
-              flarePulsesPerSec    = 3,
-              reportFormat         = ''):
-    return add_node(root,map,MAP+'Flare',
-                    circleColor          = circleColor,
-                    circleScale          = circleScale,
-                    circleSize           = circleSize,
-                    flareKey             = flareKey,
-                    flareName            = flareName,
-                    flarePulses          = flarePulses,
-                    flarePulsesPerSec    = flarePulsesPerSec,
-                    reportFormat         = '')
+#
+# Traits of this kind of object are
+#
+# - Evaluated from the start of the list to the end of the list,
+#   skipping over report and trigger traits
+# - Then evaluated from the end of the list to the start, only
+#   evaluating report and trigger traits
+# - The list _must_ end in a BasicTrait
+#
+# Traits are copied when making a copy of objects of this class, and
+# are done so using a full decoding and encoding.  This means that
+# copying is a bit susceptible to expansions of the strings of the traits,
+# in particular if they contain special characters such as ',' or '/'.
+#
+class WithTraits(Element):
+    def __init__(self,parent,tag,node=None,traits=[],**kwargs):
+        '''Base class for things that have traits
 
-# --------------------------------------------------------------------
-def add_atstart(root,map,name,*pieces,
-                location        = '',
-                useGridLocation = True,
-                owningBoard     = '',
-                x               = 0,
-                y               = 0,
-                gpid            = None):
-    '''Pieces are existing PieceSlot elements'''
-    a = add_node(root,map,MODULE+'map.SetupStack',
-                 name            = name,
-                 location        = location,
-                 owningBoard     = owningBoard,
-                 useGridLocation = useGridLocation,
-                 x               = x,
-                 y               = y)
-    # copy pieces here 
-    for p in pieces:
-        c = p.cloneNode(True)
-        a.appendChild(c)
-        if gpid is not None:
-            # Update gpid in element
-            c.setAttribute('gpid',str(gpid))
-            # Update gpid in traits state 
-            traits = get_piece_parts(p.childNodes[0].nodeValue)
-            for t in traits:
-                code = t['def']
-                stat = t['state']
-                if code[0] == 'piece':
-                    stat[3] = gpid
-            # Put back the traits 
-            c.childNodes[0].nodeValue = dicts_to_piece(traits)
+        Parameters
+        ----------
+        parent : Element
+            Parent to add this to
+        node : xml.minidom.Element
+            If not None, XML element to read definition from.
+            Rest of the arguments are ignored if not None.
+        traits : list of Trait objects
+            The traits to set on this object
+        kwargs : dict
+            More attributes to set on element
+        '''
+        super(WithTraits,self).__init__(parent,tag,node=node,**kwargs)
+        if node is None: self.setTraits(*traits)
+        
+    def addTrait(self,trait):
+        '''Add a `Trait` element to this.  Note that this re-encodes
+        all traits.
+
+        Parameters
+        ----------
+        trait : Trait
+            The trait to add 
+        '''
+        traits = self.getTraits()
+        traits.append(trait)
+        self.setTraits(*traits)
+
+    def getTraits(self):
+        '''Get all element traits as objects.  This decodes the trait
+        definitions.  This is useful if we have read the element from
+        the XML file, or similar.
+
+        Note that the list of traits returned are _not_ tied to the
+        XML nodes content.  Therefore, if one makes changes to the list,
+        or to elements of the list, and these changes should be
+        reflected in this object, then we _must_ call
+
+            setTraits(traits)
+
+        with the changed list of traits. 
+
+        Returns
+        -------
+        traits : list of Trait objects
+            The decoded traits
+
+        '''
+        from re import split
+        code                = self._node.childNodes[0].nodeValue
+        cmd, iden, typ, sta = split(fr'(?<!\\)/',code) #code.split('/')
+        types               = typ.split(r'	')
+        states              = sta.split(r'	')
+        types               = [t.strip('\\').split(';') for t in types]
+        states              = [s.strip('\\').split(';') for s in states]
+        traits              = []
+        
+        for t, s in zip(types,states):
+            tid   = t[0]
+            trem  = t[1:]
+            known = False
             
-            gpid += 1
+            for c in Trait.known_traits:
+                t = c.take(tid,trem,s) # See if we have it
+                if t is not None:
+                    traits.append(t)  # Got it
+                    known = True
+                    break
+                
+            if not known:
+                print(f'Warning: Unknown trait {tid}')
 
-    return a, gpid
+        return traits
+    
+    def setTraits(self,*traits,iden='null'):
+        '''Set traits on this element.  This encodes the traits into
+        this object.
+        
+        Parameters
+        ----------
+        traits : tuple of Trait objects
+            The traits to set on this object.
+        iden : str
+            Identifier
 
+        '''
+        types = []
+        states = []
+        for trait in traits:
+            if trait is None:
+                print(f'Trait is None (traits: {traits})')
+                continue
+            tpe, state = trait.encode()
+            types.append(tpe)
+            states.append(state)
+
+        tpe   = WithTraits.encodeParts(*types)
+        state = WithTraits.encodeParts(*states)
+        add   = AddCommand(str(iden),tpe,state)
+        if len(self._node.childNodes) < 1:
+            self.addText('')
+        self._node.childNodes[0].nodeValue = add.cmd
+
+    def removeTrait(self,ID,key=None,value=None,verbose=False):
+        '''Remove a trait from this object.
+
+        Parameters
+        ----------
+        ID : str
+            The type of trait to remove.  Must be a valid
+            ID of a class derived from Trait.
+        key : str
+            Optional key to inspect to select trait that has 
+            this key and the traits key value is the argument value,
+        value :
+            If specified, then only traits which key has this value
+            are removed
+        verbose : bool
+            Be verbose if True
+
+        Returns
+        -------
+        trait : Trait
+            The removed trait or None
+        '''
+        traits = self.getTraits()
+        trait  = Trait.findTrait(traits,ID,key,value,verbose)
+        if trait is not None:
+            traits.remove(trait)
+            self.setTraits(traits)
+        return trait
+
+    def addTraits(self,*toadd):
+        '''Add traits to this.  Note that this will
+        decode and reencode the traits.  Only use this when
+        adding traits on-mass.  Repeated use of this is inefficient.
+
+        This member function takes care to push any basic trait to
+        the end of the list.
+
+        The added traits will not override existing triats. 
+
+        Paramters
+        ---------
+        toAdd : tuple of Trait objects
+            The traits to add 
+
+        '''
+        traits = self.getTraits()
+        basic  = Trait.findTrait(traits,BasicTrait.ID)
+        if basic:
+            traits.remove(basic)
+        traits.extend(toAdd)
+        if basic:
+            traits.append(basic)
+        self.setTraits(traits)
+        
+        
+    @classmethod
+    def encodeParts(cls,*parts):
+        '''Encode parts of a full piece definition
+
+        Each trait (VASSAL.counter.Decorator,
+        VASSAL.counter.BasicPiece) definition or state is separated by
+        a litteral TAB character.  Beyond the first TAB separator,
+        additional escape characters (BACKSLAH) are added in front of
+        the separator.  This is to that VASSAL.utils.SequenceDecoder
+        does not see consequitive TABs as a single TAB.
+        '''
+        ret = ''
+        sep = r'	'
+        for i, p in enumerate(parts):
+            if i != 0:
+                ret += '\\'*(i-1) + sep
+            ret += str(p)
+
+        return ret
+        
+        
+    def cloneNode(self,parent):
+        '''This clones the underlying XML node.
+
+        Parameters
+        ----------
+        parent : Element
+            The element to clone this element into
+
+        Returns
+        -------
+        copy : xml.minidom.Element
+            The newly created clone of this object's node
+        '''
+        copy = self._node.cloneNode(deep=True)
+        if parent is not None:
+            parent._node.appendChild(copy)
+        else:
+            print('WARNING: No parent to add copy to')
+        return copy
+    
 # --------------------------------------------------------------------
-def add_pieceslot(root,elem,
-                  entryName,
-                  gpid,
-                  code,
-                  height=72,
-                  width=72,
-                  icon = ''):
-    p = add_node(root,elem,WIDGET+'PieceSlot',
-                 entryName = entryName,
-                 gpid      = gpid,
-                 height    = height,
-                 width     = width,
-                 icon      = icon)
-    add_text(root,p,code)
-    return p
+class DummyWithTraits(WithTraits):
+    TAG = 'dummy'
+    def __init__(self,parent,node=None,traits=[]):
+        '''An empty element.  Used when making searching'''
+        super(DummyWithTraits,self).__init__(tag       = self.TAG,
+                                             parent    = parent,
+                                             node      = node,
+                                             traits    = traits)
+        if parent is not None:
+            parent.remove(self)
 
+    
 # --------------------------------------------------------------------
-def add_prototypes(root,elem,*defs):
-    '''Defs are existing definition elements'''
-    p = add_node(root,elem,MODULE+'PrototypesContainer')
-    
-    for d in defs:
-        c = d.cloneNode(True)
-        p.appendChild(c)
+class PieceSlot(WithTraits):
+    TAG = Element.WIDGET+'PieceSlot'
+    def __init__(self,parent,node=None,
+                 entryName      = '',
+                 traits         = [],
+                 gpid           = 0,
+                 height         = 72,
+                 width          = 72,
+                 icon           = ''):
+        '''A piece slot.  Used all the time.
 
-    return p
+        Parameters
+        ----------
+        parent : Element
+            Parent to add this to
+        node : xml.minidom.Element
+            If not None, XML element to read definition from.
+            Rest of the arguments are ignored if not None.
+        entryName : str
+            Name of this
+        traits : list of Trait objects
+            The traits to set on this object
+        gpid : int
+            Global Piece identifier. If 0, will be set by Game
+        height : int
+            Height size of the piece (in pixels)
+        width : int
+            Width size of the piece (in pixels)
+        icon : str
+            Piece image file name within 'image' sub-dir of archive
+        '''
+        super(PieceSlot,self).\
+            __init__(parent,self.TAG,node=node,
+                     traits    = traits,
+                     entryName = entryName,
+                     gpid      = gpid,
+                     height    = height,
+                     width     = width,
+                     icon      = icon)
+
+    def clone(self,parent):
+        '''Adds copy of self to parent, possibly with new GPID'''
+        game  = self.getParentOfClass([Game])
+        gpid  = game.nextPieceSlotId()
+        #opid  = int(self.getAttribute('gpid'))
+        #print(f'Using GPID={gpid} for clone {opid}')
         
+        node  = self.cloneNode(parent)
+        piece = PieceSlot(parent,node=node)
+        piece.setAttribute('gpid',gpid)
+        
+        traits = piece.getTraits()
+        for trait in traits:
+            if isinstance(trait,BasicTrait):
+                trait['gpid'] = gpid
+
+        piece.setTraits(*traits)
+        return piece
+        
+
 # --------------------------------------------------------------------
-def add_prototype(root,elem,name,code,description=''):
-    d = add_node(root,elem,MODULE+'PrototypeDefinition',
-                 name        = name,
-                 description = description)
-    add_text(root,d,code)
-    return d
+class Prototype(WithTraits):
+    TAG = Element.MODULE+'PrototypeDefinition'
+    def __init__(self,cont,node=None,
+                 name          = '',
+                 traits        = [],
+                 description   = ''):
+        '''A prototype.  Used all the time.
+
+        Parameters
+        ----------
+        cont : Element
+            Parent to add this to
+        node : xml.minidom.Element
+            If not None, XML element to read definition from.
+            Rest of the arguments are ignored if not None.
+        name : str
+            Name of this
+        traits : list of Trait objects
+            The traits to set on this object
+        description : str
+            A free-form description of this prototype
+        '''
+        super(Prototype,self).__init__(cont,self.TAG,node=node,
+                                       traits      = traits,
+                                       name        = name,
+                                       description = description)
     
+#
+# EOF
+#
+# ====================================================================
+# From traits/dynamicproperty.py
+
 # --------------------------------------------------------------------
-def add_piecewindow(root,elem,name,
-                    defaultWidth = 0,
-                    hidden       = False,
-                    hotkey       = key('C',ALT),
-                    scale        = 1.,
-                    text         = '',
-                    tooltip      = 'Show/hide piece window',
-                    icon         = '/images/counter.gif'):
-    return add_node(root,elem,MODULE+'PieceWindow',
-                    name = name,
-                    defaultWidth = defaultWidth,
-                    hidden       = hidden,
-                    hotkey       = hotkey,
-                    scale        = scale,
-                    text         = text,
-                    tooltip      = tooltip,
-                    icon         = icon)
+# Base class for property (piece or global) change traits.  Encodes
+# constraints and commands.
+class ChangePropertyTrait(Trait):
+    DIRECT = 'P'
+    def __init__(self,
+                 *commands,
+                 numeric     = False,
+                 min         = 0,
+                 max         = 100,
+                 wrap        = False):
+        # assert name is not None and len(name) > 0, \
+        #     'No name specified for ChangePropertyTriat'
+        super(ChangePropertyTrait,self).__init__()
+        self._constraints = self.encodeConstraints(numeric,wrap,min,max)
+        self._commands    = self.encodeCommands(commands)
 
+    def encodeConstraints(self,numeric,wrap,min,max):
+        isnum             = f'{numeric}'.lower()
+        iswrap            = f'{wrap}'.lower()
+        return f'{isnum},{min},{max},{iswrap}'
+
+    def decodeConstraints(self,constraints):
+        f = Trait.decodeKeys(constraints)
+        return f[0]=='true',f[3]=='true',int(f[1]),int(f[2])
+    
+    def encodeCommands(self,commands):
+        cmds              = []
+        for cmd in commands:
+            # print(cmd)
+            com = cmd[0] + ':' + cmd[1].replace(',',r'\,') + ':' + cmd[2]
+            if cmd[2] == self.DIRECT:
+                com += f'\,'+cmd[3].replace(':',r'\:')
+            cmds.append(com)
+        return ','.join(cmds)
+
+    def decodeCommands(self,commands):
+        cmds = Trait.decodeKeys(commands)
+        ret  = []
+        for cmd in cmds:
+            parts = Trait.decodeKeys(cmd,':')
+            # print('parts',parts)
+            if parts[-1][0] == self.DIRECT:
+                parts = parts[:-1]+Trait.decodeKeys(parts[-1],',')
+            ret.append(parts)
+        # print(commands,parts)
+        return ret
+    
+    def getCommands(self):
+        return self.decodeCommands(self['commands'])
+
+    def setCommands(self,commands):
+        self['commands'] = self.encodeCommands(commands)
+        
+    def check(self):
+        assert len(self['name']) > 0,\
+            f'No name given for ChangePropertyTrait'
+        
+        
 # --------------------------------------------------------------------
-def add_tabs(root,elem,entryName):
-    return add_node(root,elem,WIDGET+'TabWidget',entryName=entryName)
+class DynamicPropertyTrait(ChangePropertyTrait):
+    ID = 'PROP'
+    def __init__(self,
+                 *commands,
+                 name        = '',
+                 value       = 0,
+                 numeric     = False,
+                 min         = 0,
+                 max         = 100,
+                 wrap        = False,
+                 description = ''):
+        '''Commands are
 
+            - menu
+            - key
+            - Type (only 'P' for now)
+            - Expression
+        '''
+        super(DynamicPropertyTrait,self).__init__(*commands,
+                                                  numeric = numeric,
+                                                  min     = min,
+                                                  max     = max,
+                                                  wrap    = wrap)
+        # print(commands,'Name',name)
+        self.setType(name        = name,
+                     constraints = self._constraints,
+                     commands    = self._commands,
+                     description = description)
+        self.setState(value=value)
+
+    
+Trait.known_traits.append(DynamicPropertyTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalproperty.py
+
 # --------------------------------------------------------------------
-def add_list(root,elem,
-             entryName,
-             height  = 0,
-             width   = 0,
-             scale   = 1.,
-             divider = 100):
-    return add_node(root,elem,WIDGET+'ListWidget',
-                    entryName = entryName,
-                    height    = height,
-                    width     = width,
-                    scale     = scale,
-                    divider   = divider)
+class GlobalPropertyTrait(ChangePropertyTrait):
+    # The real value of CURRENT_ZONE causes problems when copying the
+    # trait, since it contains slashes.  Maybe a solition is to make
+    # it a raw string with escaped slashes?  No, that's already done
+    # below when setting the type.  However, the default in the Java
+    # code is the CURRENT_ZONE real value, so setting this to the
+    # empty string should make it be that value.
+    ID = 'setprop'
+    CURRENT_ZONE = 'Current Zone/Current Map/Module'
+    NAMED_ZONE   = 'Named Zone'
+    NAMED_MAP    = 'Named Map'
+    DIRECT       = 'P'
+    def __init__(self,
+                 *commands,
+                 name        = '',
+                 numeric     = False,
+                 min         = 0,
+                 max         = 100,
+                 wrap        = False,
+                 description = '',
+                 level       = CURRENT_ZONE,
+                 search      = ''):
+        '''Commands are
 
+            - menu
+            - key
+            - Type (only 'P' for now)
+            - Expression
+        '''
+        super(GlobalPropertyTrait,self).__init__(*commands,
+                                                 numeric = numeric,
+                                                 min     = min,
+                                                 max     = max,
+                                                 wrap    = wrap)
+        self.setType(name        = name,
+                     constraints = self._constraints,
+                     commands    = self._commands,
+                     description = description,
+                     level       = level.replace('/',r'\/'),
+                     search      = search)
+        self.setState()
+
+Trait.known_traits.append(GlobalPropertyTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/prototype.py
+
 # --------------------------------------------------------------------
-def add_panel(root,elem,
-              entryName,
-              fixed = False,
-              nColumns = 1,
-              vert     = False):
-    return add_node(root,elem,WIDGET+'PanelWidget',
-                    entryName = entryName,
-                    fixed     = fixed,
-                    nColumns  = nColumns,
-                    vert      = vert)
+class PrototypeTrait(Trait):
+    ID = 'prototype'
+    def __init__(self,name=''):
+        '''Create a prototype trait (VASSAL.counter.UsePrototype)'''
+        super(PrototypeTrait,self).__init__()
+        self.setType(name = name)
+        self.setState(ignored = '')
 
+
+Trait.known_traits.append(PrototypeTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/place.py
+
 # --------------------------------------------------------------------
-def add_mapwidget(root,elem,entryName=''):
-    return add_node(root,elem,WIDGET+'MapWidget',entryName=entryName)
+class PlaceTrait(Trait):
+    ID      = 'placemark'
+    STACK_TOP = 0
+    STACK_BOTTOM = 1
+    ABOVE = 2
+    BELOW = 3
 
+    # How the LaTeX exporter organises the units.  Format with
+    # 0: the group
+    # 1: the piece name 
+    # SKEL_PATH = (PieceWindow.TAG +r':Counters\/'        +
+    #              TabWidget.TAG   +r':Counters\/'        +
+    #              PanelWidget.TAG +':{0}'         +r'\/'+
+    #              ListWidget.TAG  +':{0} counters'+r'\/'+
+    #              PieceSlot.TAG   +':{1}')
+    @classmethod
+    @property
+    def SKEL_PATH(cls):
+
+        return (PieceWindow.TAG +r':Counters\/'        +
+                TabWidget.TAG   +r':Counters\/'        +
+                PanelWidget.TAG +':{0}'         +r'\/'+
+                ListWidget.TAG  +':{0} counters'+r'\/'+
+                PieceSlot.TAG   +':{1}')
+    
+    def __init__(self,
+                 command         = '', # Context menu name
+                 key             = '', # Context menu key
+                 markerSpec      = '', # Full path in module
+                 markerText      = 'null', # Hard coded message
+                 xOffset         = 0,
+                 yOffset         = 0,
+                 matchRotation   = True,
+                 afterKey        = '',
+                 description     = '',
+                 gpid            = '', # Set in JAVA, but with warning
+                 placement       = ABOVE,
+                 above           = False):
+        '''Create a place marker trait (VASSAL.counter.PlaceMarker)'''
+        super(PlaceTrait,self).__init__()
+        self.setType(command         = command,          # Context menu name
+                     key             = key,              # Context menu key
+                     markerSpec      = markerSpec,
+                     markerText      = markerText,
+                     xOffset         = xOffset,
+                     yOffset         = yOffset,
+                     matchRotation   = matchRotation,
+                     afterKey        = afterKey,
+                     description     = description,
+                     gpid            = gpid,
+                     placement       = placement,
+                     above           = above)
+        self.setState()
+
+Trait.known_traits.append(PlaceTrait)
+
 # --------------------------------------------------------------------
-def add_chartwindow(root,elem,
-                    name,
-                    hotkey      = key('A',ALT),
-                    description = '',
-                    text        = '',
-                    tooltip     = 'Show/hide Charts',
-                    icon        = '/images/chart.gif'):
-    return add_node(root,elem,MODULE+'ChartWindow',
-                    name        = name,
-                    hotkey      = hotkey,
-                    description = description,
-                    text        = text,
-                    tooltip     = tooltip,
-                    icon        = icon)
+class ReplaceTrait(PlaceTrait):
+    ID = 'replace'
+    def __init__(self,
+                 command         = '', # Context menu name
+                 key             = '', # Context menu key
+                 markerSpec      = '', # Full path in module
+                 markerText      = 'null', # Hard message
+                 xOffset         = 0,
+                 yOffset         = 0,
+                 matchRotation   = True,
+                 afterKey        = '',
+                 description     = '',
+                 gpid            = '', # Set in JAVA
+                 placement       = PlaceTrait.ABOVE,
+                 above           = False):
+        super(ReplaceTrait,self).__init__(command         = command, 
+                                          key             = key,  
+                                          markerSpec      = markerSpec,
+                                          markerText      = markerText,
+                                          xOffset         = xOffset,
+                                          yOffset         = yOffset,
+                                          matchRotation   = matchRotation,
+                                          afterKey        = afterKey,
+                                          description     = description,
+                                          gpid            = gpid,
+                                          placement       = placement,
+                                          above           = above)
+    
 
+Trait.known_traits.append(ReplaceTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/report.py
+
 # --------------------------------------------------------------------
-def add_chart(root,elem,
-              chartName,
-              fileName,
-              description = ''):
-    return add_node(root,elem,WIDGET+'Chart',
-                    chartName   = chartName,
-                    description = description,
-                    fileName    = fileName)
+class ReportTrait (Trait):
+    ID = 'report'
+    def __init__(self,
+                 *keys,
+                 nosuppress = True,
+                 description = '',
+                 report      = '$location$: $newPieceName$ $menuCommand$ *',
+                 cyclekeys   = '',
+                 cyclereps   = ''):
+        '''Create a report trait (VASSAL.counters.ReportActon)'''
+        super(ReportTrait,self).__init__()
+        esckeys = ','.join([k.replace(',',r'\,') for k in keys])
+        esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
+        escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
+        
+        self.setType(keys         = esckeys,
+                     report       = report,
+                     cycleKeys    = esccycl,
+                     cycleReports = escreps,
+                     description  = description,
+                     nosuppress   = nosuppress)
+        self.setState(cycle = -1)
 
+Trait.known_traits.append(ReportTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/calculatedproperty.py
+
 # --------------------------------------------------------------------
-def add_dice(root,elem,
-             addToTotal           = 0,
-             canDisable           = False,
-             hotkey               = key('6',ALT),
-             icon                 = '/images/die.gif',
-             keepCount            = 1,
-             keepDice             = False,
-             keepOption           = '>',
-             lockAdd              = False,
-             lockDice             = False,
-             lockPlus             = False,
-             lockSides            = False,
-             nDice                = 1,
-             nSides               = 6,
-             name                 = '1d6',
-             plus                 = 0,
-             prompt               = False,
-             propertyGate         = '',
-             reportFormat         = '** $name$ = $result$ *** &lt;$PlayerName$&gt;',
-             reportTotal          = False,
-             sortDice             = False,
-             text                 = '1d6',
-             tooltip              = 'Roll a 1d6'):
-    return add_node(root,elem,MODULE+'DiceButton',
-                    addToTotal           = addToTotal,
-                    canDisable           = canDisable,
-                    hotkey               = hotkey,
-                    icon                 = icon,
-                    keepCount            = keepCount,
-                    keepDice             = keepDice,
-                    keepOption           = keepOption,
-                    lockAdd              = lockAdd,
-                    lockDice             = lockDice,
-                    lockPlus             = lockPlus,
-                    lockSides            = lockSides,
-                    nDice                = nDice,
-                    nSides               = nSides,
-                    name                 = name,
-                    plus                 = plus,
-                    prompt               = prompt,
-                    propertyGate         = propertyGate,
-                    reportFormat         = reportFormat,
-                    reportTotal          = reportTotal,
-                    sortDice             = sortDice,
-                    text                 = text,
-                    tooltip              = tooltip)
+class CalculatedTrait(Trait):
+    ID = 'calcProp'
+    def __init__(self,name='',expression='',description=''):
+        '''Define a trait that calculates a property'''
+        super(CalculatedTrait,self).__init__()
+        self.setType(name        = name,
+                     expression  = expression,
+                     description = description)
+        self.setState()
 
+
+Trait.known_traits.append(CalculatedTrait)
+
+#
+# EOF
+#
 # ====================================================================
-def enc_def(args,term=False):
-    '''Encode a piece (decorator) definition
+# From traits/restrictcommand.py
 
-    Parameters
-    ----------
-    args : list
-        List of fields
-    term : bool
-        If true, add terminating ';'
+# --------------------------------------------------------------------
+class RestrictCommandsTrait(Trait):
+    ID = 'hideCmd'
+    HIDE = 'Hide'
+    DISABLE = 'Disable'
+    def __init__(self,
+                 name          = '',
+                 hideOrDisable = HIDE,
+                 expression    = '',# Restrict when true
+                 keys          = []):
+        '''Create a layer trait (VASSAL.counter.RestrictCommands)'''
+        super(RestrictCommandsTrait,self).__init__()
+        encKeys = ','.join([k.replace(',',r'\,') for k in keys])
+        self.setType(name          = name,
+                     hideOrDisable = hideOrDisable,
+                     expression    = expression,
+                     keys          = encKeys)
+        self.setState(state='')
+    def setKeys(self,keys):
+        self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
+    
 
-    Returns
-    -------
-    code : str
-        The encoded definition
-    '''
-    return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
-                     for e in args])+(';' if term else '')
+Trait.known_traits.append(RestrictCommandsTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/label.py
+
+class LabelTraitCodes:
+    TOP    = 't'
+    BOTTOM = 'b'
+    CENTER = 'c'
+    LEFT   = 'l'
+    RIGHT  = 'r'
+    PLAIN  = 0
+    BOLD   = 1
+    ITALIC = 2
+    
 # --------------------------------------------------------------------
-def layer_trait(images,
-                newNames     = None,
-                activateName = 'Activate',
-                activateMask = CTRL,
-                activateChar = 'A',
-                increaseName = 'Increase',
-                increaseMask = CTRL,
-                increaseChar = '[',
-                decreaseName = '',
-                decreaseMask = CTRL,
-                decreaseChar  = ']',
-                resetName    = '',
-                resetKey     = '',
-                resetLevel   = 1,
-                under        = False,
-                underXoff    = 0,
-                underYoff    = 0,
-                loop         = True,
-                name         = '',
-                description  = '',
-                randomKey    = '',
-                randomName   = '',
-                follow       = False,
-                expression   = '',
-                first        = 1,
-                version      = 1, # 1:new, 0:old
-                always       = 'true',
-                activateKey  = key('A'),
-                increaseKey  = key('['),
-                decreaseKey  = key(']'),
-                scale        = 1.):
-    '''Create a layer trait (VASSAL.counter.Embellishment)
+class LabelTrait(Trait):
+    ID     = 'label'
+    def __init__(self,
+                 label           = None,
+                 labelKey        = '',
+                 menuCommand     ='Change label',
+                 fontSize        = 10,
+                 background      = 'none',
+                 foreground      = '255,255,255',
+                 vertical        = LabelTraitCodes.TOP,
+                 verticalOff     = 0,
+                 horizontal      = LabelTraitCodes.CENTER,
+                 horizontalOff   = 0,
+                 verticalJust    = LabelTraitCodes.BOTTOM,
+                 horizontalJust  = LabelTraitCodes.CENTER,
+                 nameFormat      = '$pieceName$ ($label$)',
+                 fontFamily      = 'Dialog',
+                 fontStyle       = LabelTraitCodes.PLAIN,
+                 rotate          = 0,
+                 propertyName    = 'TextLabel',
+                 description     = '',
+                 alwaysUseFormat = False):
+        '''Create a label trait (can be edited property)'''
+        super(LabelTrait,self).__init__()
+        self.setType(labelKey		= labelKey,
+                     menuCommand	= menuCommand,
+                     fontSize		= fontSize,
+                     background		= background,
+                     foreground		= foreground,
+                     vertical		= vertical,
+                     verticalOff	= verticalOff,
+                     horizontal		= horizontal,
+                     horizontalOff	= horizontalOff,
+                     verticalJust	= verticalJust,
+                     horizontalJust	= horizontalJust,
+                     nameFormat		= nameFormat,
+                     fontFamily		= fontFamily,
+                     fontStyle		= fontStyle,
+                     rotate		= rotate,
+                     propertyName	= propertyName,
+                     description	= description,
+                     alwaysUseFormat	= alwaysUseFormat)
+        self.setState(label = (nameFormat if label is None else label))
 
-    Parameters
-    ----------
-    main : dict
-        Main image dictionary
-    flipped : dict
-        Flipped image dictionary
 
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state
-    '''
-    if newNames is None:
-        newNames = ['']*len(images)
-    args = ['emb2',                # Code
-            activateName, activateMask, activateChar,    # ACTIVATE ; MASK ; KEY
-            increaseName, increaseMask, increaseChar,
-            decreaseName, decreaseMask, decreaseChar,
-            resetName,    resetKey,     resetLevel,
-            under,        underXoff,    underYoff,
-            ','.join(images),
-            ','.join(newNames),
-            loop,
-            name,
-            randomKey, randomName,
-            follow,    expression,
-            first,
-            version,
-            always,
-            activateKey,
-            increaseKey,
-            decreaseKey,
-            description,
-            scale ]
-    stat = '1' # CURRENT LEVEL
-    return enc_def(args), stat
+Trait.known_traits.append(LabelTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/layer.py
+
 # --------------------------------------------------------------------
-def prototype_trait(name):
-    '''Create a prototype trait (VASSAL.counter.UsePrototype)
+class LayerTrait(Trait):
+    ID = 'emb2'
+    def __init__(self,
+                 images       = [''],
+                 newNames     = None,
+                 activateName = 'Activate',
+                 activateMask = CTRL,
+                 activateChar = 'A',
+                 increaseName = 'Increase',
+                 increaseMask = CTRL,
+                 increaseChar = '[',
+                 decreaseName = '',
+                 decreaseMask = CTRL,
+                 decreaseChar  = ']',
+                 resetName    = '',
+                 resetKey     = '',
+                 resetLevel   = 1,
+                 under        = False,
+                 underXoff    = 0,
+                 underYoff    = 0,
+                 loop         = True,
+                 name         = '',
+                 description  = '',
+                 randomKey    = '',
+                 randomName   = '',
+                 follow       = False,
+                 expression   = '',
+                 first        = 1,
+                 version      = 1, # 1:new, 0:old
+                 always       = True,
+                 activateKey  = key('A'),
+                 increaseKey  = key('['),
+                 decreaseKey  = key(']'),
+                 scale        = 1.):
+        '''Create a layer trait (VASSAL.counter.Embellishment)'''
+        super(LayerTrait,self).__init__()
+        if newNames is None and images is not None:
+            newNames = ['']*len(images)
+        self.setType(
+            activateName        = activateName,
+            activateMask        = activateMask,
+            activateChar        = activateChar,
+            increaseName        = increaseName,
+            increaseMask        = increaseMask,
+            increaseChar        = increaseChar,
+            decreaseName        = decreaseName,
+            decreaseMask        = decreaseMask,
+            decreaseChar        = decreaseChar,
+            resetName           = resetName,
+            resetKey            = resetKey,
+            resetLevel          = resetLevel,
+            under               = under,
+            underXoff           = underXoff,
+            underYoff           = underYoff,
+            images              = ','.join(images),
+            newNames            = ','.join(newNames),
+            loop                = loop,
+            name                = name,
+            randomKey           = randomKey,
+            randomName          = randomName,
+            follow              = follow,
+            expression          = expression,
+            first               = first,
+            version             = version,
+            always              = always,
+            activateKey         = activateKey,
+            increaseKey         = increaseKey,
+            decreaseKey         = decreaseKey,
+            description         = description,
+            scale               = scale)
+        self.setState(level=1)
 
-    Parameters
-    ----------
-    family : str
-        Side in game
+Trait.known_traits.append(LayerTrait)
 
-    Return
-    ------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['prototype', name ]
-    stat = '' # State is nothing 
-    return enc_def(args), stat
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalcommand.py
 
 # --------------------------------------------------------------------
-def mark_trait(key,value):
-    '''Create a marker trait (VASSAL.counter.Marker)
+class GlobalCommandTrait(Trait):
+    ID = 'globalkey'
+    def __init__(self,
+                 commandName   = '',
+                 key           = '', # Command received
+                 globalKey     = '', # Command to send to targets
+                 properties    = '', # Filter target on this expression
+                 ranged        = False,
+                 range         = 1,
+                 reportSingle  = True,
+                 fixedRange    = True,
+                 rangeProperty = '',
+                 description   = '',
+                 deskSelect    = '-1',
+                 target        = ''):
+        '''Create a global key command in piece
+        (VASSAL.counters.CounterGlobalKeyCommand)'''
+        self.setType(commandName   = commandName,
+                     key           = key,
+                     globalKey     = globalKey,
+                     properties    = properties,
+                     ranged        = ranged,
+                     range         = range,
+                     reportSingle  = reportSingle,
+                     fixedRange    = fixedRange,
+                     rangeProperty = rangeProperty,
+                     description   = description,
+                     deskSelect    = deskSelect,
+                     target        = target)
+        self.setState()
+        
+Trait.known_traits.append(GlobalCommandTrait)
 
-    Parameters
-    ----------
-    family : str
-        Side in game
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalhotkey.py
 
-    Return
-    ------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['mark', key ]
-    stat = value # State is nothing 
-    return enc_def(args), stat
+# --------------------------------------------------------------------
+class GlobalHotkeyTrait(Trait):
+    ID = 'globalhotkey'
+    def __init__(self,
+                 name          = '', # Command received
+                 key           = '', # Command key received
+                 globalHotkey  = '', # Key to send
+                 description   = ''):
+        '''Create a global key command in piece
+        (VASSAL.counters.GlobalHotkey)'''
+        self.setType(name          = name,
+                     key           = key,
+                     globalHotkey  = globalHotkey,
+                     description   = description)
+        self.setState()
+        
+Trait.known_traits.append(GlobalHotkeyTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/nostack.py
+
 # --------------------------------------------------------------------
-def report_trait(*keys,
-                 nosuppress = True,
+class NoStackTrait(Trait):
+    ID = 'immob'
+    NORMAL_SELECT         = ''
+    SHIFT_SELECT          = 'i'
+    CTRL_SELECT           = 't'
+    ALT_SELECT            = 'c'
+    NEVER_SELECT          = 'n'
+    NORMAL_BAND_SELECT    = ''
+    ALT_BAND_SELECT       = 'A'
+    ALT_SHIFT_BAND_SELECT = 'B'
+    NEVER_BAND_SELECT     = 'Z'
+    NORMAL_MOVE           = 'N'
+    SELECT_MOVE           = 'I'
+    NEVER_MOVE            = 'V'
+    NORMAL_STACK          = 'L'
+    NEVER_STACK           = 'R'
+    IGNORE_GRID           = 'g'
+    def __init__(self,
+                 select      = NORMAL_SELECT,
+                 bandSelect  = NORMAL_BAND_SELECT,
+                 move        = NORMAL_MOVE,
+                 canStack    = False,
+                 ignoreGrid  = False,
+                 description = ''):
+
+        selectionOptions = (select +
+                            (self.IGNORE_GRID if ignoreGrid else '') +
+                            bandSelect)
+        movementOptions  = move
+        stackingOptions  = self.NORMAL_STACK if canStack else self.NEVER_STACK
+                 
+        '''Create a mark trait (static property)'''
+        super(NoStackTrait,self).__init__()
+        
+        self.setType(selectionOptions = selectionOptions,
+                     movementOptions  = movementOptions,
+                     stackingOptions  = stackingOptions,
+                     description      = description)
+        self.setState()
+
+
+Trait.known_traits.append(NoStackTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/deselect.py
+
+# --------------------------------------------------------------------
+class DeselectTrait(Trait):
+    ID = 'deselect'
+    THIS = 'D' # Deselect only this piece
+    ALL  = 'A' # Deselect all pieces 
+    ONLY = 'S' # Select this piece only
+    def __init__(self,
+                 command     = '',
+                 key         = '',
                  description = '',
-                 report      = '$location$: $newPieceName$ $menuCommand$ *',
-                 cyclekeys   = '',
-                 cyclereps   = ''):
-    '''Create a report trait (VASSAL.counters.ReportActon)
+                 unstack     = False,
+                 deselect    = THIS):
+        '''Create a deselect trait'''
+        super(DeselectTrait,self).__init__()
+        self.setType(command     = command,
+                     key         = key,
+                     description = description,
+                     unstack     = unstack,
+                     deselect    = deselect)
+        self.setState()
 
-    Parameters
-    ----------
-    *keys : tuple of str
-    	The keys to report actions for
-    
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    esckeys = ','.join([k.replace(',',r'\,') for k in keys])
-    esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
-    escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
-    args = ['report',    # CODE
-            esckeys,     # ESCAPED KEYS (commas escaped)
-            report,      # REPORT
-            esccycl,     # CYCLE KEYS (commas escaped)
-            escreps,     # CYCKE REPORTS (commas escaped)
-            description, # DESCRIPTION
-            nosuppress]  # NOSUPPRESS
-    stat = '-1'          # CYCLE STATE
-    return enc_def(args), stat
 
+Trait.known_traits.append(DeselectTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/restrictaccess.py
+
 # --------------------------------------------------------------------
-def moved_trait(image='moved.gif',
-                xoff = 36,
-                yoff = -38,
-                name = 'Mark Moved',
-                key  = key('M')):
-    '''Create a moved trait (VASSAL.counters.MovementMarkable)
+class RestrictAccessTrait(Trait):
+    ID = 'restrict'
+    def __init__(self,
+                 sides         = [],
+                 byPlayer      = False,
+                 noMovement    = True,
+                 description   = '',
+                 owner         = '',):
+        '''Create a layer trait (VASSAL.counter.Restricted)'''
+        super(RestrictAccessTrait,self).__init__()
+        encSides = ','.join(sides)
+        self.setType(sides         = encSides,
+                     byPlayer      = byPlayer,
+                     noMovement    = noMovement,
+                     description   = description)
+        self.setState(owner=owner)
 
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['markmoved',    # CODE
-            image,          # IMAGE
-            xoff, yoff,     # XOFF ; YOFF
-            name,           # MENU
-            key,            # KEY, MODIFIER
-            '']             # ?
-    stat = False          # MOVED?
-    return enc_def(args), stat
+Trait.known_traits.append(RestrictAccessTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/rotate.py
+
 # --------------------------------------------------------------------
-def trail_trait(key             = key('T'),
-                name            = 'Movement Trail',
-                localVisible    = False,
-                globalVisible   = False,
-                radius          = 10,
-                fillColor       = rgb(255,255,255),
-                lineColor       = rgb(0,0,0),
-                activeOpacity   = 100,
-                inactiveOpacity = 50,
-                edgesBuffer     = 20,
-                displayBuffer   = 30,
-                lineWidth       = 1,
-                turnOn          = '',
-                turnOff         = '',
-                reset           = '',
-                description     = ''):
-    ''' Create a movement trail trait ( VASSAL.counters.Footprint)
-    
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state 
-    '''
-    args = ['footprint',       # CODE
-            key,               # ENABLE KEY
-            name,              # MENU 
-            localVisible,      # LOCAL VISABLE
-            globalVisible,     # GLOBAL VISABLE
-            radius,            # RADIUS
-            fillColor,         # FILL COLOR
-            lineColor,         # LINE COLOR 
-            activeOpacity,     # ACTIVE OPACITY
-            inactiveOpacity,   # INACTIVE OPACITY
-            edgesBuffer,       # EDGES BUFFER
-            displayBuffer,     # DISPLAY BUFFER
-            lineWidth,         # LINE WIDTH 
-            turnOn,            # TURN ON KEY
-            turnOff,           # TURN OFF KEY
-            reset,             # RESET KEY
-            description]       # DESC
-    stat = [False,             # GLOBAL VIS
-            '',                # MAP
-            0]                 # POINTS (followed by [; [X,Y]*]
-    return enc_def(args), enc_def(stat)
+class RotateTrait(Trait):
+    ID = 'rotate'
+    def __init__(self,
+                 nangles          = 6,
+                 rotateCWKey      = key(']'),
+                 rotateCCWKey     = key('['),
+                 rotateCW         = 'Rotate CW',
+                 rotateCCW        = 'Rotate CCW',
+                 rotateRndKey     = '',
+                 rotateRnd        = '',
+                 name             = 'Rotate',
+                 description      = 'Rotate piece',
+                 rotateDirectKey  = '',
+                 rotateDirect     = '',
+                 directExpression = '',
+                 directIsFacing   = True,
+                 angle            = 0):
+        '''Create a Rotate trait'''
+        super(RotateTrait,self).__init__()
+        if nangles == 1:
+            self.setType(nangles          = nangles,
+                         rotateKey        = rotateCWKey,
+                         rotate           = rotateCW,
+                         rotateRndKey     = rotateRndKey,
+                         rotateRnd        = rotateRnd,
+                         name             = name,
+                         description      = description,
+                         rotateDirectKey  = rotateDirectKey,
+                         rotateDirect     = rotateDirect,
+                         directExpression = directExpression,
+                         directIsFacing   = directIsFacing)
+        else:
+            self.setType(nangles          = nangles,
+                         rotateCWKey      = rotateCWKey,
+                         rotateCCWKey     = rotateCCWKey,
+                         rotateCW         = rotateCW,
+                         rotateCCW        = rotateCCW,
+                         rotateRndKey     = rotateRndKey,
+                         rotateRnd        = rotateRnd,
+                         name             = name,
+                         description      = description,
+                         rotateDirectKey  = rotateDirectKey,
+                         rotateDirect     = rotateDirect,
+                         directExpression = directExpression,
+                         directIsFacing   = directIsFacing)
+            
+        self.setState(angle = int(angle) if nangles > 1 else float(angle))
 
+Trait.known_traits.append(RotateTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/stack.py
+
 # --------------------------------------------------------------------
-def delete_trait(name = 'Delete', key = key('D')):
-    '''Create a delete trait (VASSAL.counters.Delete)
+class StackTrait(Trait):
+    ID = 'stack'
+    def __init__(self,
+                 board     = '',
+                 x         = '',  
+                 y         = '',  
+                 pieceIds  = [],
+                 layer     = -1): 
+        '''Create a stack trait in a save file'''
+        self.setType()       # NAME
+        # print('Piece IDs:',pieceIds)
+        self.setState(board      = board,
+                      x          = x,
+                      y          = y,
+                      pieceIds   = ';'.join([str(p) for p in pieceIds]),
+                      layer      = f'@@{layer}')
+        
+Trait.known_traits.append(StackTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/mark.py
+
+# --------------------------------------------------------------------
+class MarkTrait(Trait):
+    ID = 'mark'
+    def __init__(self,name='',value=''):
+        '''Create a mark trait (static property)'''
+        super(MarkTrait,self).__init__()
+        self.setType(name = name)
+        self.setState(value = value)
+
+
+Trait.known_traits.append(MarkTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/mask.py
+
+# --------------------------------------------------------------------
+# Inset
+# obs;88,130;ag_hide_1.png;Reveal;I;?;sides:Argentine;Peek;;true;;
+# obs;88,130;ag_hide_1.png;Reveal;I;?;side:Argentine;;;true;;
+#
+# Peek
+# obs;88,130;ag_hide_1.png;Reveal;P89,130;?;sides:Argentine;Peek;;true;;
+#
+# Image
+#
+class MaskTrait(Trait):
+    ID = 'obs'
+    INSET = 'I'
+    BACKGROUND = 'B'
+    PEEK = 'P'
+    IMAGE = 'G'
+    INSET2 = '2'
+    PLAYER = 'player:'
+    SIDE = 'side:'
+    SIDES = 'sides:'
+    def __init__(self,
+                 keyCommand   = '',
+                 imageName    = '',
+                 hideCommand  = '',
+                 displayStyle = '',
+                 peekKey      = '',
+                 ownerImage   = '',
+                 maskedName   = '?',
+                 access       = '',#?
+                 peekCommand  = '',
+                 description  = '',
+                 autoPeek     = True,
+                 dealKey      = '',
+                 dealExpr     = ''):
+        '''Create a mark trait (static property)'''
+        super(MaskTrait,self).__init__()
+        disp = displayStyle
+        if displayStyle == self.PEEK:
+            disp += peekKey
+        elif displayStyle == self.IMAGE:
+            disp += ownerImage
+            
+        acc = self.PLAYER
+        if isinstance(access,list):
+            acc = self.SIDES + ':'.join(access)
+        elif access.startswith('player'):
+            acc = self.PLAYER
+        elif access.startswith('side'):
+            acc = self.SIDE
+                
+        self.setType(keyCommand   = keyCommand,
+                     imageImage   = imageName,
+                     hideCommand  = hideCommand,
+                     displayStyle = disp,
+                     maskedName   = maskedName,
+                     access       = acc, # ?
+                     peekCommand  = peekCommand,
+                     description  = description,
+                     autoPeek     = autoPeek,
+                     dealKey      = dealKey,
+                     dealExpr     = dealExpr)
+        self.setState(value='null')
+
+    @classmethod
+    def peekDisplay(cls,key):#Encoded key
+        return cls.PEEK + key
     
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['delete', name, key, '']
-    stat = '' # Nothing
-    return enc_def(args), stat
+    @classmethod
+    def peekImage(cls,ownerImage):
+        return cls.IMAGE + ownerImage
 
+    @classmethod
+    def sides(cls,*names):
+        return cls.SIDES+':'.join(names)
+
+Trait.known_traits.append(MaskTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/trail.py
+
 # --------------------------------------------------------------------
-def sendto_trait(mapName,
-                 boardName,
+class TrailTrait(Trait):
+    ID = 'footprint'
+    def __init__(self,
+                 key             = key('T'),
+                 name            = 'Movement Trail',
+                 localVisible    = False,
+                 globalVisible   = False,
+                 radius          = 10,
+                 fillColor       = rgb(255,255,255),
+                 lineColor       = rgb(0,0,0),
+                 activeOpacity   = 100,
+                 inactiveOpacity = 50,
+                 edgesBuffer     = 20,
+                 displayBuffer   = 30,
+                 lineWidth       = 1,
+                 turnOn          = '',
+                 turnOff         = '',
+                 reset           = '',
+                 description     = ''):        
+        ''' Create a movement trail trait ( VASSAL.counters.Footprint)'''
+        super(TrailTrait,self).__init__()
+        self.setType(key               = key,# ENABLE KEY
+                     name              = name,# MENU 
+                     localVisible      = localVisible,# LOCAL VISABLE
+                     globalVisible     = globalVisible,# GLOBAL VISABLE
+                     radius            = radius,# RADIUS
+                     fillColor         = fillColor,# FILL COLOR
+                     lineColor         = lineColor,# LINE COLOR 
+                     activeOpacity     = activeOpacity,# ACTIVE OPACITY
+                     inactiveOpacity   = inactiveOpacity,# INACTIVE OPACITY
+                     edgesBuffer       = edgesBuffer,# EDGES BUFFER
+                     displayBuffer     = displayBuffer,# DISPLAY BUFFER
+                     lineWidth         = int(lineWidth),# LINE WIDTH 
+                     turnOn            = turnOn,# TURN ON KEY
+                     turnOff           = turnOff,# TURN OFF KEY
+                     reset             = reset,# RESET KEY
+                     description       = description)       # DESC
+        self.setState(isGlobal  = False,
+                      map       = '',
+                      points    = 0,     # POINTS (followed by [; [X,Y]*]
+                      init      = False) 
+
+Trait.known_traits.append(TrailTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/delete.py
+
+# --------------------------------------------------------------------
+class DeleteTrait(Trait):
+    ID = 'delete'
+    def __init__(self,
+                 name   = 'Delete',
+                 key = key('D')):
+        '''Create a delete trait (VASSAL.counters.Delete)'''
+        super(DeleteTrait,self).__init__()
+        self.setType(name  = name,
+                     key   = key,
+                     dummy = '')
+        self.setState()
+
+Trait.known_traits.append(DeleteTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/sendto.py
+
+# --------------------------------------------------------------------
+class SendtoTrait(Trait):
+    ID = 'sendto'
+    LOCATION = 'L'
+    ZONE     = 'Z'
+    REGION   = 'R'
+    GRID     = 'G'
+    def __init__(self,
+                 mapName     = '',
+                 boardName   = '',
                  name        = '',
                  key         = key('E'),
                  restoreName = 'Restore',
@@ -1891,1772 +6198,5319 @@
                  xoff        = 1,
                  yoff        = 1,
                  description = '',
-                 destination = 'L',
+                 destination = LOCATION,
                  zone        = '',
                  region      = '',
                  expression  = '',
                  position    = ''):
-    '''Create a send to trait (VASSAL.counter.SendToLocation)
+        '''Create a send to trait (VASSAL.counter.SendToLocation)'''
+        self.setType(name           = name,# NAME
+                     key            = key,# KEY , MODIFIER
+                     mapName        = mapName,# MAP
+                     boardName      = boardName,# BOARD
+                     x              = x,
+                     y              = y,# X ; Y
+                     restoreName    = restoreName,# BACK
+                     restoreKey     = restoreKey,# KEY , MODIFIER
+                     xidx           = xidx,
+                     yidx           = yidx,# XIDX ; YIDX
+                     xoff           = xoff,
+                     yoff           = yoff,# XOFF ; YOFF
+                     description    = description,# DESC
+                     destination    = destination,# DEST type
+                     zone           = zone,# ZONE
+                     region         = region,# REGION
+                     expression     = expression,# EXPRESSION
+                     position       = position)                   # GRIDPOS
+        self.setState(backMap = '', backX = '', backY = '')
 
-    Parameters
-    ----------
-    side : str
-        Sub-map to move to
-    
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''
-    args = ['sendto',              # CODE
-            name,           # NAME
-            key,              # KEY , MODIFIER
-            mapName,            # MAP
-            boardName,        # BOARD
-            x, y,              # X ; Y
-            restoreName,             # BACK
-            restoreKey,              # KEY , MODIFIER
-            xidx, yidx,            # XIDX ; YIDX
-            xoff, yoff,                  # XOFF ; YOFF
-            description, # DESC
-            destination,                   # DEST
-            zone,                    # ZONE
-            region,                    # REGION
-            expression,                    # EXPRESSION
-            position ]                   # GRIDPOS
-    stat = ['',                    # BACKMAP 
-            '',                    # X
-            '']                    # Y
-    return enc_def(args), enc_def(stat)
+Trait.known_traits.append(SendtoTrait)
 
+#
+# EOF
+#
+# ====================================================================
+# From traits/moved.py
+
 # --------------------------------------------------------------------
-def basic_trait(name,
-                filename,     # Can be empty
-                gpid,         # Can be empty
-                cloneKey='',  # Deprecated
-                deleteKey=''):# Deprecated
-    '''Create a basic unit (VASSAL.counters.BasicPiece)
+class MovedTrait(Trait):
+    ID = 'markmoved'
+    def __init__(self,
+                 image      = 'moved.gif',
+                 xoff       = 36,
+                 yoff       = -38,
+                 name       = 'Mark moved',
+                 key        = key('M'),
+                 dummy      = ''  # Description
+                 # ignoreSame = True
+                 ):
+        '''Create a moved trait (VASSAL.counters.MovementMarkable)'''
+        super(MovedTrait,self).__init__()
+        self.setType(image    = image,
+                     xoff     = xoff,
+                     yoff     = yoff,
+                     name     = name,
+                     key      = key,
+                     dummy    = dummy, # Description
+                     # ignoreSame = ignoreSame
+                     )
+        self.setState(moved = False)
 
-    Parameters
-    ----------
-    main : dict
-        Information
-    gpid : int
-        Piece global identifier
-    image : bool
-        If true, add image
+Trait.known_traits.append(MovedTrait)
 
-    Returns
-    -------
-    code : str
-        Encoded trait
-    stat : str
-        Encoded state (empty string)
-    '''    
-    args = ['piece',    # CODE
-            cloneKey,   # CLONEKEY
-            deleteKey,  # DELETEKEY
-            filename,   # IMAGE  
-            name]       # NAME
-    stat = ['null',         # MAPID (possibly 'null')
-            0,              # X
-            0,              # Y
-            gpid,           # GLOBAL PIECE ID
-            0]              # PROPERTY COUNT (followed by [; KEY; VALUE]+)
-    return enc_def(args), enc_def(stat)
+#
+# EOF
+#
+# ====================================================================
+# From traits/skel.py
 
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/submenu.py
+
 # --------------------------------------------------------------------
-def enc_parts(*parts):
-    '''Encode parts of a full piece definition
+class SubMenuTrait(Trait):
+    ID = 'submenu'
+    def __init__(self,
+                 subMenu     = '',  # Title
+                 keys        = [],  # Keys
+                 description = ''):
+        '''Create a sub menu (VASSAL.counters.SubMenu)'''
+        self.setType(subMenu     = subMenu,   # CLONEKEY
+                     keys        = ','.join([k.replace(',',r'\,')
+                                             for k in keys]),
+                     description = description)
+        self.setState() # PROPERTY COUNT (followed by [; KEY; VALUE]+)
+    def setKeys(self,keys):
+        '''Set the keys'''
+        self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
+        
+Trait.known_traits.append(SubMenuTrait)
 
-    Each trait (VASSAL.counter.Decorator, VASSAL.counter.BasicPiece)
-    definition or state is separated by a litteral TAB character.
-    Beyond the first TAB separator, additional escape characters
-    (BACKSLAH) are added in front of the separator.  This is to that
-    VASSAL.utils.SequenceDecoder does not see consequitive TABs as a
-    single TAB.
+#
+# EOF
+#
+# ====================================================================
+# From traits/basic.py
 
-    Parameters
-    ----------
-    parts : tuple
-        The various trait definitions or states
+# --------------------------------------------------------------------
+class BasicTrait(Trait):
+    ID = 'piece'
+    def __init__(self,
+                 name      = '',
+                 filename  = '',  # Can be empty
+                 gpid      = '',  # Can be empty
+                 cloneKey  = '',  # Deprecated
+                 deleteKey = ''): # Deprecated
+        '''Create a basic unit (VASSAL.counters.BasicPiece)'''
+        self.setType(cloneKey  = cloneKey,   # CLONEKEY
+                     deleteKey = deleteKey,  # DELETEKEY
+                     filename  = filename,   # IMAGE  
+                     name      = name)       # NAME
+        self.setState(map        = 'null', # MAPID (possibly 'null')
+                      x          = 0,
+                      y          = 0,
+                      gpid       = gpid,
+                      properties = 0) # PROPERTY COUNT (followed by [; KEY; VALUE]+)
+        
+Trait.known_traits.append(BasicTrait)
 
-    Returns
-    -------
-    code : str
-        The encoded piece
-    '''
-    ret = ''
-    sep = r'	'
-    for i, p in enumerate(parts):
-        if i != 0:
-            ret += '\\'*(i-1) + sep
-        ret += str(p)
+#
+# EOF
+#
+# ====================================================================
+# From traits/trigger.py
 
-    return ret
-
 # --------------------------------------------------------------------
-def add_piece(typ, state):
-    '''Create and add piece command (VASSAL.command.AddPiece)
+class TriggerTrait(Trait):
+    ID      = 'macro'
+    WHILE   = 'while'
+    UNTIL   = 'until'
+    COUNTED = 'counted' # - Always one "do ... while"
+    def __init__(self,
+                 name            = '',
+                 command         = '', # Context menu name
+                 key             = '', # Context menu key
+                 property        = '', # Enable/Disable
+                 watchKeys       = [],
+                 actionKeys      = [], # What to do
+                 loop            = False,
+                 preLoop         = '', # Key
+                 postLoop        = '', # Key
+                 loopType        = COUNTED, # Loop type
+                 whileExpression = '',
+                 untilExpression = '',
+                 count           = 0,
+                 index           = False,
+                 indexProperty   = '',
+                 indexStart      = '',
+                 indexStep       = ''):
+        '''Create a layer trait (VASSAL.counter.Trigger)'''
+        super(TriggerTrait,self).__init__()
+        encWKeys = Trait.encodeKeys(watchKeys, ',')
+        encAKeys = Trait.encodeKeys(actionKeys,',')
+        self.setType(name            = name,            
+                     command         = command,          # Context menu name
+                     key             = key,              # Context menu key
+                     property        = property,         # Enable/Disable
+                     watchKeys       = encWKeys,       
+                     actionKeys      = encAKeys,         # What to do
+                     loop            = loop,            
+                     preLoop         = preLoop,          # Key
+                     postLoop        = postLoop,         # Key
+                     loopType        = loopType,         # Loop type
+                     whileExpression = whileExpression, 
+                     untilExpression = untilExpression, 
+                     count           = count,           
+                     index           = index,           
+                     indexProperty   = indexProperty,   
+                     indexStart      = indexStart,      
+                     indexStep       = indexStep)       
+        self.setState(state='')
 
-    Each part is separated by a SLASH ('/').  The first part is the
-    command type (PLUS '+') to perform.  Second is the IDENTIFIER,
-    possibly 'null'.  Then comes the fully encoded type of the piece,
-    followed by the state of the piece (also fully encoded).
+    def getActionKeys(self):
+        return Trait.decodeKeys(self['actionKeys'],',')
+    
+    def getWatchKeys(self):
+        return Trait.decodeKeys(self['watchKeys'],',')
+    
+    def setActionKeys(self,keys):
+        self['actionKeys'] = Trait.encodeKeys(keys,',')
+        
+    def setWatchKeys(self,keys):
+        self['watchKeys'] = Trait.encodeKeys(keys,',')
+        
+        
 
-    Parameters
-    ----------
-    typ : str
-        Encoded piece definition
-    state : str
-        Encoded state definition
+Trait.known_traits.append(TriggerTrait)
 
-    Returns
-    -------
-    cmd : str
-        The add command encoded
-    '''
-    args = ['+',      # Type
-            'null',   # IDENTIFER  
-            typ,      # DEFINITION
-            state]    # STATE 
-    return '/'.join(args)
+#
+# EOF
+#
+# ====================================================================
+# From traits/nonrect.py
 
 # --------------------------------------------------------------------
-def add_proto(typ,state):
-    '''Create an add prototype command.  This is the same as 'add_piece'
+class NonRectangleTrait(Trait):
+    ID      = 'nonRect2'
+    CLOSE   = 'c'
+    MOVETO  = 'm'
+    LINETO  = 'l'
+    CUBICTO = 'l'
+    QUADTO  = 'l'
+    def __init__(self,
+                 scale    = 1.,
+                 filename = '',
+                 path     = [],
+                 image    = None):
+        '''Create a NonRectangle trait (static property)'''
+        super(NonRectangleTrait,self).__init__()
+        l = []
+        if len(filename) > 0:
+            l.append(f'n{filename}')
 
-    Parameters
-    ----------
-    typ : str
-        Encoded piece definition
-    state : str
-        Encoded state definition
+        if len(path) <= 0:
+            path = self.getShape(image)
 
-    Returns
-    -------
-    cmd : str
-        The add command encoded
-    '''    
-    return add_piece(typ,state)
-                      
+        if len(path) > 0:
+            # print(path)
+            l += [f'{p[0]},{int(p[1])},{int(p[2])}' if len(p) > 2 else p
+                  for p in path]
+    
+        self.setType(scale = scale,
+                     code  = ','.join(l))
+        self.setState()
 
+    @classmethod
+    def getShape(cls,image):
+        if image is None:
+            return []
+
+        from PIL import Image
+        from io import BytesIO
+
+        code = []
+        with Image.open(BytesIO(image)) as img:
+            alp = img.getchannel('A') # Alpha channel
+            # Find least and largest non-transparent pixel in each row
+            rows  = []
+            w     = alp.width
+            h     = alp.height
+            bb    = alp.getbbox()
+            for r in range(bb[1],bb[3]):
+                ll, rr = bb[2], bb[0]
+                for c in range(bb[0],bb[2]):
+                    if alp.getpixel((c,r)) != 0:
+                        ll = min(c,ll)
+                        rr = max(c,rr)
+                rows += [[r-h//2,ll-w//2,rr-w//2]]
+                    
+            # Now produce the code - we start with the top line
+            code = [(cls.MOVETO,rows[0][1],rows[0][0]-1),
+                    (cls.LINETO,rows[0][2],rows[0][0]-1)]
+            
+            # Now loop  down right side of image
+            for c in rows:
+                last = code[-1]
+                if last[1] != c[2]:
+                    code += [(cls.LINETO, c[2], last[2])]
+                code += [(cls.LINETO, c[2], c[0])]
+                
+            # Now loop up left side of image
+            for c in rows[::-1]:
+                last = code[-1]
+                if last[1] != c[1]:
+                    code += [(cls.LINETO,c[1],last[2])]
+                code += [(cls.LINETO,c[1],c[0])]
+
+            # Terminate with close
+            code += [(cls.CLOSE)]
+
+        return code
+
+
+Trait.known_traits.append(NonRectangleTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/click.py
+
 # --------------------------------------------------------------------
-def add_proto_code(decs):
-    typs = [d[0] for d in decs]
-    stts = [d[1] for d in decs]
-    typ  = enc_parts(*typs)
-    stt  = enc_parts(*stts)
+class ClickTrait(Trait):
+    ID = 'button'
+    def __init__(self,
+                 key         = '',
+                 x           = 0,
+                 y           = 0,
+                 width       = 0,
+                 height      = 0,
+                 description = '',
+                 context     = False,
+                 whole       = True,
+                 version     = 1,
+                 points      = []):
+        '''Create a click trait (static property)'''
+        super(ClickTrait,self).__init__()
+        self.setType(key          = key,
+                     x            = x,
+                     y            = y,
+                     width        = width,
+                     height       = height,
+                     description  = description,
+                     context      = context,
+                     whole        = whole,
+                     version      = version,
+                     npoints      = len(points),
+                     points       = ';'.join([f'{p[0]};{p[1]}'
+                                              for p in points]))            
+        self.setState()
 
-    return add_proto(typ,stt)
 
-# ====================================================================
+Trait.known_traits.append(ClickTrait)
+
 #
-# Below specific to the export app
+# EOF
 #
 # ====================================================================
-def get_bb(buffer):
-    '''Get bounding box of image
+# From game.py
 
-    Parameters
-    ----------
-    buffer : bytes
-         The image bytes
+# --------------------------------------------------------------------
+class Game(Element):
+    TAG = Element.BUILD+'GameModule'
+    def __init__(self,build,node=None,
+                 name            = '',
+                 version         = '', 
+                 ModuleOther1    = "",
+                 ModuleOther2    = "",
+                 VassalVersion   = "3.6.7",
+                 description     = "",
+                 nextPieceSlotId = 20):
+        '''Create a new Game object
 
-    Returns
-    -------
-    ulx, uly, lrx, lry : tuple
-         The coordinates of the bounding box 
-    '''
-    from PIL import Image
-    from io import BytesIO
+        Parameters
+        ----------
+        build : xml.dom.Document
+            root note
+        node : xml.dom.Node
+            To read from, or None
+        name : str
+            Name of module
+        version : str
+            Version of module
+        ModuleOther1 : str
+            Free form string 
+        ModuleOther2 : str
+            Free form string
+        VassalVersion : str
+            VASSAL version this was created for
+        description : str
+            Speaks volumes
+        nextPieceSlotId : int
+            Starting slot ID.
+        '''
+        super(Game,self).__init__(build, self.TAG,
+                                  node            = node,
+                                  name            = name,
+                                  version         = version,
+                                  ModuleOther1    = ModuleOther1,
+                                  ModuleOther2    = ModuleOther2,
+                                  VassalVersion   = VassalVersion,
+                                  description     = description,
+                                  nextPieceSlotId = nextPieceSlotId)
+    def nextPieceSlotId(self):
+        '''Increment next piece slot ID'''
+        ret = int(self.getAttribute('nextPieceSlotId'))
+        self.setAttribute('nextPieceSlotId',str(ret+1))
+        return ret
+    #
+    def addBasicCommandEncoder(self,**kwargs):
+        '''Add a `BasicCommandEncoder` element to this
 
-    with Image.open(BytesIO(buffer)) as img:
-        bb  = img.getbbox()
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : BasicCommandEncoder
+            The added element
+        '''
+        return self.add(BasicCommandEncoder,**kwargs)
+    def addGlobalTranslatableMessages(self,**kwargs):
+        '''Add a `GlobalTranslatableMessages` element to this
 
-    return bb
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : GlobalTranslatableMessages
+            The added element
+        '''
+        return self.add(GlobalTranslatableMessages,**kwargs)
+    def addPlayerRoster(self,**kwargs):
+        '''Add a `PlayerRoster` element to this
 
-# ====================================================================
-def piece_body(family,main,flipped,gpid):
-    '''Create an full add piece command from the side, image, and ID
-    information.  This adds (in reverse order)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PlayerRoster
+            The added element
+        '''
+        return self.add(PlayerRoster,**kwargs)
+    def addChessClock(self,**kwargs):
+        '''Add a `ChessClockControl` element to this
 
-    - BasicPiece
-    - Side prototype
-    - Flip layer (if 'flipped' is not none)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : PlayerRoster
+            The added element
+        '''
+        return self.add(ChessClockControl,**kwargs)
+    def addLanguage(self,**kwargs):
+        '''Add a `Language` element to this
 
-    Parameters
-    ----------
-    family : str
-        Side in the game
-    main : dict
-        Main image information
-    flipped : dict
-        Flipped image information, possibly None
-    gpid : int
-        Global piece identifier
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Language
+            The added element
+        '''
+        return self.add(Language,**kwargs)
+    def addChatter(self,**kwargs):
+        '''Add a `Chatter` element to this
 
-    Returns
-    -------
-    cmd : str
-        The command to add the piece
-    '''
-    # Create the layer, prototype, and basic piece traits
-    decs = []
-    if flipped is not None:
-        imgs = [ main['filename'], flipped['filename'] ]
-        decs.extend([ layer_trait(images=imgs,
-                                  newNames=['','Reduced +'],
-                                  activateName = '',
-                                  decreaseName = '',
-                                  increaseName = 'Flip',
-                                  increaseKey  = key('F'),
-                                  decreaseKey  = '',
-                                  name='Step'),
-                      report_trait(key('F')) ])
-    for m in main.get('mains',[]):
-        decs.append(prototype_trait(m + ' prototype'))
-    ech = main.get('echelon',None)
-    cmd = main.get('command',None)
-    if ech is not None: decs.append(prototype_trait(ech + ' prototype'))
-    if cmd is not None: decs.append(prototype_trait(cmd + ' prototype'))
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chatter
+            The added element
+        '''
+        return self.add(Chatter,**kwargs)
+    def addKeyNamer(self,**kwargs):
+        '''Add a `KeyNamer` element to this
 
-    decs.extend([ prototype_trait(family+' prototype'),
-                  basic_trait(main['name'], main['filename'],gpid)
-                 ])
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : KeyNamer
+            The added element
+        '''
+        return self.add(KeyNamer,**kwargs)
+    def addNotes(self,**kwargs):
+        '''Add a `Notes` element to this
 
-    typs = [d[0] for d in decs]
-    stts = [d[1] for d in decs]
-    typ  = enc_parts(*typs)
-    stt  = enc_parts(*stts)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Notes
+            The added element
+        '''
+        return self.add(Notes,**kwargs)
+    def addLanguage(self,**kwargs):
+        '''Add a `Language` element to this
 
-    return add_piece(typ,stt)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Language
+            The added element
+        '''
+        return self.add(Language,**kwargs)
+    def addChatter(self,**kwargs):
+        '''Add a `Chatter` element to this
 
-# --------------------------------------------------------------------
-def proto_body(family):
-    '''Create an full add prototype com from the side information.
-    This adds (in reverse order)
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : Chatter
+            The added element
+        '''
+        return self.add(Chatter,**kwargs)
+    def addKeyNamer(self,**kwargs):
+        '''Add a `KeyNamer` element to this
 
-    - An empty BasicPiece
-    - An eliminate (or sendto) trait
-    - A delete trait
-    - A mark moved trait
-    - A move trail trait 
+        Parameters
+        ----------
+        kwargs : dict
+            Dictionary of attribute key-value pairs
+        Returns
+        -------
+        element : KeyNamer
+            The added element
+        '''
+        return self.add(KeyNamer,**kwargs)
+    def addGlobalProperties(self,**kwargs):
+        '''Add a `GlobalProperties` element to this
 
-    Parameters
-    ----------
-    family : str

@@ Diff output truncated at 1234567 characters. @@


More information about the tex-live-commits mailing list.