Some new design thoughts

A logfile is a well-known feature in VASSAL. It facilitates PBEM.

A logfile consists of a start state and a number of individual moves. A logfile is created by first saving the game and then recording moves. At log end the recorded moves are saved together with the saved game.

When you load a file, as soon as appended moves are detected then it’s a logfile, not just a saved game.

Saving and loading of games has already been implemented. Also, recording of moves has been implemented through the undo-stack. So the only new code necessary is to combine a saved game with appending the moves on the undo-stack.


Choosing Begin logfile... from the menu lets you specify the name of the logfile and then starts the recording. A red circle appears on the toolbar to clearly remind you that recording now takes place.

End logfile will append the made moves (the content of the undo-stack) to the logfile specified in Begin logfile...

Opening a logfile will activate the step buttons on the toolbar. There are three buttons: Step, Go To End and Abort. Illustrations can be found on my GitHub.

Step will step forward one move. Go To End will step to the end of the logfile (if you for some reason don’t need to see the individual steps). Abort will exit stepping at this point. This can be of use if a mistake has been detected that makes it impossible to go on. Needless to say, the reason for aborting a logfile must be good and made clear. Often a mistake in a logfile need not influence later moves. There has to be a very good reason for aborting a logfile.

I have imposed a few restriction on recording and stepping. These restrictions are temporary. If good arguments for removing them come, I will consider that.

In recording you can not undo a move. It has a very practical reason. If you could undo say a bad die roll then using logfiles would never work. I know very well that moves need not be saved on the undo-stack but on a seperate stack. But that would need more code. I think that the nature of recording should prevent undoing moves. If you make a mistak it’s fair to take it back but this should be explained and made apparent in the recording. So no undo while recording.

In stepping through a logfile, a step can not be undone. Steps in a logfile are not put on the undo-stack. Even more important, when stepping a logfile no other operation is possible. All counters are disabled. No new counter can be dragged to the map.

I see no purpose in doing anything else while stepping a logfile. If you could, say, delete a counter that is later moved by the logfile, it would not be good. It is the land where bugs grow. All kinds of complications could arise if you can do things to the game that your opponent can not see. If an error occurs in the logfile then you can abort.

Again, if there is a good reason to make your own moves while stepping a logfile, then this restriction can be removed. Usually it is possible to step through a logfile and then start recording and make your own moves. The gameplay becomes cleaner by this in my opinion. But it depends on the game and circumstances. I do not have all the answers.

Note also, it is not possible to record while stepping through a logfile. Given the restrictions above, this is useless.


All actions will always be logged to a chat-console window (and possibly also to an output file).

Note: the chat-console window is not made yet. It will be the third window besides the map window and repository window. I will come back to it later. For now it’s more important to implement vital functionality like cards and grids.


A summary of the engine so far:

  • A modules consist of Luau script files, a C++ binary, the Luau VM engine and the Qt library. Each module has it’s own copy of the GUI library so we never need to worry about changes.
  • A configurable window for access to all the game’s counters, the Repository window. Counters can be accessed in tabs, lists and combo boxes in any order.
  • Definition of traits (counter abilities). Inheritance of traits to build more complex traits.
  • A script-driven interface to the C++ engine. Script can do most of the operations a user can do.
  • A Luau sandboxed environment for security.
  • A rigorous undo/redo system for undoing mistakes or just checking what took place.
  • The ability to scale the map, either by a fixed percentage or free scaling with the mouse wheel.
  • The ability to rotate the map by fixed increments of 90 degrees.
  • The ability to open and manipulate stacks, either by increasing the offset, or as offset-less stacks where you select and reorder counters in a popup window.
  • Saving and loading of games.
  • Recording and stepping through logfiles.

Up until now I have basically made a copy of VASSAL. Now it’s time to go into new territory and try to implement features you do not find in VASSAL. I will try to implement the so-called artillery-tables in Verdun: A Generation Lost. I will describe in detail what these tables are and what they are meant to do.

It will be a good test. It must follow the design. I can not make “hacks”. It will be the real test of the engine. I hope that in making these tables I will show the abilities of the engine.

1 Like

Some thoughts on your critique of this feature:

It’s sometimes useful to record an online session.

If a module has to handle its own undos, this will require more effort in module development in proportion to the complexity of the actions that the module allows (movement triggers come to mind).

Vassal allows “presumptive play”. This speeds up PBEM turnaround considerably for highly interactive games. Unrestricted undo and ability to add into a log file are key features that enable this feature.

I don’t mean to detract from your concerns. Undo is certainly one of the more bug prone areas of Vassal, in my experience. And, it’s not uncommon to have to make manual corrections where a log file makes changes that an intervention has invalidated.

Aside: One thing I could use in Vassal v3 is a checkpoint feature, so that a module can prevent undo beyond a key points (say when some hidden information has been revealed).

1 Like

Thank you for your comment!

I can not in any way have a complete knowledge of what a board game engine needs. Therefore comments like yours are very important.

In my development of the prototype I have gone through many issues lightly. Typically I make a simple version of something (like logging) or just provide an example of something (like with ownership-rights). I do this to keep the overall goal in mind. I have to focus on the essential (next up cards, then grid) and not get bogged down in detail.

In my opinion it’s better to give users choices rather then decide how things are done. An example are stacks, with or without an offset. I don’t think it’s wise to decide what is “right”. If it is necessary to undo while recording then fine. It will be developed and given as an option. If players want to undo while stepping through a logfile, or be free to move their own counters, then fine. Implement it and give it as an option.

A module should never need to handle their own undo. Then the design is flawed.

It is possible to record while online, just not when stepping through a logfile offline.

You mention prohibiting undo after hidden information is revealed. I agree. One solution is to record all undo in a fashion that can itself not be undone. This may be easier to implement.

2 Likes

In Verdun: A Generation Lost there are French and German artillery counters. They are used in the Bombardment Segment and the Defensive Fire Segment.

As in most games, an artillery counter can fire only at one target and only once in a segment. More than one artillery counter may fire at the same target. The total fire power is then added up. The counter needs to be in range of its target. Different artillery counters have different ranges. The counters have a reduced strength side. Different power values (combat factors) are used for
Bombardment and Counter-Battery fire.

