I am happy to say that Panzergrüppe Guderian has been implemented. The only thing now missing is the die rolling. Here is a screenshot from an actual game showing the whole map.

A closeup of the central western side.

Panzergrüppe Guderian was not a “simple” module. The untried Soviet units resemble cards in Paths of Glory. A lot had to be been done with the engine. I will try to describe the whole development of Panzergrüppe Guderian in the following. It was certainly a challenge.
Code is as usual found on GitHub.
Important: There is no images
directory in the source code. The images
directory must be installed separately. Download the VASSAL module found here here (PGG-TAHGC-v1.2.vmod). Open it with the Archive Manager and extract the images
directory. Place this directory in the source code alongside the __images
and setups
directories.
GitHub is not made to hold a large number of images. The images used by this engine will from now on have to be downloaded separately.
The Panzergrüppe Guderian map does not have coordinates. Since I was limited to the using the original image resources, I had to generate these coordinates first.
In my opinion using the game engine to generate image resources is a no, no. Image resources are to be made beforehand using Photoshop, GIMP, etc. I once made a GIMP script that generated a hex grid. It would not be difficult to modify this script to generate coordinates as well. Or it can be done manually by placing text boxes and a lot of copy/paste.
The generation and scaling of coordinates had to be hard-coded in C++. This code will likely be removed. Rendering of coordinates in this way uses valuable paint resources and really is quite unnecessary.
A hexagonal grid had to be defined. The code for this had already been made and just needed to be ported to Lua, see Zone:makeHexagonalGrid(width, height)
. The width and height parameters are the same as the a and b values in the illustration on the GitHub wiki.
By displaying the grid points with (in dev.luau):
show_grid(true)
and reloading the script (CTRL+R), I found the values of a and b to be
a = 51.74
b = 89.61
which makes a grid that is quite close to the hex depiction on the map. It seems to me that the grid is not quite regular, that a and b are not the same across the whole grid. This may because the grid artwork was made by hand, not generated by a computer. Or maybe a third decimal is needed to make the values even more precise.
With the grid points on place, it was time to define the zone. On the PGG map there is only one zone, the map itself minus the edges. This zone can be defined by a square.
mainzone:setBorder({x0 = 36, y0 = 38, x1 = 4643, y1 = 2864})
In addition there are three locations outside the map that are added to the zone points:
mainzone:addGrid(477, 43, 'German Air Interdiction', 0, 0, Offset:new(2, 2, 3, 0))
mainzone:addGrid(634, 43, 'Soviet Interdiction')
mainzone:addGrid(786, 43, 'Soviet Rail Cut', 0, 0, Offset:new(2, 2, 3, 0))
The Offset:new(2, 2, 3, 0) is a new trait. The ‘2’ and ‘2’ define x and y offset of each counter in a closed stack. The ‘3’ is the multiplier of the offsets when the stack is opened. The last number tells how many counters to show in the stack. A ‘0’ means an unlimited number. A ‘3’ would mean that the stack can only show the top three counters.
A zone must be populated by counters (zone members). A counter first has to be defined in repository.luau. Up to now this has been done with a statement like
Repository["German/ge_army01"] = Repository:new({"German/ge_army01", "German/ge_army01_back"})
for every single counter. This is obviously a bit tedious if there are many counters. I have now made it possible to use regular expressions to define groups of counters. Lua has a simple version of regular expressions that are sufficient for the task. All German and Soviet counters are defined in one batch with these expressions (in repository.luau):
assign('ge%-cav%-%w+%-*%d*$')
assign('ge%-inf%-%d+$')
assign('ge%-mot%-%w+$')
assign('ge%-mot%-%w+%-%d*$')
assign('ge%-pz%-%w+%-%-%d+pz%-*%d*$')
assign('ge%-pz%-%w+-%d+pzgr-%d*$')
assign('ru%-arm%-%d+$')
assign('ru%-HQ%-%w+.+$')
assign('ru%-inf%-%w+$')
assign('ru%-mech%-%w+$')
This replaces the need to write a statement for each counter like
Repository["ge-inf-005"] = Repository:new({"ge-inf-005", "ge-inf-005-1", "ge-inf-005-2", "ge-inf-005-3"})
It is still possible to use statements for individual counters:
Repository["Soviet Rail Cut"] = Repository:new({"Soviet Rail Cut"})
Likewise you can use regular expressions to set traits
assignTraits('ge%-inf%-%d+$', German)
set the C++ class
assignClass('ge%-inf%-%d+$')
and assign members to zones.
mainzone:addMembers('ge%-inf%-%d+$')
Using regular expressions cuts down what you have to write substantially, but you have to make sure they are correct.
Note: The images directory of PGG is flat. It is a better idea to use sub-directories and group similar
counters together in them. That makes the regular expression easier to write and understand.
Panzergrüppe Guderian has a custom toolbar (in addition to the system toolbar). This bar holds the turn track, the Set-up/Reinforcements button, the Graveyard button, the Charts button and the Markers button.
A new toolbar is created (in dev.luau) as
create_toolbar('ct', 'Custom toolbar')
The ‘ct’ is the tag (identifier) of the toolbar.
Note: By right clicking the toolbars in Qt you can hide them by unticking a checkbox. This is very handy if you want to make the map area as large as possible.
The Set-up/Reinforcements button is defined by:
local callback = "toggle_window('Rf')"
add_toolbar_button('ct', 'rf', 'control_button', 'Set-up / Reinforcements', 'Show/Hide window', callback)
toolbar_enable('ct', 'rf')
By clicking this button the Set-up/Reinforcements window is shown (hidden). The tag is ‘Rf’. This window has its own zone with its own grid and members, look at dev.laua.
Note: One of the most important changes I did to the C++ code was to make the central widget of each window the same (class CentralFrame). I scrapped the idea of “tokens” and “simplified counters”. Besides being able to delete a few hundred lines of code and avoid code duplication, this approach simplifies the movement of counters between windows. Each window now has the same base class (class Window
) and the same central frame (class CentralFrame
).
Note: For now only the main window (with tag ‘main’) can scroll and resize. The repository window (tag ‘repository’) has its own layout and can resize. The other sub-windows are static. I will need to come back to this later.
The custom toolbar has a turn track. It consists of three separate items: an icon showing the player turn (Soviet or German), a text box displaying the turn/phase and a button that when pressed goes to the next turn/phase.
The turns and phases are defined like this.
local turns = {'Turn 1 (3-4 July)', 'Turn 2 (5-6 July)', 'Turn 3 (7-8 July)', 'Turn 4 (9-10 July)',
'Turn 5 (11-12 July)', 'Turn 6 (13-14 July)', 'Turn 7 (15-16 July)', 'Turn 8 (17-18 July)',
'Turn 9 (19-20 July)', 'Turn 10 (21-22 July)', 'Turn 11 (23-24 July)', 'Turn 12 (25-26 July)'}
local phases = {'Soviet Movement Phase', 'Soviet Combat Phase', 'Soviet Disruption Removal Phase',
'Soviet Interdiction Phase', 'German Initial Movement Phase', 'German Combat Phase',
'German Mechanized Movement Phase', 'German Disruption Removal Phase', 'German Air Interdiction Phase'}
If you are not familiar with Panzergrüppe Guderian, each turn consists of a Soviet player turn and a German player turn. Each player turn has phases, the Soviet four, the German five. By pressing the turn/phase button you go to the next turn/phase in sequence.
The text box displaying the turn/phase is made with:
local css = 'font-size: 15px; border: 1px solid grey; height:20px; margin-right:4; border-radius: 2px;'
add_toolbar_label('ct', 'label2', '', 410, 22, css)
toolbar_enable('ct', 'label2')
In Qt you can send a css (Cascading Style Sheet) to a widget and set its style and look. border-radius: 2px
gives text boxes rounded corners.
The Charts window is interesting because it uses the same layout as the Repository window. This makes it possible to use tabs.
create_window('Ch', 'Charts', '#777777', false, 'Layout')
set_window('Ch', 70, 70, 870, 780)
root(0, 'Ch')
tabs(1, 'Ch')
tab(2, 'CRT', 'Ch')
image_item('CRT', 'Ch')
tab(2, 'Terrain Effects', 'Ch')
image_item('Terrain Effects Chart', 'Ch')
tab(2, 'Terrain Key', 'Ch')
image_item('terrain_key', 'Ch')
Note that the calls for making the layout now has a window tag (‘Ch’). It would be an idea to set the window tag as default so I don’t have to repeat the tag in every call.
Panzergrüppe Guderian has untried Soviet units (infantry and armor). All Soviet units are placed untried on the board. Only during combat are these units revealed. This can give both the defender and attacker unpleasant surprises. Some units have zero value and are discarded immediately. Others have high combat values and can in good terrain be very unpleasant for the Germans. This mechanism of untried units is one of the reasons why Panzergrüppe Guderian is a great game.
It should be obvious that untried units resemble cards. They are drawn from a “deck”, placed face down and only turned (drawn) when needed. The “card” may be discarded (placed in a dead pile) if it suffers a step loss.
Unlike the decks in Paths of Glory, a “drawn card” is not immediately revealed. Panzergrüppe Guderian uses the Deck
trait but in a more abstract fashion. A deck is not a pile of cards. It just holds the available “cards” not revealed.
There are four abstract decks
Deck:new('ru-inf', {}, {0,0})
Deck:new('ru-inf-d', {}, {0,0})
Deck:new('ru-mech', {}, {0,0})
Deck:new('ru-mech-d', {}, {0,0})
holding the Soviet unrevealed and dead infantry and mechanized units. The last {0,0} is the position of the deck which in this case has no meaning. The middle {} is the yet unpopulated cards table. The first parameter is the tag (identifier) of the deck.
Giving the decks “cards” (infantry and armor/mech counters) is done only once when making the setup. If you look at the very bottom of dev.luau you will see code that does this. This code uses regular expressions to find the correct counters to add. Note that the decks ‘ru-inf-d’ and ‘ru-mech-d’ always start empty.
In addition you need to create the necessary untried Soviet counters. There are 78 infantry counters and 20 armor counters. I was lazy. I didn’t want to manually drag exactly 78 Soviet untried ?-6 counters from the repository to the setup window. What if I lost count and placed just 77 there. So instead, make code that does this for you. It is a good illustration of what the script can do.
After the setup is saved this code must be deleted or commented out with --[[
and --]]
.
Here is a screenshot of the right-click menu of a German panzer unit.