When there are 35+ artillery counters on the map, you can imagine that keeping track of who fire where, ranges and total firepower on each target can be demanding. Actually, this is one of the reasons why Verdun: A Generation Lost demands a special interest in the WW1 Western Front to play.

To keep track of the book-keeping and speed up play, I devised a special table, the Artillery Fire Table, for each side. This is the table I will now implement on my game-engine.

Interested can first check out the VASSAL module I made and see how the Artillery Fire Table was implemented on that engine.


The main point of this implementation is to show how a complex module feature can be made. In the VASSAL module this was the most difficult feature to do I remember. Let us then look at its equivalent on this engine. Note that I will not make an implementation with all the counters. GitHub is not designed to host a large number of image files. Also, I will only make the German Artillery Fire Table. The French Artillery Fire Table is just a copy.

It is important not to make “hacks” in the implementation. A “hack” in this context means that the implementation of this specific feature (the Artillery Fire Table) depends on code in C++ to work. A module developer can never change the C++ part. A “hack” would mean that the design is flawed. I have managed to avoid “hacks” to a large part, and what “hacks” are left can easily be removed. I will return to this later.

In order to make clear what implements the table, I have made a new script file called dev.luau. This file, together with module.luau, implements the table. Note: for the time being new script files are “hard-coded”. Later, a module developer will be able to create as many new luau script files as needed.


dev.luau:

-- create fire table window and add button to toolbar with callback

create_window("German artillery table", "artillery-window", "GermanArtilleryB")


local callback = "toggle_window()"

add_toolbar_button("Icons/GermanArtillery", "German artillery table", callback)

create_window creates the class Window. The parameters are window title, window tag and resource name for the window background. The tag is used to access the instance of this Window class. The tag “artillery-window” can access the created C++ class Window.

The C++ class Window is defined in window.cpp.

Note: I renamed the old window.cpp to repository.cpp. The new window.cpp implements the new Window class.

Note: Right now class Window can only exist in one instance. This will fixed later.

Note: create_window is a C API call. I have (mostly) used the convention that these calls have an underline. Right now there are 41 such calls in luau.cpp. I can imagine they will go up in their hundreds. As mention before, a C API call is the way that Luau can access the C++ engine and Qt. It is the only way. Luau can not access the “outside” in any other way.

The created window needs to be shown. This is done by creating a toggle button on the toolbar (add_toolbar_button). The parameters are the resource name for the image to be used on the button, a string for the button tool tip and a string (callback) containing the hook (or handler) for the button, what is executed when you click the button. The callback is naturally a luau script, a so-called chunk. A chunk is just a piece of code (like dev.luau itself) that is run though. It may
contain functions but they have to be called.

The chunk is toggle_window(), a C API function that does the actual toggle.

Note: The toolbar is of now “hard-coded” in C++, mostly in main.cpp. This will of course have to be changed. It is by script that the module developer adds buttons and features to the toolbar. Also note that the function show() in main.cpp is “hard-coded” to show a specific instance of class Window. This is what I mean when I say that there are still “hacks”.These “hacks” can be avoided. For instance: if toggle_window uses the tag defined in create_window, toggle_window(“artillery-window”), then the class instance can be retrieved by Window::instances(tag).


Next up is setting the custom grid in the window. You define a Lua table called grid, define an adder, and then add each grid point.

-- define and set custom grid coordinates

local grid = {[1] = {}}

function grid:add(x, y)
    table.insert(grid[1], {x = x, y = y})
end

-- storage locations
grid:add(104, 136)
grid:add(192, 136)
grid:add(280, 136)
grid:add(368, 136)
grid:add(456, 136)
grid:add(544, 136)
grid:add(632, 136)


grid:add(104, 234)
grid:add(192, 234)
grid:add(280, 234)
grid:add(368, 234)
grid:add(456, 234)
grid:add(544, 234)
grid:add(632, 234)

-- fire calculation locations
grid:add(128, 376)
grid:add(128, 471)


set_grid(grid, 1)

Note: These grid points are the center of where a counter is supposed to be placed. The points have meaning in relation to the background image, defined in create_window as GermanArtilleryB. There are storage location for artillery pieces and target locations or fire calculation locations. See this image (I use imgbox for uploaded images, there are ads):

Note: The grid points have to be manually entered. The module developer who has created the background image will have an understanding of where these points are. They can be measured and written down. There is no tool to do this job.

Next we define the coordinates of the locations where firepower is to be calculated. These are the fire locations.

--- define fire location, key = target

local FireLocations = { {x = 128, y = 376, gridx = 1, gridy = 1}, 
	                    {x = 128, y = 471, gridx = 1, gridy = 1}}
	                   

function setTarget(k, x, y)
    FireLocations[k].gridx = x
    FireLocations[k].gridy = y
end

Note: In my VASSAL module there were 10 such locations and 10 target counters. Here I only define 2 locations for 2 targets.

-- create boxes for target coordinate and calculated firepower

create_label('coor1', 180, 355, 78, 38, 'border:1px solid black; background:white; font:bold 30px; qproperty-alignment: AlignCenter;')		
create_label('valu1', 268, 355, 58, 38, 'border:1px solid black; background:white; font:bold 30px; qproperty-alignment: AlignCenter; color:red;')	
    
set_label_text('coor1', '---')		
set_label_text('valu1', '0')

create_label('coor2', 180, 450, 78, 38, 'border:1px solid black; background:white; font:bold 30px; qproperty-alignment: AlignCenter;')		
create_label('valu2', 268, 450, 58, 38, 'border:1px solid black; background:white; font:bold 30px; qproperty-alignment: AlignCenter; color:red;')	
    
set_label_text('coor2', '---')		
set_label_text('valu2', '0');

The boxes holding the coordinates for the target counters and the boxes holding the calculated firepower for each target is defined. These boxes are QLabels. The first parameter is the tag (the indetifier) of the box. The four next parameters is x, y, width and height of the box. The last parameter a stylesheet for the QLabel. The stylesheet is a very convenient way of specifying the whole look of the box in a single parameter.

Note: Qt Css is unfortunately not the same as Html Css. Among other things text-align:center does not work. Instead you use qproperty-alignment:AlignCenter :confused:

Setters for the text is set_label_text. There is also a getter called get_label_text.


-- create check box for setting counter battery fire

callback = "recalculate()"

create_checkbox('cb', 100, 20, 200, 28, 'Counter Battery Fire', callback,
			    'QCheckBox:checked {color: black;} QCheckBox {font:bold 16px Arial,sans-serif; color:white;}');

In Verdun: A Generation Lost artillery pieces also have a counter-battery role with a fire factor generally less than the Bombardment factor. You can toggle the calculation of Bombardment/Counter-Battery with this checkbox.

Both create_label and create_checkbox are general in the sense that they can create QLabels and QCheckboxes anywhere, also on the main map. They just need an extra parameter (tag) to decide in which window they are placed. I will implement many more such widgets that can be used by the module developer. Among other things create_label can be used (with a transparent background) to draw circles and squares on the map. A background can be build dynamically with a set of create_labels.


Now comes the definition of the “counters” in the Artillery Fire Table which I call tokens because they must not be confused with the counters on the map.

-- create Luau tokens


local Token = {}

function Token:new (fromid, toid)
    local o = Counter:duplicate(Repository[fromid])
    o = Counter:merge(o, Artillery[fromid])
    o.FullStrength = (o.Image.imageIndex == 1)
    o.counter = toid
    o.gridx = 0
    o.gridy = 0	
    o.Overlays = Overlays:new ()
    o.Overlays:add(Label:new('', "arial", 18, "black", 0, 0, function(o) return o.text ~= '' end))
    return o
end


function Token:flip (id)
    Token[id].Image.imageIndex = Token[id].Image.imageIndex + 1
    local length = #(Token[id].Image.images)
    if Token[id].Image.imageIndex > length then
	    Token[id].Image.imageIndex = 1
    end
    return Token[id].Image.images[Token[id].Image.imageIndex]
end


function Token:pos (id, x ,y)
    Token[id].gridx = x
    Token[id].gridy = y
end

This definition follows the usual defintion of a trait, the Token trait. It has a ‘new’ function and utility functions for flip and pos. Tokens have no right-click menu. They are automatically created when you drop an artillery counter on the map. They are automatically deleted when you delete an artillery counter on the map.

The definition of a token is up to the module developer depending on what is needed. Tokens have a lot in common with counters. They need the same basic image(s) from the Repository, so we duplicate Repository[fromid]. But then they need special attributes to hold values for firepower (see the Artillery trait below). We need to save its id on the map (toid). The grid coordinates on the map must be known.

Note: In Verdun: A Generation Lost there is a square grid. The coordinates are a letter (A-V) + a number (1-34).

The Artillery trait is defined. It needs a field for the various firepower values and must hold the range of the artillery piece. The x, y values are the pixel coordinates (not the grid coordinates) of token in the Artillery Fire Table window. Each piece has a start location.

Artillery = {}

function Artillery:new (x, y, bfull, cfull, breduced, creduced, range)
    local o = {}
    -- placement in artillery window
    o.x = x
    o.y = y
    o.Bombardment = bfull
    o.CounterBattery = cfull
    o.Bomb2nd = breduced
    o.Counter2nd = creduced
    o.Range = range	
    return o
end


Artillery["German/Artillery/5-77-2"] = Artillery:new(104, 136, 2, 2, 2, 2, 6)
Artillery["German/Artillery/5-150H"] = Artillery:new(192, 136, 3, 4, 2, 2, 8)
Artillery["German/Artillery/5-420H-6"] = Artillery:new(280, 136, 6, 2, 3, 1, 14) 

Note: If more than one instance of an artillery piece is on the map (like “German/Artillery/5-77-2”) then they are put on top of eachother.

local function char(num)
    return string.char(string.byte("A") + num - 1)
end


function printCoordinates(x, y)

    x = math.floor((x + 20) / 113)
    y = math.floor((y + 20) / 113)
    
    return tostring(char(y + 1)) .. tostring(x + 1)
    
end


function xyCoordinates(x, y)

    x = math.floor((x + 20) / 113)
    y = math.floor((y + 20) / 113)
    
    return (x + 1), (y + 1) 
    
end

These functions calculate the coordinates. The coordinates come in two variations: what is show above the token (like A3) and what is need when firepower is calculated (the numerical x,y values for determining if tokens are within range: A3 = 1, 3).

Here is the function that does the calculation itself.

function recalculate()

    local counterBattery = get_checkbox_checked('artillery-window', 'cb')
    

    for k, v in ipairs(FireLocations) do
    
	    local text = get_label_text('coor' .. k)
	    
	    -- if target counter on map
	    
	    if text ~= '---' then
		    
		    -- find tokens in target slot and add firepower
		    
		    local power = 0
		    
		    for key, token in pairs(Token) do
		    
			    if type(key) == "string" and key ~= '__index' and type(token) ~= 'function' then				
				    if  v.x == token.x  and  v.y == token.y  then
					    if  math.abs(v.gridx - token.gridx) <= token.Range and
						    math.abs(v.gridy - token.gridy) <= token.Range then
						    if token.FullStrength then
							    if counterBattery then
								    power = power + token.CounterBattery
							    else
								    power = power + token.Bombardment
							    end
						    else
							    if counterBattery then
								    power = power + token.Counter2nd
							    else
								    power = power + token.Bomb2nd
							    end	
						    end
					    end
				    end					
			    end
		    end
		    
		    set_label_text('valu' .. k, tostring(power))
		    
	    end
			    
    end
    
end

get_checkbox_checked finds out if the Counter-Battery box is checked or not. The parameters are the tag for the window and a tag for the box. In this way we avoid “hacks”, hard-coded values in the C++.

A typical Artillery Fire Table window and the corresponding main map looks like this.

Note that target 1 lies beyond the range of the artillery piece in A2, so the firepower is 0. When more than one artillery piece points at the same target the tokens will lie on top of eachother. This will make it difficult to see individual coordinates. I never dealt with this problem in the VASSAL module. Here you can always add a hoover window to inspect the stack, but I drop this now.

Note: class Window still needs work to make in general. I will add more functionality to this class in the prototype for Paths of Glory. Among other things, this class can hold a hand of cards. The class can likely be a base for a mat.

Class Window has its own drag/drop, its own setImage for rendering of tokens. It has the first grid functionality (Window::Frame::snaptoGrid) and functions that implement Label and Checkbox. Actually these functions could be moved out of class window and into a general class GUI or something.

I made an additional trait. In traits.luau

OnlyOneInstance = {
}