‘Take 3’ is disabled because it is only valid in the Set-up/Reinforcements window. It selects the 3 top counters, handy when moving a whole panzer division. All units can take step losses in combat. Soviet units have only one step to loose and go directly to the dead pile. Soviet HQ go to the graveyard. Germans units can place a control marker in their hex. All units can place a Disrupted marker on top of themselves.
Units can retreat. To not make the right-click menu too long, a separate sub-menu can hold what direction to retreat to.

Soviet untried units have only a Reveal choice (besides the standard Moved to toggle move status).

A revealed Soviet unit has the options a German unit has except to place a control marker.

In the Luau functionactionStepLoss
(module.luau) is shown how traits can add fields dynamically.
local zone = Zones:get('rfzone')
local toId = next_id()
local zorder = top_zorder()
if Counter[id].Type == 'Inf' then
local pos = zone:getGrid('Soviet Infantry Dead Pool')
local dx, dy = get_size('ru-inf-??')
copyCounter('ru-inf-??', toId, zorder, 'Rf', pos.x - dx, pos.y - dy)
Counter:setTrait(Counter[toId], 'MoveStack', MoveStack:new('Reuse pile', zone:getGrid('Soviet Infantry Pool')))
Counter:setTrait(Counter[toId].MoveStack, 'menutrait', 'MoveStack')
Counter:setTrait(Counter[toId].MoveStack, 'menushow',
function(window, id)
local number = Counter:getStackSize('Rf', zone:getGrid('Soviet Infantry Pool'))
return number == 0
end)
Actually, since the right-click menu is created from a counter at the instance you right click, totally new traits (Lua tables) can be defined, added and executed while the game is running. Traits or any trait key/value pair of a trait can also be deleted while the game is running. This shows how immensely dynamic the engine is.
Decks and globals (like the turn of the game) needs to be saved.
Decks are found in the Decks
table. This table is saved in the same way as the table for counters.
I have put globals in a table called Globals
.
Globals = {}
function Globals:add(id, o)
Globals[id] = o
end
The global ‘Turn’ is initiated like this:
Globals:add('Turn', {turn = 1, phase = 1})
Note: Globals, unlike counters and decks, have no fixed format. Right now I just hard-code the saving of the turn:
std::string key = "turn";
fs.write(key.c_str(), key.size() + 1);
Luau::Turn turn = Luau::getTurn();
fs.write(reinterpret_cast<const char*>(&turn.turn), sizeof turn.turn);
fs.write(reinterpret_cast<const char*>(&turn.phase), sizeof turn.phase);
This is obviously not right. The module developer should have a way of defining the format of globals (what is saved and what is loaded from a saved file) which may differ from game to game. I need to come back to this.
In Panzergrüppe Guderian the Soviet leaders (the HQ units) have a command radius. In the VASSAL module you can see the radius by right-clicking and selecting ‘Show Radius’.
This is a good opportunity to use the Area of Effect trait.
AreaOfEffect = {}
function AreaOfEffect:new (type, radius, color, opacity)
local o = {}
setmetatable(o, self)
self.__index = self
o.type = type
o.radius = radius
o.color = color
o.opacity = opacity
o.menuname = "Show Radius"
o.menutrait = "AreaOfEffect"
o.menuclick = "actionToggle"
o.menushow = function(window, id) return window == 'main' end
o.area = {}
o.apply = false
return o
end
The ‘area’ table holds the points the make up the outline of the effect. It is a set of consecutive points, where it is presumed that the last point connects to the first point to make a closed polygon. The set of points is made by calling Zone:makeAreaOfEffect(x, y, radius)
.