Counters that can only exist as one instance has this trait. The trait is used in all target counters (repository.luau):

Repository:addTrait("German/Targets/GermanTarget1", "OnlyOneInstance", OnlyOneInstance)
Repository:addTrait("German/Targets/GermanTarget2", "OnlyOneInstance", OnlyOneInstance)	

The implementation of the trait can be made purely in script (module.luau):

if counter[id]['OnlyOneInstance'] ~= nil then
	-- check if counter already exists
	for k, v in pairs(Counter) do		
		if type(k) == "string" and k ~= '__index' and type(v) ~= 'function' then
			if v.Image.images[1] == id and Counter[k].OnlyOneInstance ~= nil then
				return false
			end
		end
	end
end

Now is time to talk about the new hooks. Recall that hooks are called in the C++ at important moments. There are now 7 hooks (module.luau).

  • beforeDrag - called just before a counter is picked up in drag/drop. If false the counter can’t be moved. This is where the code for OnlyOneInstance is.

  • afterDrag - called just before a counter is dropped. If false the counter can’t be dropped. Here is where code for DoesNotStack is.

  • dropped - called after the counter has dropped. It is then time to create a token in the Artillery Fire Table.

  • moved - called after a counter has moved on the map. Dropped will also call moved. This is when recalculation of the Artillery Fire Table happens.

  • deleted - called just before a counter is deleted to do additional clean-up. This is time to delete tokens and reset coordinate and firepower boxes.

  • flipped - called after a counter has flipped. Time to recalculate.

  • undohook - called after any undo. All boxes in the Artillery Fire Table are reset.

Note: There is no undo functionality in class Window. Undoing movements in this window is meaningless. But since undo changes the main map, undo will also have an effect on the Artillery Fire Table. The way undo works is to move all counters their initial positions and then go forward. Since tokens in the Artillery Fire Table do not “go forward” they remain in their start positions. This means that the whole allocation of targets and tokens must be redone. This is better then trying to “undo” the Artillery Fire Table when no undo exists.


A few comments about how it was to develop dev.laua and class Window.

I was surprised at how much code already existed or could be reused with little effort. I am happy to say that this new functionality followed the general design.

It is never easy to develop in C++. Maybe it’s time to go over to a C++ IDE with a debugger … lol

The Luau code has become a bit “sprawling”, spread over many files. It has to be like this, but note one important thing: all variables and functions are global unless they have the keyword ‘local’. Try to use ‘local’ as much as possible. The pollution of namespace is a problem in scripted languages.

Maybe there should be a better separation between script that is part of the engine and script that is part of the module. I experienced how easy it is to introduce bugs in the undo/redo system. Another point: in the hook beforeDrag lies the implementation of the trait OnlyOneInstance which actually has nothing to do with the the Artillery Fire Table. How do you separate general code from temporal code?

It is the power of script that it is flexible, but changing the script wrongly can introduce bugs or crash the engine. These issues need more attention. Obviously good documentation is important.

The module developer must not have to do a lot of Luau programming to get his module to work. That would be a design flaw. dev.laua is an example of a complex module feature. Most modules don’t need such features. Later I will try to implement a complete game of a “simple” module to see how much coding is actually needed.


New code is on GitHub.

CPLUS_INCLUDE_PATH=/home/me/Qt/6.8.1/gcc_64/include;export CPLUS_INCLUDE_PATH

LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:/home/me/Qt/6.8.1/gcc_64/lib;export LD_LIBRARY_PATH

g++ -Wall -o main main.cpp luau.cpp counter.cpp window.cpp repository.cpp frame.cpp overlay.cpp io.cpp scale.cpp toolbar.cpp settings.cpp -I/home/me/luau/VM/include -I/home/me/luau/Compiler/include -I/home/me/Qt/6.8.1/gcc_64/include/QtWidgets -Wl,--copy-dt-needed-entries -L/home/me/Qt/6.8.1/gcc_64/lib -lQt6Core -lQt6Widgets -L/home/me/luau -lLuau.VM -lLuau.Compiler -lLuau.Ast -lisocline

Now comes a prototype of Paths of Glory. I am sure a totally new game on the engine will give me some “surprises”. The main purpose of the prototype will be to introduce cards. A hand of cards will use a more developed version of class Window. Regarding cards, they have a lot in common with counters. To not duplicate code, what is likely needed is a base class called Piece with derived classes Counter and Card. I must look into this.

After cards come grid. A custom grid has already been made in class Window. Hex grids and square grids must by made. I already did this in GTK.

But before anything else one very important thing has to be done. I have to check if the engine runs on Windows. The C++ code must be compiled with a Windows compiler. I must install the Windows version of the Luau VM

(if I can find it, lol, found it: Luau download | SourceForge.net)

and I must download Qt for Windows. I will test it on Windows 7 and 11.

If the code runs on Windows I presume it will run on Mac and Android too. I have never worked with iOS/macOS. I have no idea how to test software on these platforms without actually owning the hardware …

I got the engine to work on Windows 11. There were a few surprises.

It is best practice to be able to build the engine for all platforms on a single platform. The single platform is Linux. Cross-compilers must exist on Linux that can build the other platforms.

I will go through the Windows 11 version of the engine.


Downloaded the Windows version of Luau (/0.671/luau-windows.zip) from Luau.

Extracted to folder luau.

In folder luau README.md describes how to build the libs.

Cmake must be installed. A cross-compiler must be installed. The obvious choice is g+±mingw-w64.

Read about g+±mingw-w64 here.

Set the compiler Cmake uses like this:

cmake .. -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DCMAKE_BUILD_TYPE=RelWithDebInfo

The Windows versions of libLuau.VM.a, libLuau.Compiler.a, libLuau.Ast.a and libisocline.a are made.


You now need to compile the engine source code with the cross-compiler.

First download the Windows version of Qt. In a previous post I described how this is done. Click Windows x64 under Download Qt for open source use. The downloaded installer is called qt-online-installer-windows-x64-4.9.0.exe. Run it (with Wine) and log in to your Qt account.

Compile the engine with:

CPLUS_INCLUDE_PATH=/data/Utvikling/Alben/Windows/Qt/6.9.0/mingw_64/include;export CPLUS_INCLUDE_PATH