Creating this polygon from the hex-grid was complicated and I just refer to the code in zone.luau.
The rendering of the area of effect is done in CentralFrame::paintEvent(QPaintEvent *event)
, see frame.cpp. It is important to note that all rendering is always done in the C++ part. The rendering in this case only renders a given set of points with painter.drawConvexPolygon(points, areaOfEffectNumber)
. The creation of these points is entirely up to the module developer in the Luau part. He has full control with how these points are made. It illustrates the important difference between the role of the C++ part and the Luau part.
Note: Rendering a command radius of 5 had a substantial lag. This is due to the inefficient method of iterating through all grid points. In Panzergrüppe Guderian there are 31 x 59 = 1829 grid points. These points have to be iterated over and over, not good.
function Zone:isGrid(x, y)
for k, v in pairs(self.grid) do
-- a bit dirty, takes into consideration rounding errors
if x <= v.x + 2 and x >= v.x - 2 and y <= v.y + 2 and y >= v.y - 2 then
return true
end
end
return false
end
The table holding grid points (self.grid) should have been hashed. Unfortunately, there is also the possible problem with rounding errors. This explains the +/- 2 in the code above.
The problem with lag is partly due to the need to crop the area of effect. If a Soviet HQ unit is close to the map edge you do want the area of effect to spill out of the map.