LD_LIBRARY_PATH=/usr/bin/x86_64-w64-mingw32-g++:/data/Utvikling/Alben/Windows/Qt/6.9.0/mingw_64/lib;export LD_LIBRARY_PATH

x86_64-w64-mingw32-g++ -Wall -static-libgcc -static-libstdc++ -o main main.cpp luau.cpp counter.cpp window.cpp repository.cpp frame.cpp overlay.cpp io.cpp scale.cpp toolbar.cpp settings.cpp -I/data/Utvikling/Alben/Windows/luau/VM/include -I/data/Utvikling/Alben/Windows/luau/Compiler/include -I/data/Utvikling/Alben/Windows/Qt/6.9.0/mingw_64/include/QtWidgets -Wl,--copy-dt-needed-entries -L/data/Utvikling/Alben/Windows/Qt/6.9.0/mingw_64/lib -lQt6Core -lQt6Gui -lQt6Widgets -L/data/Utvikling/Alben/Windows/luau/cmake -lLuau.VM -lLuau.Compiler -lLuau.Ast -lisocline

The flags -static-libgcc and -static-libstdc++ are for portability.


Now the great surprise. When I compiled I got an error in io.cpp line 142

if (Settings::playerSide == "")
{
	Settings::playerSide == findSide();
	Luau::updateSide(Settings::playerSide.c_str());
}

It should of course be

if (Settings::playerSide == "")
{
	Settings::playerSide = findSide();
	Luau::updateSide(Settings::playerSide.c_str());
} 

This was just a typo but it was x86_64-w64-mingw32-g++ who detected it. g++ just ignored it !!

This corrected, another error showed up on line 385:

string filename{i->path().relative_path()};

This was a type mismatch and was corrected to:

string filename = string(i->path().relative_path().generic_string()); 

In both cases g++ never issued a warning or error. I am very impressed with x86_64-w64-mingw32-g++.


A main.exe is made, the Windows version of the engine.

Copy main.exe to your Windows 11 platform. Make sure Qt for Windows is on the Windows platform. For me it is in C:\Qt.

Open a cmd and write this:

set PATH=%PATH%;C:\Qt\6.9.0\mingw_64\bin

so that main.exe can find the Qt dlls.


When I first ran main.exe I got this message:

The program can't start because MSVCP140.dll is missing from your computer.
Try reinstalling the program to fix this problem.

The missing dll is a Visual C++ 2015, 2017 and 2019 redistributable.

Link to Microsoft.

Choose the X64 version. With the dll installed, the engine ran. But now came another problem. Many of the the resources (images) were not found. The resource names had back-slashes. Good, old Windows with back-slashes in its file path that can be mistaken for escape characters … lol.

In io.cpp line 374 this:

string str = i->path().parent_path().string() + "/" + i->path().stem().string();

was replaced by this:

string str = i->path().parent_path().string();
			
// convert back-slashes to fore-slashes
std::replace(str.begin(), str.end(), '\\', '/');

str = str + "/" + i->path().stem().string();

The engine now ran and looked like this:

But what? No menu bar? Only an arrow to the right? The menu items were found there:

I realized that the problem had to do with the height of the menu bar. In line 224 of main.cpp I changed this:

menuBar->setFixedHeight(20);

to this:

menuBar->setFixedHeight(22);

That was it. The engine works under Windows. Wow !!!

I am glad this was possible with little hassle. The code that runs under Windows is exactly the same as the code that runs under Linux. The small changes made had no influence on how the engine ran on Linux. This is very important from a practical point of view. No different versions of code for different platforms … arrg.


Regarding Windows 7 it is clearly stated that Qt6 does not support Windows 7.

It is possible to use the old Qt5 … but I do not want to use time on that now.


Besides platforms I wanted to look at deployment. I wanted to check out how the engine could run independently, not only in my development environment.

The first thing to do was to collect all needed files in one folder. They are:

  • The resource files __images and images
  • The Luau script files
  • main
  • The needed Qt libraries

By using ldd I could see the dependencies of the Qt .so-files. I ended up with this collection:

This is a complete list of all Qt files needed to run the engine. The file libicudata.so.73.2 is the largest at 32 MB.

The total size of all the needed files for the engine (the size of the folder) was 75,9 MB (72 items).

Fortunately this size can be reduced substantially by using compression. There is a tool called makeself (https://makeself.io/) that can both compress and run a script when decompressed. I call the compressed file main.run. It is generated by this call:

makeself --nox11 ./makeself ./main.run "Game engine" ./run.sh

The --nox11 option is there to not show a terminal. The 75,9 MB folder is called ./makeself.
./run.sh is a script that is run after decompression. It is:

#! /bin/bash
export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:./Qt
export QT_PLUGIN_PATH=./Qt
export QT_QPA_PLATFORM_PLUGIN_PATH=./Qt
./main

It sets needed environment variables relative to ./makeself and calls main.

makeself creates a self-decompressing and self-starting application called ./main.run. In order to run ./main.run create a Linux launcher shortcut as:

/home/me/Utvikling/Alben/main.run &

The ‘&’ has the effect of starting a new thread for main. The downside is that the user clicks on the shortcut and “nothing” happens for a second or two while the decompression takes splace. I am sure there are other ways of doing the deployment than makeself.

main.run is 32,1 MB, substantially less than 75,9 MB. Yet, a module will always have about this size. With hard disks of 1 TB and download speeds of 10+ Mbps this should not pose a problem today. main.run is totally self-contained. It has its own copy of the Qt library, which means that the module never becomes “out of date”. Only the computer runtime can make a module not run anymore.


Obviously more work has to be done regarding platform and deployment. I have shown that the engine can run on a major platform besides Linux. I have shown that the engine can run on a platform without Luau or Qt installed.

Now comes what I look forward to: a prototype for a totally new game, Paths of Glory. This game is of course far more popular that Verdun: A Generation Lost.

It will still just be a prototype, not a full game. In this prototype cards will be introduced.

2 Likes

Hi all. A lot has been done. There were certainly a few surprises, a couple of serious problems (I will come back to them) but I have made a crude but functional module for Paths of Glory.

The new code is as usual found on my GitHub. If you want a complete guide on what has changed study the code.


First of all, it was time to start using cmake. Total lines of code has passed 12000. In the source directory there is now a CMakeLists.txt. In the README are instructions for building and running. Note this line:

cmake -B ./build -D CMAKE_CXX_COMPILER=g++ -D CMAKE_RUNTIME_OUTPUT_DIRECTORY=..

It makes sure the executable is in the source directory so it can find images and __images.

I used the Paths of Glory classical map.

setBackgroundMapID("map_classic_vassal")

In Paths of Glory you have predefined setups. There is no repository window. I turned off the repository window with

repository_visibility(false)

Note: even if the repository window is not used it is very important to define all the counters. This is done in repository.luau. It is also a good idea to set the contents of the repository window in an orderly manner after root(0). You will use this window to make setups. Set repository_visibility(true) when making setups.

I will later make the listing of counters and traits easier by using wild-cards (*) so you don’t have to write every single counter seperately.


A setup is like any saved game. I call my setup for setup.vsav. At game start it will be loaded with

load_setup('setup.vsav')

Note: it is very important that the script can not load arbitrary files from the host machine. load_setup is in C++ defined like this:

void IO::loadSetUp(string filename)
{
    QFileInfo fileInfo(QString::fromStdString(filename));
    
    QString base = fileInfo.baseName();
    
    
    
    string completeFileName = "./setups/" + base.toStdString() + ".vsav";

    LoadGame *load = new LoadGame(completeFileName);	
    delete load;
    
    Counter::resetId();
    Counter::resetZorder();
    
    Counter::setGUI();
    
}

The input file name is stripped to its base name. Then it tries to load the base name with the standard suffix “vsav” from the “setups” directory in the source directory. If it can’t find this file nothing happens.


When you start the game you only see a few counters on the western front, the six CP Action markers for the Central Powers Action Round Chart, the Combined War Status, Allied War status and CP Was Status markers on the General Records Track and four cards down in the Central Power Draw pile. GitHub is not made for storing large numbers of images files, so the module only contains these playing pieces.

I will go through all these counters and describe how I have implemented some key functionality. I have not tried to implement a complete functionality.


Loading up a VASSAL map for Paths of Glory (version 10.2 Classic Map), the first thing one notices is the huge amount of grid points. It would have been fine to somehow automate the collection of these points, but that is not for now. Instead I use the already defined grid points in the VASSAL module.

One also notices how the map is divided into various areas with their own grids. I think that these areas are called zones in VASSAL. Each zone has its own type of grid. It can be a custom grid or a regular (hex or square) grid.

The concept of zones is so important that it will be implemented now. There will (for the time being) be two types of zones, one with a custom grid and one with a square grid. The definition of a zone will be as follows:

  • An area the defines the boundary of the zone (border). For the time being it will just be a square defined by the top-left and bottom-right coordinate. There is nothing that prevents this area from being defined as a polygon. Simple algorithms that decide whether a point is inside the polygon exist, see f.ex. stackoverflow.

  • A set of points (grid) inside the defined area.

  • A set of counters (members) that belong to the zone.

  • The type of grid (gridType), custom or square.

  • A flag that tells what happens if you drop a counter in the zone that is not a member (allowUndefined). If true a non-member will snap to the default grid (not the zone’s grid). If false you will not be able to drop a non-member within the zone’s boundary. This can be useful if you want to avoid counter clutter.

Note: It is fully possible for a counter to be a member of several zones. If a zone overlaps another zone the result is undefined. All zones have a unique tag (identifier).


When implementing zones I first made a C++ class Grid. I quickly realized that this was wrong.
There already is a snap-to-grid functionality in the hook afterDrag (module.luau). There is no reason to implement zones with C++.

A new file called zone.luau now exists that implements the trait Zone (for a particular zone) and the trait Zones (for general operations on a Zone).

In making snap-to grids my first problem came. I started off wrongly. I forgot a vital fact about the design … yes, the designer forgets his own design .. a lot of things to remember. It has to do with how the undo system works. I will describe the problem because it is instructive.

Each time you undo, the undo system works by starting off from a base level and going forward to the next to last level. If you delete a counter going forward, the counter is deleted in C++ but not in Luau. The reason is that information about deleted counters must exist somewhere in order to start at the base level again (when the counters were not deleted). Information about deleted counters is not stored in the C++ part.

This became apparent to me (even if I should have known it very well) only when I started to iterate over counters in Luau. In zone.luau I find a snap-to-point like this:

function Zones:snaptoDefault(id, x, y)

    local counter
 		
 	if Counter[id] ~= nil then
	    counter = Counter[id]
    else
	    counter = Repository[id]
    end
    
    
    local maxDistance = 8;
	    
    for k, v in pairs(Counter) do
    
	    if type(k) == "string" and k ~= '__index' and type(v) ~= 'function' and v.deleted == false then
    
		    if k ~= id then
			    
			    if math.abs(v["x"] - x) < maxDistance and math.abs(v["y"] - y) < maxDistance then
			       	
			       	-- a DoesNotStack counter never stacks			
				    if counter["DoesNotStack"] ~= nil then
					    return false, x , y
				    end
				    if v["DoesNotStack"] ~= nil then
					    return false, x , y
				    end
				    
				    -- a card only stacks with other cards
				    if counter["Card"] ~= nil and v["Card"] == nil then
					    return false, x , y
				    end
				    if counter["Card"] == nil and v["Card"] ~= nil then
					    return false, x , y
				    end
				    
				    x = v["x"]
				    y = v["y"]					
				    return true, x , y
			       
			    end 
			      	
		    end	 
	    end	
    end
    
    return true, x , y 
    
end

Note the v.deleted flag and testing if this flag set to false. Counter:new is defined as:

function Counter:new (id, images, imageIndex, zorder, x, y)	
    local o = Counter:duplicate(Repository[images[1]])	
    setmetatable(o, Counter)
    Counter.__index = Counter
    o.Image.imageIndex = imageIndex
    o.id = id
    o.x = x
    o.y = y
    o.zorder = zorder
    o.deleted = false
    return o
end

When you delete a counter o.deleted is set to true. The problem now is that the number of deleted counters may grow large. This is why it will be necessary to allow, say, only 200 undo levels. When this number is reached the base level is reset to how the game looked like at 100 undo levels, and so on.

When you record a log file, you can of course not reset the base level before you have saved the log file.

I realize why undo functionality is bug-ridden. The only answer is testing. This engine has the advantage of automating testing by feeding it with thousands of test-games.


In Paths of Glory it’s pointless to show the map beneath each counter in the hoover window. Instead the name of the location is shown. This is set with:

hoover_ShowMap(false)
hoover_ShowPlace(true)

It’s possible to both show the map and location (place) by setting both these to true.

Note: the hoover window is rather crude in comparision with the window in the VASSAL module. It’s fully possible to customize the hoover window (background, corners, bars, captions, etc.) but this is not something I want to use time on now.


Let us now look at the counters on the Western Front.

(obviously a lot of counters are missing but the intention was not to make a complete module)

You see the BEF, 3 French armies and 3 German armies. There are also two Trench Level 1 counters set up where they are supposed to be. Note how they have the same offset as in the VASSAL module. They belong to the same stack as the armies but have a rendered offset.

To achieve this I give the trench-counters a special trait called Marker:

Marker = {}

function Marker:new(dx, dy, side)
    local o = {}
    o.renderOffset = {dx = dx, dy = dy}
    o.Side = side
    o.Delete = Delete
    o.CantBeMoved = CantBeMoved
    return o
end

Repository["Allied/ap_trench1"] = Repository:new({"Allied/ap_trench1", "Allied/ap_trench2"})

Repository:addTrait("Allied/ap_trench1", "Marker", Marker:new("Allied/ap_trench1", -20, 0, "Allied"))

The trench marker can not be moved. It has a special CantBeMoved trait.

CantBeMoved = {
}

The advantage of this is that when you select a stack with a trench-marker, the marker is not selected and therefore also not moved.

function selectable(id)
    
    return Counter:findTrait(Counter[id], 'CantBeMoved') == nil
    
end

Note: In the VASSAL module you can select the trench and also move it. I am not sure if moving a trench has any purpose. In this module you can also right-click the trench counter (to delete or flip it).

In the same manner you can right-click the “vicinity” of an army to place a trench marker there. You can not select the “vicinity” (unlike the VASSAL module). Vicinity (Area) is defined as an area extending 15 pixels at 100% scale around the counter.

bool CentralFrame::Area::findCounter(QPoint point, Counter *&foundCounter)
{
    
    int extra = (int)(15 * Scale::scaleFraction);
    
    .
  
}

When you right-click, only menu-items that can be selected will show. Since there are no CP trenches (in this module) you see no menu when you right-click close to any German army. Whether not showing unused menu items is a good idea I don’t know. It is of course possible to disable menu-items rather than not showing them.

In the VASSAL-module you can right-click any location (even without an army/corps of any nation there) and place a trench counter. This can not be correct (but maybe it doesn’t matter).


The Central Powers Action Round Chart has been implemented with six CP Action markers. They start off in in their start positions along the bottom.

The Central Powers Action Round Chart is defined as its own zone with a custom grid. You can right-click each marker. You can reset the individual marker or reset all markers. Each CP Action marker has a Move trait.

Move = {}

function Move:new(name, point)
    local o = {}
    o.x = point.x
    o.y = point.y
    o.menuname = name
    o.menuclick = "actionMove"
    return o
end

It has the coordinates where to move if you select ‘name’ in the menu. The ‘Reset all markers’ has a MoveAll trait.

MoveAll = {}

function MoveAll:new(zone, name)
    local o = {}
    o.zoneTag = zone	
    o.menuname = name
    o.menuclick = "actionMoveAll"
    return o
end

The MoveAll action (actionMoveAll) runs through all the members of the zone and calls its Move trait.

local tag = Counter[id].MoveAll.zoneTag
local zone = Zones:get(tag)

for k, v in pairs(Counter) do
	if type(k) == "string" and k ~= '__index' and type(v) ~= 'function' and v.deleted == false then	
		if zone:hasMember(Counter[k].Image.images[1]) then					
			actionMove(window, k) 
		end
	end
end

Down in the left corner is the General Records Track. Only 3 counters are defined, the Allied War status marker, the CP Was Status marker and the Combined War Status marker. The Combined War Status marker can never be moved by the user. It has a CantBeMoved trait.

Repository:addTrait("Markers/warstatus_combined", "CantBeMoved", CantBeMoved) 

The Combined War Status marker can only move as a result of moving the Allied or CP Was Status markers. The Combined War Status can never be moved beyond 40.


All windows now have a base class called Window, which in turn is an implementation of QMainWindow. This means you can create an unlimited number of sub-windows. The main map window is the parent. The sub-windows are children of the main map window.

Each window has a tag (an identifier). This tag is used to access any particular window. The tag is defined by the script (the module developer). The tag for the main window is ‘main’.

As a base class, Window has a few general utilities that all windows have in common. You can create various widgets like buttons, labels and check boxes and you can create so-called tokens, which are playing pieces you can move like counters. Tokens are simplified Counters.

The Window class also renders all widgets and tokens in a window.

It was here that the most serious problem I have hit upon in the prototype occurred. I have not been able to solve the problem but have made a work-around (an “ugly hack”) that is sufficient.

First I give the code that crashed. When you have to delete a token with this code

void Window::removeAToken(int key)
{

    Token *token = tokens[key];   
    delete token; 
    tokens.erase(key);
    setWidgets();
    
}

you get a crash with a free(): invalid pointer message. No matter what I did I still got the crash. The work-around consisted of using so-called ‘delayed deleting’.

void Window::removeAToken(int key)
{
    
	    
    // a very ugly hack; need to return to this
    
    Token *token = tokens[key];
    
    setAsDeleted.push(token);
    
    token->setVisible(false);
		    

    
    tokens.erase(key);
    
}

together with the code in Window::setWidgets

// "delayed" deleting - ugly hack 

while (!setAsDeleted.empty()) 
{
    delete setAsDeleted.top();
    setAsDeleted.pop();
}

The work-around is perfectly acceptable. There are no memory leaks now.

I suppose the crash has to do with threading. The thread Luau is in is maybe not the same thread as the sub-windows are in. I doubt whether it has to do with the heap, because Qt states it uses a single heap. In any case when it tries to delete a token in this way, it does not have enough information, so a crash happens.

I will try to make a small program that illustrates the problem and ask people for more information.


A hand in Paths of Glory is a sub-window. You open it with the button on the toolbar. The button is small compared with the VASSAL module. I don’t think it is a good idea to change the height of the main toolbar. Instead the module developer will be able to make a custom toolbar with a height that shows the button better. This will be done later.

You resize the hand-window by resizing its height. There are no zoom buttons.

I was surprised at how big a 100% card is. So I put in an automatic resize to height 400 pixels. This number is for the time being hard-coded.

// resize if 100% too big, 400 is height of window minus scrollbar 	
if (h > 400)
{
	((Frame *)this->frame)->height = 400;
	this->resize(w, 400);
}
else
	this->resize(w, h);

The greatest difference from Verdun: A Generation Lost is the cards. To implement cards it is useful to look at the difference between cards and counters.

  • Cards are usually larger than counters. Obviously the size difference can not be quantified. There is no “magic” size where counters become cards. The size therefore doesn’t matter for the implementation.

  • Cards have only two sides, the face-down side with a generic image and the face-up side with its own unique image. Counters can also only have two sides, so this doesn’t matter for the implementation.

  • Cards on top of each other form a deck, while counters on top of each other form a stack. This means that cards and counters together are never considered a single entity. They never stack. This is important for the implementation.

  • You can hoover over a deck of cards (at least if the number of face up cards in the deck is not too large, there is no point in showing face-down cards).

  • A deck usually has more cards than a stack has counters. This means that its representation must be different. An offset-less deck is like an offset-less stack (except that the number telling the size of the deck may not be there). An offset deck can only have offset for a limited number of cards. The offset itself is usually smaller then for a stack.

  • Decks most often have a random order while a stack has a fixed order. It depends on whether the deck is always reshuffled or not. In an always-shuffled deck the top card of a face-down deck is chosen randomly from all the cards in the deck while the top counter of a stack is always the same.

  • If you turn a face-down card from the top of a deck the top card is known. It is not given what happens if the face-up card is turned down again. Does the same card remain on top or is a new card drawn? It depends on whether the deck is ‘shuffled’. For now decks are never shuffled, which means that drawing a card from the top will always be the same card.

  • Cards (unlike counters) can not be deleted, only moved to a discard pile or to another place as ‘used’ or ‘spent’.

  • Cards can scaled like counters (but I doubt whether rotated cards have any meaning).

It should be clear that cards only differ from counters in small ways. It is not necessary to implement a whole new C++ class for cards. This cuts down much code.

The difference between cards and counters can be expressed with a trait. A card has a Card trait. For now it is simply:

Card = {}

function Card:new(deck)
    local o = {}
    -- 1 face up, 2 face down (default)
    o.deck = deck
    o.face = 2
    return o 
end

In addition it is necessary to define a deck. A deck contains a number of cards. A deck is at a specific location.

Deck = {}

function Deck:new (id, move, gridpoint)	
    local o = {}
    setmetatable(o, Deck)
    Deck.__index = Deck
    o.id = id
    o.cards = {}
    o.x = gridpoint.x
    o.y = gridpoint.y	
    o.moveto = move
    Decks:add(id, o)
    return o
end	 

A deck is added to Decks, the total set of decks. A very important additional feature of decks is that they define where its cards can be moved (o.moveto). A deck is typically defined as

local draw = Deck:new('draw',{Move = Move:new('Discard', p2)}, p1)

where p1 is the deck’s location and p2 is where its cards can be moved to. Ex: in the ‘draw’ deck p2 will be the coordinates of the ‘discard’ deck.

Note: all these definitions are done in the Luau part. They are not “hard-coded”. Maybe different definitions of cards and decks can be useful. This is up to the module developer. The definition of cards and decks is not final. My definition is just useful for the implementing cards in Paths of Glory.


A sub-window holding the CP hand has been defined. It only has two slots, not eight. Like in the VASSAL module, the slots have a Draw button. You draw cards from the Draw deck. The function-member doing the drawing of cards looks like this:

function Deck:draw()
    if #self.cards == 0 then
	    return nil
    end	
    local n = math.random(1, #self.cards)
    local toId = self.cards[n]
    table.remove(self.cards, n)
    delete_class(toId)
    return toId
end

Once drawn the card is discarded when it has been used. After all cards have been drawn/discarded, the Discard Deck needs to be reshuffled back to the Draw Deck. This loop of draw-discard-reshuffle is a key element of Paths of Glory.

You can also drag a scaled card from the CP hand to the map window by holding down the Left-Ctr key while dragging. Without Left-Ctr the card is shown 100% (which is very big).


Undo functionality of cards works poorly if at all. I will have to look at this. I will have to implement
Undo over several windows, a global Undo functionality. Also, it’s high time to turn off rendering while undoing. This avoids the annoying ‘blinking’ when undoing. It also avoids lag when the huge cards are rendered over and over.


A comment about the card files. In the VASSAL module they are given as SVG (Scalable Vector Graphics) files. Qt has a SVG renderer class , see here. To be useful in this engine, a scaled SVG file has to be rendered as a QImage object. First the SVG is scaled, then it’s transferred to a QImage. Unfortunately when I tried to do this, the transparent areas of the SVG file was not rendered transparent in the QImage object. What is the reason for this? Therefore I used ImageMagick to convert the VASSAL SVG files to PNG files. ImageMagick manages to maintain transparency.

I compared my rendered cards with the cards in the VASSAL module. The only difference I could see was a slightly better rendering of ‘Mobilization’ on top of the card.

The topic of using SVG has to be returned to. But note one thing: it is never a good idea to use scanned text. The numbers on the bottom of the counters in Verdun: A Generation Lost was not made by scanning original images but by font rendering, where I found a font and a size that matched the counter art. It was a tedious process to render all the counters in this way, but the quality became much better.


Paths of Glory is a large and complex module and it has obviously not been implemented in its full. I hope it is shown that there is nothing in principle that prevents a full implementation of the module on this engine.

Next up is Panzergrüppe Guderian. Hex grids will be implemented. I hope (unlike Paths of Glory) to make a full implementation of the module (minus the online part). I hope to do this because PGG is a relatively “simple” module. No counters will be provided. Instead the counters in the VASSAL PGG module must be used and installed in the images folder.

After Panzergrüppe Guderian it is time to make the chat/log window. Logging of all moves will be implemented, also the output of the Luau compile- and runtime errors. It is high time they are visible. With this in place, there is a good opportunity to look at undo while recording a log file.

1 Like

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 :slight_smile: .

2 Likes