Changes were done to the GUI. The main change was that the default stack selection is the top counter, not the whole stack. The need for this became apparent when you want to drag units from the reinforcement window to the map. By right-clicking, you can take 2 or 3 units at a time. Three units is especially handy for the German panzer divisions that come as three individual counters.
Moving a whole stack or part of a stack presented a problem when the stack was tall. The aiming point is now always the bottom of the moved stack.
A lot of work went into making sure the undo system worked with decks and globals. The undo system now works when moving counters between windows.
The undo system is still basically simple. I am surprised it has never required more code.
A very important thing I had do was to turn off painting while undoing. The map can not be painted while undoing/redoing. When I first made the setup with a complete number of counters in PGG the undo lagged terribly. Two crucial paint event (in class QtCounter
and class CentralFrame
) had to be shut down.
if (!CentralFrame::allowPainting)
{
event->accept();
return; // no painting while undoing
}
In addition, in two vital places, a widget had to be updated, not repainted. Repaint forces a total redraw while update just redraws invalid areas (which in this case is zero).
Note: I am sure there are still errors (or even embarrassing crashes). As always this prototype has not been tested properly. Real testing will wait until the engine is at a more finished state.
The only lacking thing now for PGG is die rolling. It will be part of the chat/log window coming up next.
One thing I have always missed in the VASSAL module of Panzergrüppe Guderian is the ability to see what hexes are in German supply. Supply is calculated as movement points from the western edge. Terrain influences movement points. The calculated path must be free of Soviet units. It would be cool to hit a button and see all hexes supplied. But this is not easy and time is limited. We will see.
After the chat/log window I will remake what I did in Paths of Glory with the new version of the the prototype.
Panzergrüppe Guderian was not a “simple” module. Maybe The Russian Campaign is. The prototype will only approach a finished status when X number of games have been implemented.
It is interesting to look at the lines of code.
1270 counter.cpp
1818 frame.cpp
701 io.cpp
2095 luau.cpp
376 main.cpp
844 overlay.cpp
210 scale.cpp
236 settings.cpp
595 toolbar.cpp
907 window.cpp
217 counter.h
171 frame.h
100 io.h
87 luau.h
123 overlay.h
43 scale.h
43 settings.h
93 toolbar.h
183 window.h
10112 total
523 counter.luau
398 dev.luau
1113 module.luau
269 repository.luau
358 state.luau
508 traits.luau
565 zone.luau
3734 total
Much is still missing in the code, not least input verification (that the script must never be allowed to crash the engine) and the online part. But still, the number of lines gives an indication of what is needed to implement an actual game.
I started off 2+ years ago with a blank sheet of paper and a C++ compiler, and here we are
.