Some new design thoughts

Hi all. Been awhile.

I have recently had some more thought about the design of a boardgame engine. The topic intrigues me. Some may remember my efforts with Lua, Qt and binding.

https://forum.vassalengine.org/t/lua/10461

The conclusion was that the binding simply did not work. Also, the size of the binding libraries was an order of magnitude greater than the Qt libraries themselves, not good.

On my GitHub wiki I have sketched another way of doing the binding while at the same time ensuring safety. Check it out. Being able to call the GUI from Lua like this is very cool. No binding. I use GTK because it is a standard part of Linux but any other graphic library may do, including Qt.

I will continue to analyze designs and do small feasibility tests. There are a number of issues that Vassal must deal with which I will not deal with. But I hope my design ideas will have some value. I will continue on an irregular basis and post on this thread when the wiki is updated. Comments are welcome.

By the way, how is development of Vassal 4.0 going ?

So quiet here? I hope this thread is not considered spam even if it is not directly related to Vassal.

Next up is the module, see page 2 on my GitHub wiki. Not surprisingly, the module is not too different from a Vassal module. But unlike a Vassal module it contains a directory of Luau scripts. Here I stumbled into my first big problem in design.

There is a good reason why Java classes are closely associated with their files. If all Java classes were in a single file the code would become unwieldy. In Java it is easy to reference classes in other files.

file 1: public class ClassName {}
file 2: ClassName variable;

In Lua we have the require(“fileName”). The problem is that when you load and compile Luau script files into the VM the file name of each individual file becomes meaningless. The problem was solved by using the chunk name in the load-step to define a Luau global that in a unique way can reference the script (chunk) in a file.

What does the Luau script in the design actually do? Two things. It defines a start state for the whole game, how windows, maps, counter, cards, etc. look. Second, it defines what happens when various events are triggered, like right clicking over a counter. That is why the script must be able draw a specific menu with entries. But the script never calls the GUI directly, only calls exposed C functions with parameters which in turn calls the actual GUI. For complex calls C is able to directly access datastructures (tables) in Lua for information.

In principle the module designer must be able to draw whatever on the map, like circles and dotted lines. It depends on what GUI featues are exposed.


In my tests I use GTK simply because it is available by default on Linux systems. I just have to install the header files (libgtk-3-dev).


Next up is Drag-and-Drop. This feature is fundamental and must work.

Then

Grid
Counters and counter attributes
Counter stacks
Game state and undo functionality
… and the list goes on

The fun thing about such a design project is that you never know where it leads you. I may well stumble into problems that can’t be solved or problems I do not wish to use more time on. We will see.

1 Like

My GitHub wiki has been updated with a page on Drag and Drop.

The module designer can set the transparency (alpha) of a dragged counter. All else is located in the C++ part of the engine and need not be modified by the module developer.

The drag and drop is done with GTK3. I have not worked much with GTK. My impression is not good. The knowledge base is small. Examples of code are rare. New versions of GTK require applications to be rewritten. That must be the reason why a Linux distribution have libs for both GTK2, GTK3 and GTK4.

In GTK4 you can not set z-order. Yes, you read correctly. Maybe it’s possible to reorder the children of a container? Who knows. Why have they made such a basic feature so inaccessible?

It will likely soon be necessary to use Qt instead of GTK. But making a small suitable Qt
lib is a project in itself. I had hoped to make some simple pilot test with GTK.

It is my impression that GTK is not well designed. That each new version is so different
shows this. A good design never needs change, only additions. Linux/UNIX is an example of a design that has never needed change.

Next up is Grid. I have the basic idea of the design. Grid is the first real test for how a module is developed. I hope to show what I mean by “the script is the module”. The main idea is that you should not need a separate tool to develop a module. The script of a module tells you how it works. To test and see your modifications you just need to reload the script.

1 Like

My GitHub wiki has been updated with a section on grids.

The code generates a simple hex grid with snap-to functionality on the center dots.

It is possible to display the grid as a circle with a given radius, color and alpha. Such a display makes it possible to align the grid to the map. The display can be turned off with the function gridShow(false). If you run this code, make sure colors and alpha lie between 0.0 and 1.0. There is no input validation.

This is how the module developer will work. He will have the ability to specify the size of the grid (with what I call that the a and b parameters) and a grid offset. Be displaying the grid, he will then be able to see to what degree the grid fits with the map’s grid and make adjustment.

The adjustment is immediately visible by the ability to reload the Luau script while the engine is running!

There is no tool to align the grid, only this reloading of script. You can say that the tool is the module window itself. This is the core of the design.

There are pros and cons with such a design. Pros are flexibility and simplification. Cons may be the fact that the module developer needs to know Lua scripting, that more is done “manually” and that documentation (and good error messages) are vital. The method of making a module must be intuitive or else the design in flawed.


Next now is the counter and its traits. The concept of traits will be similar to that of VASSAL. It is also a good idea that traits exist in stacks, where a trait influences all traits below it on the stack.

There must be a separate counter window that will show the counters. A change to the counters in the script will be easily visible after a reload of the script.

The basic premise is that the definition of counters and their traits are defined in a flat Luau script file (or files). Traits can be thought of as Luau functions. Many common traits can be pre-made in the Luau part of the engine, like the Delete trait.

The interesting thing about scripting traits is that the module developer can make his own traits. Since all traits are Luau scripts, any trait can be modified or expanded as needed. The only limitation is what is offered from the exposed functions of the C++ API.

Traits have functional aspects, but they can also have visual aspects (like the Can Rotate trait). Traits may or may not have an entry in the right-click menu of a counter.

The data structures (tables) in Lua must be used to define counter-templates that can be copied up for each new counter.

The concept of traits in counters is obviously a large and complex subject. I will not try to solve all problems at once but go forward step by step. The first step is to load the image resources and give them an id. The obvious id is the filename but if a file has the same name in two sub-directories then a number must be added. By printing to a log-file what id belongs to what image file, the module developer can use the ids to specify counter images.


Later it will be time to deal with stacks of counters, among other things changing the position of counters in a stack. I’m afraid I will have to start using Qt for this, which means making a Qt library that is small, sufficient and can be redistributed. This will take time.

A lot has been done. I am glad to say that three traits have been implemented. VASSAL players will recognize them. The design is starting to show itself. Read on.

First off a few concepts. Traits only exist in the Luau-part of the engine. The C++ part of the engine only performs necessary GUI tasks to make the effect of the traits shown. This split is important, for it gives the module designer freedom to define what ever trait is necessary as long as it fits in with what the C++ part can offer.

A trait is a Lua table of key/value pairs. Some keys are standard, like “menuname” (if the trait has an entry in the right-click menu) and “menuclick” (what function to call if a particular right-click menu entry is clicked). But basically a key/value pair can be whatever as long as their use is defined in the implementation of the trait. The Image trait will for instance have the key “images” and as value a table of all the possible images the trait can take. The Lua table data structure is very flexible and can contain other Lua tables.

A counter is a Lua table of traits. Counters (unlike traits) need to have a C++ representation. Therefore a counter must also have a function called “create_class” that creates a C++ class for the counter. But a module developer is free to define whatever special functionality a counter may have.

I know these concepts are a bit confusing at start. The best way to see what I mean is to study the code. Link here:

GitHub wiki

In file traits.luau all the traits exist. In VASSAL designers see a popup window with all available traits on the left side and arrows that move traits into the right side and that can also change their order. In this design lies in the script. There is no tool, only a script file (traits.luau) that define all available traits and a class called Traits that is used to assign a group of traits to a counter. Here is traist.luau:

-- a trait is a table
-- traits have common table-fields but extra custom fields may be added



Image = {}

function Image:new (images)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    --- fields
    o.images = images
    o.menuname = "Flip"
    o.menuclick = "actionFlip"
    return o
end


--- since Rotate and Delete has the same function everywhere only one instance may exist


Rotate = {	
    degrees = 60,
    menuname = "Rotate",
    menuclick = "actionRotate"
}				



Delete = {
    menuname = "Delete",
    menuclick = "actionDelete"
} 



Traits = {}

function Traits:new (images)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    --- fields
    o.Rotate = Rotate	
    o.Image = Image:new(images)
    o.Delete = Delete
    return o
end


return Traits

Since this is a script, the module designer is free to make new Traits classes with a custom selection of traits. The order of traits is the order in the script but the use of order has not yet been implemented. Here is counter.luau:

-- a counter is a table of traits
-- the order of traits is as they appear in the table with the lowest trait first

Counter = {}


function Counter:new (images)
    local o = Traits:new(images)
    setmetatable(o, Counter)
    Counter.__index = Counter
    o.imageIndex = 1
    table.insert(Counter, o)
    create_class(images[1])
    return o
end


function Counter:delete (id)	
    Counter[id] = nil	
end


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



return Counter

Counters are created in module.luau:

-- create two counters


Counter["12R-51R"] = Counter:new({"12R-51R", "12R-51R_f"})
Counter["BRIGADE-3"] = Counter:new({"BRIGADE-3", "BRIGADE-3_f"})

Counters are key/value pairs where the key is the name (id or filename without extension) of the counter and the value is an instance of the counter class (a table). The constructor takes a list of images the counter can be (here two images, the front and flip side of a counter).

On the GitHub wiki page I have illustrated the sequence of calls between the Luau part and C++ part of the engine to implement and execute traits. I hope this shows how flexible the design is.


The window with counters (the counter repository) is a separate window. The chat window and server window may also be separate windows. It may be useful to maximize the map window and pin the chat window/counter window on top of it or just maximize when needed.

There should be an option to have a multi-windowed application or a single-window application with sub-windows.


Regarding what’s coming next, I am unsure if I want to use time right now on dragging copies of counters to the map window, stacking them and dealing with selection/reorder of stacks.

The basic idea is to be able to open a stack in a window and do reorder/selection there.

Only one stack can be opened at a time. This prevents clutter on the board. Stacks with no offsets between counters can have a small number in the top right corner telling how many counters are in the stack.

Stacking is more an implementation issue than a design issue. I will have to find another GUI library for that. If anyone thinks it’s possible to do this with GTK, feel free to comment. My knowledge of GTK is limited.

What is more interesting from a design perspective is to implement game state and UNDO/REDO functionality. Fortunately the design encourages a split between the logical representation of the game and its visual representation. The logical representation should at any time be able to generate the visual representation. Undo is then a matter of running through the state-stack until you reach the next to last move. Redo is simple, for the next state on the stack will not be deleted until a totally new move is done.

The split between a Luau part and a C++ part, where the C++ part solely deals with the visual representation, means that if game state lies in the Luau part then it’s totally independent of any visual representation. The state-stack can be changed without changing what you see on the board. When you run up the state-stack to the next to last move, the state is ready to be shown on the map.

Undo/Redo has been implemented with a stage stack and a stage pointer.

Check out the new GitHub wiki page.

First let me say more about what was done first.

Drag and drop was already implemented but I had to make a new trait which VASSAL users will recognize as the Marked When Moved trait. I call it MarkMoved. This trait is basically a mask with an offset relative to the counter. The MarkMoved trait inherits a basic Mask trait as shown below in traits.luau

Mask = {}
    
function Mask:new (mask, x, y)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    --- fields
    o.mask = mask
    o.offsetx = x
    o.offsety = y
    return o
end



MarkMoved = {}
    
function MarkMoved:new (x, y)
    local o = Mask:new("_Moved", x, y)
    setmetatable(self, {__index = Mask})
    self.__index = self
    o.moved = false 	
    o.menuname = "Moved"
    o.menuclick = "actionMoved"
    o.movetrigger = function(id) Counter:markMoved(id) end	
    return o
end

This is how inheritance works in Lua. MarkMoved creates a Mask with _Moved as the image and offsets x and y. In addition MarkMoved needs its own fields to tell if the mask is active (moved) and a trigger for what shall be done when it receives a move call from the C++ part (function movetrigger). Besides this it needs the usual menuname and menuaction for the counter’s right-click menu. The menuaction flips the counter’s move status.

The _Moved is an index to the image resources.

string file = Counter::resources["_Moved"];

Both system resources (inherent images of the engine) and resources loaded from the module (the images created by the module developer) will be indexed in the same way with the convention that system resources have an underline.

If you look at the definition of a counter in counter.luau

Counter = {}


function Counter:new (images, x, y)
    local o = Traits:new(images)
    setmetatable(o, Counter)
    Counter.__index = Counter
    o.degrees = 0
    o.x = x
    o.y = y
    --table.insert(Counter, o)
    create_class(images[1], x, y)
    State:add(images[1], Counter:copy(o))
    return o
end

you will see fields for x and y position. It’s probably a good idea to move this out to a new trait called Position. Besides x, y coordinates this trait can give the position in a stack and also the position (location) within a hex. A module developer can f.ex. add a custom field for what floor the counter is on.

In module.luau where counters are created you will now see this:

-- create two counters


Counter["12R-51R"] = Counter:new({"12R-51R", "12R-51R_f"}, 130, 100)
Counter:addTrait("12R-51R", "MarkMoved", MarkMoved:new(72, 15))


Counter["BRIGADE-3"] = Counter:new({"BRIGADE-3", "BRIGADE-3_f"}, 250, 100)

I have made a special addTrait function that can add traits individually to counters. “12R-51R” has a MarkMoved trait while “BRIGADE-3” does not have one. Also note the calculated offsets 72 and 15. This presumes all counters are of the same size. It is possible to let these offsets be calculated as a function of the counter size.


In the C++ part a very important change was necessary. A counter has to be generated from a base image to its current image as defined by the state of the counter. The state of a counter in C++ is for the moment defined by these five fields in a state struct.

struct State
{
	int x;
	int y;
	bool moved;				// only true if trait		
	int degrees;			// only not zero if trait
	std::string name;		// name of current (flipped) image	
};

Every instance of a counter-class has a function to generate it visual appearance:

void Counter::setImage()

See the full code on GitHub. I also made some changes to the rotate function but “unexpected results” may occur when moving rotated counters. It will be rewritten with the new GUI library. Note that struct State is updated from the Luau part, not the other way around.

counter.luau

function Counter:generateGUI()
    
    for k, v in pairs(Counter) do
	    
	    if type(k) == "string" and k ~= '__index' and type(v) ~= 'function' then
	    
		    local name = Counter[k].Image.images[ Counter[k].Image.imageIndex ]
		    
		    generateGUI(k, 
					    Counter[k].x, 
					    Counter[k].y,  
					    Counter[k].MarkMoved ~= nil and Counter[k].MarkMoved.moved,
					    Counter[k].degrees,
					    name)
	    end
    end

end

luau.cpp

static int generateGUI(lua_State *L)
{		
    
    const char *name	= lua_tostring(L, -1);	
    int degrees 		= lua_tonumber(L, -2);	
    bool moved 			= lua_toboolean(L, -3);
    int y 				= lua_tonumber(L, -4);
    int x 				= lua_tonumber(L, -5);
    const char *id 		= lua_tostring(L, -6);
    lua_pop(L, 6);
    
    Counter *found = Counter::findObj(id);
    if (found)
    {
        found->state.x = x;
        found->state.y = y;
        found->state.moved = moved;
        found->state.degrees = degrees;
        found->state.name = string(name);
        found->setImage();
    }
    else
        printf("error\n");
    
    
    return 0;
}

A game state is just the value of all fields in all traits in all counters at a given instance.

A move brings the game from one state to the next state.

Moves are stored on the stageStack.

A stage is a number of moves that brings the game state from on meaningful instance to the next meaningful instance. An example of this is moving a counter. This consists of two moves, one updating the x field and the other updating the y field. It would be meaningless to undo the x and y coordinate independently of each other.

Moves on the stageStack are separated with a special ‘end’ move. The ‘end’ move separates stages.

The stagePointer points at the current stage, the stage that is visible on the screen.

An Undo will move the stagePointer back to the previous ‘end’. A Redo will move the stagePointer forward to the next ‘end’ (if there are any).

This is the basic idea behind the implementation of Undo and Redo.

In order to generate the game state at n - 1, it is necessary to update all the fields from
the base state up until stage n - 1. The implementation of this is very simple in Lua.

function State:executeUntil(stage)
    
    for i = 1, stage do
	    
	    local id = State.stageStack[i].id
	    local trait = State.stageStack[i].trait
	    local key = State.stageStack[i].key
	    local value = State.stageStack[i].value	
	    
	    
	    if trait == 'Counter' then
		    Counter[id][key] = value		 
	    end
	    
	    if trait == 'MarkMoved' then
		    Counter[id][trait][key] = value		 
	    end
	    
	    if trait == 'Image' then
		    Counter[id][trait][key] = value 
	    end
	    
	    if trait == 'Delete' then			
		    Counter[id] = nil	
	    end
	    
    end
    
end

Loop through all moves from index one the the desired stage and update the fields in the traits. When this is done it’s just a matter of generating the GUI (the visible representation of the state).

function State:undo()
    
    local i = State:findLastStage ()
    
    State:reset()			-- reset Counter with saved State	
    
    State:executeUntil(i)	-- execute moves up to last stage
    
    
    Counter:generateGUI()
    
end

Next up is finding and making a new library that will replace Gtk3.

Then it’s time to deal with stacking. First drag and drop must be implemented with the new library.

Then it must be possible to drag copies of counters from the counter window to the map window.

A grid must be set up and counters must be able to be stacked with an given offset.

Then it must be possible to open a stack and do two things: select some units (SHIFT key)
and reorder units.

I will follow the usual conventions. Hoovering over the stack will display a popup window with its contents. Double clicking a stack will open a window where selecting and reordering will be possible.

I do not think it’s a good idea to open several stacks at the same time. That clutters the board. But exactly how this is done depends on practical considerations. It is important to avoid clutter but also important to make it as user friendly as possible.


Regarding the new library, I will look closely at what I need. It must be possible to place a GUI element at x, y on the screen. It must be possible to change the z-order of GUI elements if they happen to lie on top of each other.

It is a plus (but maybe not that important) that all functions in the library that can read/write the host file system be removed. Reading/writing files is done only with C++.

I think it is good design practice to make the module as self-contained as possible. By this I mean that it should have all the needed libraries supplied. It should not be necessary for the user to install any extra software.

This means distributing the Luau VM (Virtual Machine) and the GUI library with game engine.

Of course, since many libraries are dynamic libraries, “self-contained” is a relative notion.
But the rule still holds, a user should not have to install any software to run the module.
The user just downloads the module (which is a combination of the implementation of a game, the game engine and the GUI library) and can run it straight away.

The downside of this is that the size of a module is greater than the size of a typical VASSAL module. Let us look at what the size may be.

The size of the Gtk3 library (libgtk-3.so.0.2404.29) my Linux distribution is just 8,5 MB. This is a dynamic library. The actual needed size is larger but I guess not by much. Among other libraries GTK3 needs gdk-pixbuf (libpixman-1.so ?) which is just 694,4 kB and Cairo (libcairo.so.2.11600.0) which is just 1,2 MB. On a Linux system these libraries are already
installed and need not be redistributed.

The size of the Luau executable is 10,5 MB.

When I link I use these static libraries:

libluauvm.a 2,3 MB
libluaucompiler.a 7,7 MB
libisocline.a 1,0 MB
libluauast.a 4,9 MB

The total Luau size is then about 26 MB.

Total size of Luau and libraries is then roughly 26 MB + 10 MB or 36 MB.

The compiled size of the game engine is right now 6,7 MB but will of course increase. Finally there are the image resources. My WW1 VASSAL module has images resources of 6,7 MB. Large VASSAL modules may have image resources of 20 MB. It varies greatly.

A typically self-contained module will then be around 50 MB which is of course
much compared with a typical VASSAL module.

The downside of self-contained modules is that a lot of redundant software is distributed.
But with today’s bandwidth and storage capacity this should not really be a problem. 40
downloaded modules is still just 2 GB, which is nothing on disks with a 500 GB or 1000 GB
capacity.

The upside of such a distributions is that the module never becomes obsolete. As long as
an opponent has the same distribution the module will always work. There is nothing outside the module that will make it stop working.

The C++ part (and GUI library) of the engine can be changed independently of the Luau part as long as the Luau C API remains the same. This is useful for the inevitable correction of bugs and new features. Only if changes imply a change of script must the user download a new version of the module. But the old module will still run with its version of binary, script and library.

A distribution of this game engine is an independently compiled C++ binary with a GUI library together with a VASSAL-like module for any particular game. The module will contain the image resources and the Luau script files for a particular game. The Luau script files are based on a template (the Luau part of the engine). They are then modified/developed to implement a particular game.

Regarding integrity, the script files can be verified before they are compiled with a checksum. The sum can be compared with a server checksum. The C++ part will have a build version for checking.


When stacking is done it’s time to piece it all together. The pilot tests I have made will be merged into a very rough pilot game engine. This in itself will demand more development.

After the game engine is in place, a very interesting phase starts. I will try to implement my WW1 Verdun VASSAL module on the new engine. How will it go? How will I be able to implement those artillery fire tables? It will be a good test on how the engine copes. It will bring into focus what more development is needed.

One very important issue is map scaling. The map and counters must be scaled independently and placed relative to each other.

It was not difficult to chose a new GUI library. But first a few words about wxWidgets.

I managed to download, compile, setup and run their drag-and-drop example in one afternoon. Good. Then I made investigations into the existence of functions for z-order.
To my disappointment there were none. Here it says plainly:

wxWidgets does not support overlapping siblings.

The choice was then simple. It was of course Qt.


On the GitHub wiki 7. GUI page I have described in detail what I did to install and run Qt. It did not go as smoothly as with wxWidgets. Among other things there was no default generation of the so-called xcb-plugin even if this plugin is vital for running Qt on Linux. You have to explicitly give options in the configure-step.

Don’t forget to use a virtual machine when you install Qt. You need to install a number of additional developer (-dev) libraries.

I have reimplemented the code in 6.State with Qt. That is to say, the functionality is
the same but 6.State used Gtk3 while 7.GUI uses Qt. The difference is interesting to study. On the whole I think that the Qt code is better but a few things were in my opinion done better in Gtk. One example is signal connecting:

In Gtk3 :

g_signal_connect (menuitem, “activate”, G_CALLBACK (do_activate), (gpointer)e.entryaction);

In Qt :

QObject::connect( action, &QAction::triggered, this, [=]()->void{ do_activate(action); } );

Come on, guys. What on earth is [=]()->void{ do_activate(action) ??? It is a “lambda expression”. Read more here. Parameter passing in signals is done a lot easier in Gtk3. You just give a gpointer to whatever you like and it will be accessible in the handler.


Qt documentation is excellent. The Qt knowledge base is ok. It’s what you expect from a professional company. The down-side is they are profit-driven. Profit-driven companies are of course needed but maybe not for an application like this. Their software is mainly open-source and distributable but you never know what kind of restrictions they may put.

The size of the dynamic Qt libraries are modest:

libQt6Core.so.6.5.2         6,9 MB
libQt6Gui.so.6.5.2          9,7 MB
libQt6Widgets.so.6.5.2      7,1 MB    

To my amusement I discovered that there already is a Qt5 library on my Linux distribution. So they do take free distribution seriously. Still, it would be a good idea to contact Qt and describe in detail what distribution of their software this application has and get an explicit ok. Maybe we have to state visibly that the engine is powered with Qt?


A great plus with Qt is that all the major platforms are support: Linux, macOS, Windows, Andriod and iOS.

Read more about their supported platforms here.

That means that little or no modification is necessary to generate the engine on any platform. But of course, it is necessary to compile and maintain five different executables. As long as the platform has a C++ compiler this should not be a problem. It may also be possible to cross-compile from a single platform (Linux) to make maintaining five executables easier.

Linux has something called Wine that may be used to run Windows-executables. MacOS (and maybe Android) has something similar.


The flipping, rotation, moving and deleting of counters should now work. There has been no extensive testing. The Undo and Redo functionality works well and has a simple implementation. Note that the menu bar can be customized to look better.

Regarding rendering, I don’t see a difference in quality between Gtk3 and Qt. Compare the excecutables of 6.State and 7.GUI.


Next is dragging of counter-copies from the counter window to the map window and stacking of counters, the ability to open a stack and reorder a stack. Then comes selection of counters and the ability to perform the same task on multiple counters (like moving, rotating, etc).

There are two ways to organize an application: single-windowed or multi-windowed. In a single-windowed application sub-windows are organized in a grid. In a multi-windowed application there are separate windows for every major function like map window, repository window, chat window and server window.

For the time being I go for a multi-windowed application. Most players want to see as much of the map as possible. A single-windowed version can be made later.

Technically it is not more difficult to make a multi-windowed application than a single-windowed one. You can easily drag counters in Qt between windows. No extra code is needed. You just need to pay attention to the source of a drag-operation.


The main window is the map window. The first separate window I make is what I call the repository window. This is where every counter/marker in the game is stored.

I refer to the new section on the GitHub wiki for more information and images.

Later the repository window will have tabs, subtabs and subsubtabs.


I then started dealing with stacks of counters.

First of all: stacking is just a rendering issue. That means that the Luau-part need not know about stacking. It only knows that certain counters have the same x,y coordinate.

The fundamental design idea is this: the data structure needed for stacks is generated every time it is needed from the position of the counters. “Every time it is needed” means every time the map is repainted.

This may sound a bit strange. But it is not, because the time needed to do this generation is small on modern CPUs, especially when considering that the maximum number of counters is in the magnitude of a thousand.

This design idea means that the data structure holding stacks need not be updated in complex way every time a counter or a stack of counters move. It saves code and it lessens the risk for bugs.


I have made two types of stacks: with or without offset. The user will later be able to chose what type he wants. Stacks without offset have a white number on a black background in the lower corner showing stack size.


As usual the code has not been tested much. The quality of the code is not tip-top. In particular I use externs which I don’t like. You can avoid them but for the time being they are useful.

One more issue: a rotated counter needs a mask so only the visible part of the counter triggers the popup menu (like it is in VASSAL).

Next up is showing stack contents when hoovering, opening and closing stacks, stack reordering, selection of counters and the ability to move a selected stack.

I am busy now but I hope to post again in late November.

Glad to post here again. I have now worked with stack inspection. See my wiki.

Stack inspection is about showing the contents of a stack when the cursor hoovers over the stack. I will go through the function that sets up the hoover view, the popup window that shows the contents.

void Counter::QtCounter::hooverAction()
{	
    
    CentralFrame::Stack stack = {{this->owner->zorder, this->owner}};
    
    QPoint point = QPoint(this->owner->state.x, this->owner->state.y);
    
    
    for ( auto obj = counters.begin(); obj != counters.end(); ++obj  )	
    {
	    QPoint p = QPoint(obj->second->state.x, obj->second->state.y);
	    
	    if (p == point)		
		    stack[obj->second->zorder] = obj->second;	
    }
    
    
    
    
    int cellWidth = stack.begin()->second->width;
    int cellHeight = stack.begin()->second->height;

    
    string filename = "./images/Map.jpg";
    
    QImage baseImage = QImage(QString::fromStdString(filename));
    
    int centerX = this->owner->state.x + (int)(cellWidth / 2);
    int centerY = this->owner->state.y + (int)(cellHeight / 2);

    int imageSize = cellHeight + 2*StackFrame::border;
    
    QImage background = baseImage.copy(centerX - (int)(imageSize / 2), centerY - (int)(imageSize / 2), 
								       imageSize, imageSize);

    Counter::hooverView->setImage(background);
    
    

    
    int box = imageSize + 2*StackFrame::border;
    
    Counter::hooverView->grid->move(box, 2);	
    
    
    
    popupPoint += this->pos();
	    
    Counter::hooverView->setPostion(this->owner->parentFrame->mapToParent(popupPoint));
								    
    
    Counter::hooverView->setImages(box, stack);
    Counter::hooverView->setVisible(true);
    
}

First of all the showed stack has to be generated. Remember that there are no data structures holding stacks. Loop through all the counters and find the counters that have the same x,y.

Second, generate the section of the map below the stack. This is just a square of the game map. Notice that the file for the map is loaded. Later all resources will only be loaded once and they will be referenced with an id.

It is best to use layouts. hooverView->grid is the parent of the layout(s) holding the images of the counters in the stack.

A position for the popup window has to be determined based on the current position of the cursor (this->pos()).

Finally the actual setting of images is done (see setImages in class StackFrame) and the hoover view is made visible.


Having now worked more with Qt I am impressed with its functionality. I will give two examples of what I mean.

When you make a widget this is a counter on the map, you will need to have rotate functionality. A rotated counter is still a square image but with areas of transparency. Obviously you don’t want the transparent areas to trigger events. The solution is very elegant, a single line of code (in counter.cpp):

this->counter->setMask(QBitmap::fromImage(image.createAlphaMask())); 

You set a mask. Not only does the mask prevent handling of events in the transparent areas, it also passes the event to the widget (if any) below the masked widget!

Make a stack of counters. Rotate them in turn. When you rotate the top counter the corners of the counter below become visible. Put the cursor over this corner and right click rotate again. Continue this until all counters in the stack are rotated.

The second example has to do with the computation of coordinates (frame.cpp).

QRect source(0, 
			 0, 
			 obj->second->baseBuffer.width(), 
			 obj->second->baseBuffer.height());
				
QRect dest(x,
		   y,
		   obj->second->width,					   
		   obj->second->height);
		   
		   
			
			  
source.moveCenter( dest.center() );

obj->second->counter->move(source.topLeft());

You want to compute the top left coordinate of a rotated counter where the centre of the counter is given. Instead of making your own hack you use the functionality of QRect. You move the source (the QRect of the counter) to the centre of the QRect where you want it placed. Then you just call topLeft() of the moved QRect!


Next up is to open a stack and rearrange the order of the counters. I will do this in a way different from how VASSAL does it. It will suite cases of stacks with offset and without offset.

The VASSAL way of opening stacks will also be implemented but after selection of counters is implemented. There is no “correct” way of opening a stack. It is better to let the user have a choice.

Implementing stack reorder led as usual to a number of issues.

The way I implemented stack reorder is described on my wiki.

This way is different from how it’s done in VASSAL. I will also implement the VASSAL way later. A sketch of how that may be done is this:

  • Increase the offset of the counters in a stack.
  • Implement drag-and-drop so that a counter (or selection of counters) can be put on top of any counter in a stack.

In my own implementation of stack reorder I came across a problem regarding layouts.

First of all, layouts are a great way to automatically order elements. In Qt you have the
QHBoxLayout that lines up widgets horizontally.

I realized that no ready-made layout matched my needs. More specifically I needed something like the QHBoxLayout that was able to span several lines. The need for this arises from the fact it is impractical (or impossible depending on window size) to have more than a certain number of counters on a single line. In VASSAL the limit is 9 (or so I think).

I had to make my own layout, a class called FlowLayout. I got the idea from here.

You need to implement the abstract base class QLayout. See the code in overlay.cpp. I will not go into the details but I got it to work. The result is fine.

A function in the FlowLayout class also updates the stack after a reorder.

void Overlay::FlowLayout::updateStackOrder()
{
    
    foreach (QLayoutItem *item, itemList)
    {
	    Overlay::StackOpen::Item *listItem = (Overlay::StackOpen::Item *)((QWidgetItem *)item)->widget();
	    Counter *counter = listItem->source;

	    counter->zorder = Counter::topZorder();
	    counter->counter->raise();
    }
    
}

Note that since itemList is ordered, the only thing you need to do is assign a new zorder value to the counter and raise it. The zorder value in itself has no meaning, only its relative numeric value. The zorder value is the application’s way a deciding Z-order. Qt itself does not operate with a value. Qt uses a default way to decide Z-order (position in list ?). Z-order is not a property in Qt. Z-order in Qt can only be changed with raise(), lower() and stackUnder() – under certain conditions.


Next up is selection. Individual counters and a group of individual counters must be selected. A stack of counters must be selected.

In addition to the selection itself, actions must be performed on selected counters. Foremost is the ability to move a selected stack so that all the counters in the stack move as one.

Selection and movement of stacks have been implemented, see the wiki.

Just as with stacks there is no data structure holding selection. Instead each counter has a field that tells if it is selected or not. Each time you need the selected counters you loop through all the counters.

You can move a counter or stack of counters without selecting them. This is unlike how VASSAL does it where the stack you are clicking is always selected. In my opinion it is important to not have automatic selection. Selection should occur on a need-to-basis.

Below is the code in the mousePressEvent that controls selection in frame.cpp.

auto result = drag->exec(Qt::MoveAction);
	
	
if (result == Qt::IgnoreAction) 
{
	
	// select counter(s)
	
	// add to selection if SHIFT button (right or left) is pressed
	if (QApplication::keyboardModifiers() == Qt::ShiftModifier)
		stackSelect(child->owner);
	else
	{
		if (!anySelected(child->owner))
		{
			unselect();
			stackSelect(child->owner);
		}
	}	
} 

If no drop occurs the call to drag->exec returns IgnoreAction. You can add to the selection if you hold down a SHIFT key (right or left shift as in VASSAL). If not, you delete all selections and then select the clicked stack, unless the stack already has selected counters (selection can always be removed by clicking on the map).

The point about no selection if any counter(s) are already selected is more important that it seems. In Qt like in any other GUI library (?) double-click is implemented by a single-click twice within a time frame set by a timer. That means that when you double-click you also single-click (there are ways of working around this but I don’t want to make a hack).

There is a handler for double-clicks but you have to make sure that the hanlder for single-clicks don’t interfere with what double-click handler does.

The double-click handler opens the stack. In my implementation this means creating a box where you both can both reorder counters and select/unselect them. The single-click handler must not destroy whatever setting you have made of selected counters in the double-click handler.

Some argue that applications should avoid the double-click, see Stackexchange. It could be worth while making an alternative to double-click, for instance by holding down a key when clicking.


Clicking on a stack selects the whole stack. Dragging a stack drags the whole stack. This is the default behavior: to consider the stack as a whole. If you want to break up the stack you need to select what counters you want to move by opening the stack. Once counters are selected, only the selected counters are moved.

If you want to move several stacks at once, you need to select them. Only then do they move as one. It is not more difficult to implement multi-stack moving than single-stack moving. But in my experience you rarely need to move many stacks at once.


Reordering of counters must be undone. There is no problem undoing movement of whole stacks. The code for this is already in place. In my opinion there is no need to undo selections. You remove all selections by clicking on the map (as in VASSAL). It is reordering that needs new Undo/Redo code.

I had not looked at the Luau part for half a year. When I looked at it again it seemed totally incomprehensible … lol. But of course, after an hour I got into it again. The code is understandable if you are willing to spend the time necessary. That actually goes for all code.

Let us look in detail what changes were necessary for undoing reorder. It will illustrate the relationship between the Luau part and the C++ part.

  1. The first time a counter appears on the map is when you drag it from the repository.
    The creation starts in the drop handler in frame.cpp on line 255. Since it is the
    script that creates counters, you must call a function in the script. That is what
    Luau::copyCounter does.

  2. Since we need to include the zorder, a variable called zorder is given a new value
    (Counter::topZorder) and is included in the parameters that create (copies) a new
    counter (Luau::copyCounter). The numeric value of zorder itself has no meaning. It
    is only the relative numeric value that has meaning. Counter::topZorder just returns
    the next higher integer. You may say that the new counter lies on top of all the other
    counters.

  3. Luau::copyCounter is a call to the luau function copyCounter in module.luau line 178.
    zorder is then included in the parameter list in the call to Counter:new in counter.luau.

  4. In Counter:new we add a new field called o.zorder (line 61). You may think of zorder
    as a new trait. But since zorder is just a number (not a “class” with functions that
    manipulate the number) it can just be added as a field in Counter (like the fields
    x and y which are just coordinate numbers).

  5. Counter:new calls a C function that does the actual creation of the counter (line 62).
    In luau.cpp we see on line 164 the C function that is called. On line 175 we see
    the call to the Counter constructor for counters on the map.

  6. On line 371 of counter.cpp we see a new parameter for zorder. Instead of assigning
    a new zorder value (commented out on line 398) we assign the value of the parameter
    (line 399). Note that in the constructor for counters in the Repository (line 324)
    zorder has no meaning, so no assignment of a zorder.

  7. We have added a new field called zorder in the Luau part (counter.luau). We can say
    that Luau now “knows” the zorder. The next step is to add this new field to the state.
    State (state.luau) is the mechanism that implements the Undo/Redo functionality.

  8. On line 257 in frame.cpp a call to Luau::doCreate is done. This call is what adds
    add (among other things) zorder to state. On line 416 in luau.cpp zorder is pushed
    on the stack and the luau function create is called on line 149 in module.luau.

  9. Create in turn calls State:addCreate.

  10. zorder is added to the table holding the state’s stageStack at the latest stagePointer
    (line 25 in state.luau).

  11. Evert time we undo we need to update all counters with its zorder, just like its other
    fields (x, y, etc).

  12. We are not done yet because zorder can change in other ways too, not just by creating
    a counter on the map. There are two ways zorder can change, either when we move a
    counter (it is always placed on top of all other counters) or when we reorder a stack.

  13. Let us first look at move. We see in frame.cpp lines 213 that when we move
    a counter we also give it a new zorder number. The fields x, y and zorder change
    simultaneously. Therefore we add a new trait/value combination for the zorder
    (line 219). It is not necessary to use a new type of event, just the same “move” event.
    A move is both a change in x,y and a change in zorder. We need not do anything else with
    the code.

  14. The second way zorder can change is after a reorder. This is also a move, only
    that x, y remains the same.

  15. The place where only zorder changes is swapWidget on line 432 in overlay.cpp. Here
    you need to add a new stage on the state stack (lines 446-448).

  16. The new undo/redo value must be added to State:executeUntil (line 129 in state.luau)
    and State:reset (line 100).

  17. The update of state needs the extra paramter Counter[k].zorder in counter.luau (line 141).

  18. In luau.cpp you need to change the C API to include zorder (line 264 and 276). By the way,
    this reminds me that zorder really belongs in the struct State as state.zorder. Zorder will
    determine the state of a counter and will be needed in the save and load of a game.

  19. Final fun point. In the paintEvent handler of CentralFrame you need to raise the counter
    explicitly (line 623 in frame.cpp). This was earlier a default behavior because zorder
    did not change by itself.


Stack selection and stack movement marks the end of basic development.

The amount of code is not huge. A run down of the major files:

counter.cpp - 682 lines of code
frame.cpp - 631 lines of code
overlay.cpp - 637 lines of code
luau.cpp - 513 lines of code

A huge amount of functionality has been implemented with not too much code. This is because I use a modern GUI (Qt6.5) and a modern C++ (#define __cplusplus 201703L or C++17).

The down side of using C++ can be summed up in one word: pointers. It is the need to manage your own garbage collection. But there are tools that can check for memory leaks.


Issues:

  • It is also possible to select counters by pulling out a rectangle on the map. This has
    not been implemented and can be done later.

  • No right-click menu actions have been implemented that do the same operation to the whole
    stack. This is not difficult to implement but care has to be taken so that a given action can actually be applied on all the counters (you must be careful with Delete).

  • I have not dealt with cards at all. Cards are similar to counters, but a deck of cards is
    not similar to a stack of counters. Cards will not be used in the raw prototype and can
    be implemented later.

  • Undo/Redo has a blinking effect. That is because all the counters are not hidden during
    update. This is not difficult to mend. But maybe some kind of visual feedback is ok?

  • There are very many counter loops. That is because there are no data structure holding
    stacks and selections. Time will tell if this is ok when a real game is implemented. I am sure
    all these loops could have been cut down, but that would have been at the expense of simplicity.
    The more optimizations you make, the greater the risk for bugs. It also makes the code more incomprehensible.

  • Little testing has been done. The base functionality should work but I don’t doubt
    it’s possible to generate errors and make core dumps. There is little error handling.


Now starts the really interesting part, to create a complete prototype by implementing a real game. Implementing a game will be a fine opportunity to understand what more is needed in the engine.

First out are resources. All images in the images folder will be read into memory only once and assigned an id. This id must be made available to the module developer. He must see what id corresponds to what file in the images folder. The most intuitive id is simply the file name and its path in some form.

Then comes the loading of a real map. The first thing that must be done is to scale the map. Scaling of counters must follow the scaling of the map. This is not trivial.

This is the time to implement rotation of the game map. The only useful rotations must be -90 degrees, +90 degrees and +180 degrees. The counters are not rotated. I don’t think this will be difficult to implement.

I will use the WWI VASSAL module I made (see Verdun: A Generation Lost) as the prototype game.

A few words about it. In 2018 my regular opponent and I decided to play two WWI games in connection with the 100’th anniversary of the ceasefire. The games we chose were Verdun:A Generation Lost and Paths of Glory. You can imagine what game we played the most. You have to be very, very interested in the Western Front of WWI to sit through this game. But even if the game itself is not very interesting it has all elements of an advanced VASSAL module, and implementing it on this engine will be very interesting.

1 Like

All resources (images) the module needs must be read from disk a single time at start.

The class IO does this, see io.cpp.

The IO class is important because it is the only class that can access the host file system. The Luau script should never be able to access the host file system. No other classes in the C++ part should access the host file system.

The IO class defines how access happens. To have all the access gathered in one place is obviously done for security reasons.

There are two types of resources: those given by the module and made by the module developer (like the map and counters) and internal resources use by the engine itself (like the image for the Moved-marker).

See the wiki for details on how resources are identified.


The whole map from the module Verdun: A Generation Lost is now loaded up in the map window, see the wiki.

Scroll bars are in place, but I have also implemented a feature that I have missed in VASSAL. It is the ability to drag the map in any direction you want by holding down the left mouse button.

See an example of this feature on the wiki.

In VASSAL the left mouse-button-drag is used to drag out a selection rectangle. This will later be implemented with SHIFT left mouse-button-drag, i.e. holding down the SHIFT key and holding down the left mouse button.


Scale and rotation of map with counters have been implemented. Note that the counters themselves are not rotated. Again I refer to the wiki for images.

I have tested the cases ratio = 2.0/3.0 and rotation = 90 , -90 and 180.

I have also seen that rotation = 45 and 135 give correct results. Actually you can scale and rotate with
any value of ratio and rotation, but I can not imagine that rotations other than 0, 90 , -90 and 180 have any meaning. In the GUI it will likely be necessary to limit the rotation to these values.

A counter has been placed on the map at start to see the effect scaling has. Note that undo/redo functionality does not work with counters placed at start (they will disappear). This will be fixed later.

To see the effect of scaling and rotation you need to change the values in scale.h and recompile.


I decided to drop the double-click. Instead a stack is opened with CTRL + left button click. This has the benefit of not selecting the whole stack each time you open it.

You open a stack, then left click on the counters you want to select. Only selected counters in a stack are moved. If no counters are selected, then the whole stack moves (the default operation).

Double-click may very well be an option for those who prefer it. But then one must make sure that any selection made by a single click is invalided when a double click occurs.

As of now, a single click will select all the counters in a stack (the default behavior).


I had to make changes to the traits in traits.luau

Masks have been defined.

Masks = {}

Masks["MovedMarker"] = {
    mask = "__moved", 
    x = 72, 
    y = 15
}


create_mask("MovedMarker", "__moved", 72, 15)

The point with this is that masks are independent of counters. Any number of counters may use the same mask. Any given mask may then only exist as one instance.

The ‘create_mask(“MovedMarker”, “__moved”, 72, 15)’ makes sure that the C++ part knows the mask
with its image and offset.

Obviously, a mask may have the same image but a different offset.

The MarkMoved trait now looks like this with the ‘mask’ field set to the key in ‘Masks’.

MarkMoved = {}
    
function MarkMoved:new ()
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.mask = "MovedMarker"
    o.moved = false 	
    o.menuname = "Moved"
    o.menuclick = "actionMoved"
    o.movetrigger = function(id) Counter:markMoved(id) end	
    return o
end 

When developing in C++ you always have to be mindful of memory leaks.

A good example of subtle memory leaks can be found on line 50 of frame.cpp.

QDrag *drag = new QDrag(this);

The memory of ‘drag’ is never released. I found this function (added on line 51):

drag->deleteLater();

According to the documentation at the Qt website:

The object will be deleted when control returns to the event loop. If the event loop is not running when this function is called (e.g. deleteLater() is called on an object before QCoreApplication::exec()), the object will be deleted once the event loop is started. If deleteLater() is called after the main event loop has stopped, the object will not be deleted. If deleteLater() is called on an object that lives in a thread with no running event loop, the object will be destroyed when the thread finishes.

As I understand it, ‘drag’ will be deleted and memory released once control returns to the idle loop.


A bit more about scripting.

Scripting helps to move code out of a compiled binary and gives the module developer a possibility to change/add to it. An example is the UNDO/REDO functionality. The move stack can be extended to make a move tree for developing a game opponent.

But there as more benefits from scripting.

It is now possible to create a counter on the map with script. The Luau function can look like this:

Counter["1000"] = Counter:new("1000", {"12R-51R", "12R-51R_f"} , 1, 0, -1, 100, 100)

But it is just as easy to create a Luau function that can move a counter on the map from A to B. Luau functions can do anything a user can do such as flip, rotated, mark as moved, stack and delete a counter. What use does this have?

  • Imagine you have made a change to the C++ part and want to check if everything still works. You can then run a script that simulates a game and see if any errors happen. This is what is often called a sanity-test.

  • Imagine you want to stress-test the engine. Let the script create 1000 counters and move/flip/rotate all. It is possible to measure the time this takes. Are these times acceptable? Or must the game engine be made more efficient? One particularly useful issue is to measure the time it takes to select a given counter with 1000 counters on the map. How many counters can there be before the engine bogs down?

  • There are certain actions in a game that may consist of many separate moves. By letting the user execute a script that does these actions, it makes the gaming experience easier.

  • Being able to let the script move counters is important when making a computer opponent.


I have to remind everyone that the code I make is just test/prototype code. In a design phase it is more important to keep the fundamental goals in mind than getting bogged down in detail. I will give an example of what I mean:

In io.cpp there is a getter for scaled size:

QSize IO::getScaledSize(std::string str)
{
    return _scaledResources[str].size;
}

Now, what if _scaledResources[str] does not exist?

Then the call to _scaledResources[str].size will produce a core dump.

I don’t have to check whether _scaledResources[str] exists because I know that I only call IO::getScaledSize with a valid str. I know this because I am the only developer. But when the code gets larger, you can’t really presume that anyone “knows” anything. Therefore the code should look more like this:

QSize IO::getScaledSize(std::string str)
{
    if (str.empty())
        return 0;
    
    if (_scaledResources[str] == nullptr)
        return 0;

    return _scaledResources[str].size;
}

In addition, it’s useful to have some kind of logging of unusual calls. I am very aware of all this, only now it’s design, not production.


Updated versions of Luau and the Qt library are used.

When I compiled with the new 6.6.2 I only got this warning:

overlay.cpp: In member function ‘void Overlay::FlowLayout::doLayout(const QRect&)’:
overlay.cpp:93:42: warning: ‘constexpr typename std::add_const<_Tp>::type& qAsConst(T&) [with T = QList<QLayoutItem*>; typename std::add_const<_Tp>::type = const QList<QLayoutItem*>]’ is deprecated: Use std::as_const() instead. [-Wdeprecated-declarations]
   93 |         for (QLayoutItem *item : qAsConst(itemList))

It is very professional to warn of soon-to-be-disused functions. Not only do they warn, they also suggest a replacement. By the way, I see 6.6.3 has already come out. They issue new versions all the time.


Next up is populating the repository window with the complete set of counters from Verdun: A Generation Lost.

This will about specifying GUI elements with script. I will make a new script file called repository.luau.

In general, the repository must have a set of submenus that can group counters together in such a way that they are accessible. Submenus can be tabbed pages, combo boxes or text list. They all have to be scrollable and they can basically come in any order.

It is now possible to generate the repository window with Luau script.

Because this feature is so central to the design, I will go though it in great detail.

On my GitHub wiki there are a few examples of repository windows and the Luau code that generated them.

IMPORTANT: I could not upload to GitHub all the counters of the module Verdun: A Generation Lost.

You have to download the module from VASSAL, extract the images folder and put this folder in the directory found here. Note that there is no images folder in this directory for this very reason. You must add the folder from the VASSAL module.


Issue: Anyone who has played a VASSAL module knows there is a window that holds the counters of the game. This window can be called the ‘Pieces window’ or ‘Counters window’ or sometimes the ‘Markers window’. The contents of this window is dynamic and depends on the module developer who made it. In VASSAL you go to ‘Edit module’. Under ‘Game Piece Palette’ you can define the tabs, scrolled lists, etc. to put the various game pieces.

Task: It is necessary to create this window using Luau script. The script is just a flat text file (repository.luau). The script is executed when the engine loads, just after the Luau VM (Virtual Machine) is running. The script must have all the commands necessary to specify and generate the window, including tabs, comboboxes and selection boxes (lists) and the counters themselves. Tabs, comboboxes and selection boxes may come in any order and are often nested inside each other.

How: Let us first look at the simplest repository.luau file.

-- set the content of the repository window

root(0)

    Repository["German/12R-51R"] = Repository:new({"German/12R-51R", "German/12R-51R_f"})
	Repository["German/38-94"] = Repository:new({"German/38-94", "German/38-94_f"})

This window just has two counters.

Every script file starts with a root(0) call. The C++ code for root(0) is

void Window::root(int level)
{	
    
    Pane *pane = new Pane(repositoryFrame);
    
    paneList.append(pane);
    
    
    repositoryFrame->layout()->addWidget(pane);
    
    pane->resize(repositoryFrame->size());
    
    
    lastLevel = level;
    
    
    panes.push(pane);
    
}

Note that the zero means level 0. In general, the repository window is a tree with a single root at level 0. The branches of the tree are where the counters sit. The tree is traversed (the script file is executed) depth first. That means that you first go out to the first branch, then back and up the next branch and so on.

The first thing that happens in root(int level) is the creation of a pane (which is basically a QLabel). This pane can hold counters or it can hold one of the three GUI elements: tabs, comboboxes or lists.

The pane is saved to a list of panes (ignore this for the time being).

The pane is then added to the layout of the parent.

The pane is resized to fit the window.

The current level is saved.

Finally, in order to traverse the tree you need to save the current parent for each level. This is done by pushing the pane onto a stack, panes.push(pane).

In the script, after a call to root(0) there follows the Luau script for creating two counters.

Note the indentation. In itself it does nothing but it helps readability a lot. The same level has the same indentation. Note that the commands for counters have no parameter for level. Therefore counters can be moved freely around in the script.


Now let us now look at an example with both tabs, comboboxes and lists. This is how the repository.luau script file looks like.

-- set the content of the repository window

root(0)

    tabs(1) 

	    tab(2, "A") 

		    tabs(3) 

			    tab(4, "German") 

				    combobox(5) 

					    comboitem(6, "German")

					    Repository["German/12R-51R"] = Repository:new({"German/12R-51R", "German/12R-51R_f"})
					    Repository["German/38-94"] = Repository:new({"German/38-94", "German/38-94_f"})
					    

					    comboitem(6, "French")


					    listbox(7)

						    listitem(8, "German")

							    listbox(9)

								    listitem(10, "12R")

								    Repository["German/12R-51R"] = Repository:new({"German/12R-51R", "German/12R-51R_f"})

								    listitem(10, "38")

								    Repository["German/38-94"] = Repository:new({"German/38-94", "German/38-94_f"})


						    listitem(8, "French")

						    Repository["French/VII-40"] = Repository:new({"French/VII-40", "French/VII-40_f"})


						    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})


			    tab(4, "French") 

			    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})
			    Repository["French/VII-40"] = Repository:new({"French/VII-40", "French/VII-40_f"})


			    tab(4, "Markers") 

	    tab(2, "B") 

	            Repository["French/VII-40"] = Repository:new({"French/VII-40", "French/VII-40_f"})

Note the indentation. Without it the script becomes difficult to understand.

We see levels 0 up to 10.

tabs, tab, combobox, comboitem, listbox and listitem are all C++ function like root was. I will go through the tabs/tab combination. tabs looks like this.

void Window::tabs(int level)
{
    
    QTabWidget *w;
    
    
    
    Pane **parent = (Pane **)std::any_cast<Pane*>(&panes.top());
    
    assert( parent != nullptr );
    
    
    
    (*parent)->setLayout(new QVBoxLayout((*parent)));	
    (*parent)->layout()->setContentsMargins(QMargins());
    (*parent)->layout()->setSpacing(0);	
    
    
    
    
    w = new QTabWidget(*parent);
    
    
    (*parent)->layout()->addWidget(w);	
    w->resize((*parent)->size());
    
    
    lastLevel = level;
    
    panes.push(w);
  
}

The previously pushed pane is now used. panes.top() is the top of the stack.

It must not be null.

The pane gets a layout.

The Qt TabWidget is created.

This widget is added to the pane layout and resized to fit the window.

The current level is saved.

The TabWidget is pushed on the stack (it is the parent of what follows).

The tab function looks like this.

void Window::tab(int level, std::string text)
{
    
    if (level == lastLevel)
	    panes.pop();
    
	    
    if (level < lastLevel) 			
	    for (int i = 0; i <= lastLevel - level; i++)
		    panes.pop();
    
    
    Pane **parent1 = std::any_cast<Pane*>(&panes.top());		
    QTabWidget **parent2 = std::any_cast<QTabWidget*>(&panes.top());
    
    assert( parent1 != nullptr || parent2 != nullptr );
    
    
    
    
    if (parent1 != nullptr)
    {
	    
    }
    
    else
    
    {
	    
	    Pane *pane = new Pane(*parent2); 
	    
	    paneList.append(pane); 
    
	    // IMPORTANT: you can not presume anything about what follows a tab;
	    // therefore you can not set its layout here
	    

	    (void)(*parent2)->addTab(pane, QString::fromStdString(text));
	    
       
	    pane->resize( ((QWidget*)(*parent2)->parent())->size() );
	    
	    panes.push(pane);
	    
    }
    
    
	    
    lastLevel = level;
    

}

Note the additional parameter text. It holds the text (name) of the tab.

If the level is equal to the last level (remember that the last level was saved) the stack is popped. This just means that elements of the same level need the same parent. What was on the stack was the parent of the content of the level. We don’t need that parent anymore.

If this level is less than the last level we pop the stack a number of times equal to the difference in levels. This means that we go back to the parent of this level. We go down the tree until we find the branch we continue with.

This parent can be a pane or a tabs. Remember that a pane is a kind of generic parent.

If the parent is a pane we just wait for the next script command (we do nothing and exit, saving the last level.

If the parent is a tabs we create a new pane and add a tab to the QTabWidget as this pane. This pane is the new parent we push on the stack.


combobox/comboitem and listbox/listitem follow a similar pattern, see the code.

There are a few syntax rules.

A tab must always follow a tabs. A comboitem must always follow a combobox. A listitem must always follow a listbox. The alternative is meaningless.

A tabs, a combobox or a listbox may very well follow a tab.

Note that there is no enforcement of syntax. A wrong syntax will crash the engine. This will be fixed later.

This brings me to issue of input verification. Very little, if any, input verification has been done in the game engine as a whole. This is because I want to focus on design.

Think about it. The Luau script files may be legal (they may compile) but there content can just be junk. Two points are important here:

  1. The script files must not allow the engine to crash.
  2. Error messages must be printed out so that the module developer can correct the script.

Point one is the most important but point two is important to make the engine usable.


This is what is needed of GUI in the repository window for Verdun: A generation lost. The actual script file is too large to be reproduced here. See the code for the actual file repository.luau. As noted before, you need to get the images directory from the module. It is not included and not present on GitHub.

-- set the content of the repository window

root(0)

    tabs(1) 		

	    tab(2, "German") 

			    listbox(3)

				    listitem(4, "German")

					    listbox(5)

						    listitem(6, "12R")

						        Repository["German/12R-51R"] = Repository:new({"German/12R-51R", "German/12R-51R_f"})

						    listitem(6, "38")

						        Repository["German/38-94"] = Repository:new({"German/38-94", "German/38-94_f"})


				    listitem(4, "French")

					    Repository["French/VII-40"] = Repository:new({"French/VII-40", "French/VII-40_f"})
					    Repository["French/VII-40"] = Repository:new({"French/VII-40", "French/VII-40_f"})   
					    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})
					    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})
					    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})
					    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})				    
					    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})


	    tab(2, "French") 

		    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})
		    Repository["French/VII-40"] = Repository:new({"French/VII-40", "French/VII-40_f"})


	    tab(2, "Markers") 

I have made the script file for Verdun: A generation lost. It is an exact replica of the counters wondow in VASSAL. This is very cool. It shows that the engine can be used for real games :slight_smile: .

It was a bit tedious to make this script file (a lot of copy-paste-checking). Since this file is just a text file, it could be possible to generate it with a smart program. That would also lessen the risk for syntax errors.

When a module developer makes a module, he does what I did when making this script file. The most effective way to do this is when the engine is running and you can reload the script with CTRL+R. Reloading the script means you first have to delete all memory to avoid memory leaks. It can be quite huge chunk of memory.

Freeing all memory is complex. You need to traverse the tree and delete children’s children in a recursive manner. This has to be done later.


A few words about my experience with Lua, Qt and C++ development over the last year.

It did not take me long to see that Lua was a good design. It goes under “less is more”. Simple syntax. Simple data structures but the table is very versatile. It is fast and has a wonderful integration with the language it is written in (C). Lua is the perfect script interface to a C++ binary.

Downside: Lua compile errors and run-time errors are not so informative. Also, the game engine does not dump errors to the console but this can be fixed. Right now, if something “happens” with Lua you just have to guess what’s wrong … it has not been a problem for me, but for a module developer it will be a problem.

Qt is great. I have really come to appreciate all its classes. I can create a lot of functionality with little code. I like the Qt documentation. It is consistent and complete. It includes not only formal syntax but also information on how the functions are used and examples. The Qt knowledge base is not as large as Java’s but it is sufficient. I have always found a solution.

Downside: Like all large libraries, you must learn to use it the way it was designed. In particular you must learn what layouts are and how they are used (no kidding). I have struggled with the paint system and the event system. But again, I have never come across a problem I could not solve.

C++ or C++17 has been a joy to return to. There is a huge set of ready-made classes you can use. I appreciate std::map. Of course, C++ is not Java. In C++ when get an error, you get a core dump, not a nice write-out of the call stack and a message on the line the code crashed. Add to this I do not develop with a debugger. Yep. I just use a simple editor (Geany) and compile in the console. If I need to debug, I print values to console. I wouldn’t recommend working like this, but I can do it because it’s my own code and I can go forward in small increments. There are rarely memory overwrites in C++, more typical null pointers.

Downside: To be honest I would rather have developed in Java. But that is impossible. And Java has it’s own problems which I guess you guys know very well. In my design the Luau VM, the Qt library and the C++ binary are bundled together. Of course, no software is carved in stone. And there is a price to be paid in having to maintain a module on every platform it runs on.


Now it’s time to look more at traits. One trait especially, the Side trait, I look forward to develop. With the prototype loaded with a real set of counters, it is time to look at how traits and groups of traits are assigned to counters.

Then comes the generation of the standard button bar, with buttons for undo/redo, scaling and so on. Fun to come.

I will likely not post again until late august.

2 Likes

I looked at the code and realized most of the coding was already done regarding assignment of traits. But there is still enough to do regarding traits. The new code is found here (I have made a branch).


For all who have developed a VASSAL module a trait should be a well known concept.

If you edit a module in VASSAL, under Game Piece Prototype Definitions, you are able to define a prototype. A prototype is collections of traits that many counters may share. In a popup window you see all available traits on the left hand side. You are able to add individual traits to the prototype definition and change their order.

A trait is basically a functionality that the engine offers to counters. It may have a right-click menu entry (like Delete), a combination of a right-click menu entry and an “intrinsic” effect (like Mark When Moved) or only an “intrinsic” effect (like Report Action).

It is useful to think of prototypes as “classes”. Like classes in Java/C++ a prototype can be build from a common prototype that is inherited together with additional traits.


Following the class concept, I will use the trait concept in a broader sense. Generally a trait is a set of one or more fields (key/value pairs). Traits are always defined as Lua tables containing key/value pairs.

Example: The MarkMoved trait has a status field (key=moved, value=true/false), a field for the right-click menu entry (key=menuname, value=“Moved”) and a field for what action is triggered when right-clicking (key=menuclick, value=“actionMoved”).

MarkMoved = {}
    
function MarkMoved:new ()
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.mask = "MovedMarker"
    o.moved = false 	
    o.menuname = "Moved"
    o.menuclick = "actionMoved"
    o.movetrigger = function(id) Counter:markMoved(id) end	
    return o
end

There is also a mask field for telling what image to use for marking a counter as moved and a movetrigger field for the “intrinsic” effect of the trait, that is the function that is called when the engine itself sets the counter as moved after a drag/drop.

Note: in VASSAL a trait is a popup window with boxes where you fill in the values of the different keys, here the trait is just lines of Luau script.


Traits are build in a hierachy. You have a Base trait with fields that all counters have in common.
Then there is a Piece trait for counters that need to be marked as moved and rotated. Finally there is the Side trait that tells what nation a counter belongs to. An example of how traits can be build (in traits.luau):

Base = {}

function Base:new ()
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.Delete = Delete
    o.Side = ""
    return o
end


Piece = {}

function Piece:new ()
    local o = Base:new ()
    setmetatable(self, {__index = Base})
    self.__index = self 
    o.MarkMoved = MarkMoved:new()
    o.Rotate = Rotate:new()
    return o
end


German = {}

function German:new()
    local o = Piece:new ()
    setmetatable(self, {__index = Piece})
    self.__index = self
    o.Side = "German"
    return o   	
end  


French = {}

function French:new()
    local o = Piece:new ()
    setmetatable(self, {__index = Piece})
    self.__index = self
    o.Side = "French"
    return o   	
end 

Traits get assigned to counters like this (in repository.luau).

-- set the content of the repository window

root(0)

    tabs(1)
	    tab(2, "German")
	    
		    Repository["German/12R-51R"] = Repository:new({"German/12R-51R", "German/12R-51R_f"})
		    Repository["German/38-94"] = Repository:new({"German/38-94", "German/38-94_f"})
		    
	    tab(2, "French")
	    
		    Repository["French/BRIGADE-3"] = Repository:new({"French/BRIGADE-3", "French/BRIGADE-3_f"})	
		    Repository["French/VII-40"] = Repository:new({"French/VII-40", "French/VII-40_f"})
		    
	    tab(2, "Markers")
		    
		    Repository["Markers/FLAME"] = Repository:new({"Markers/FLAME"})
		    Repository["Markers/GAS"] = Repository:new({"Markers/GAS"})
    
    
    Repository:addTraits("German/12R-51R", German:new())
    Repository:addTraits("German/38-94", German:new())
    
    Repository:addTraits("French/BRIGADE-3", French:new())
    Repository:addTraits("French/VII-40", French:new())
    
    Repository:addTraits("Markers/FLAME", Base:new())
    Repository:addTraits("Markers/GAS", Base:new())
    
    
    
return Repository

The function addTraits adds additional traits (besides the Image trait and the x,y “trait”) to each counter. I have put addTraits in repository.luau but it can be put anywhere as long as the counters in the repository have first been defined.


Let us for the sake of argument say that there are two types of traits. The first type are those that I as the engine developer can fully implement. A good example of such a trait is the Delete trait. The functionality of the Delete trait is narrowly defined and need not be changed. It is simply the ability to remove a counter from the board.

Note: even if the Delete trait itself need not be changed, the consequences of deleting a counter can change, for instance if a whole stack of counters are meant to be deleted when a single counter is deleted.

Then we have the second type of trait. These are the traits that the engine developer can not (fully) develop. A good example of such a trait is the Side trait.

Note: there is no Side trait in VASSAL. Instead you use the Marker trait that has a name/value pair, where name can be “Side” and the value can be say “German”.

The Side trait is an example of a trait with a single field only.

The Side trait simply tells what side a counter belongs to. A counter may also not belong to any side, like markers.

The important point is this: the engine developer can not in any way foresee how this Side trait can be used. Therefore the engine developer can not develop this trait. It is up to the module developer to define and implement it. Therefore the Side trait is fundamentally different from the Delete trait.

Think carefully about this point because it is so important.

The obvious question now is how to give the module developer the necessary means to develop this Side trait. Remember that the module developer can not create his own C++ code. That would create a huge security risk.


I will now outline how this is done. I do not presume to have the full answer yet but it is an answer that follows the basic principles of the design.

Let me first give a concrete example.

One very common use of the Side trait is to prevent one side from moving the counters of the other side. This means preventing drag/drop operations on certain counters. Drag/drop functionality can only exist in the C++ part (only C++ implements Qt event handlers). So how is this done?

Just before a drag operation is about to begin we call a Luau function from C++ called beforeDrag with the counter id (name) as argument (code line 106 in frame.cpp).

if (!Luau::beforeDrag(child->owner->name.c_str()))
    return; 

The function beforeDrag looks like this (code line 191 in module.luau).

-- event hooks

function beforeDrag(id)

    local side
    
    if Counter[id] ~= nil then
	    side = Counter[id]["Side"]
    else
	    side = Repository[id]["Side"]
    end
    
    if side ~= "French" then
	    return true
    else
	    return false	
    end
    
end

The important point is this: no matter what the code is in beforeDrag, if it returns false the drag operation does not take place. The module developer is free to put whatever code he wishes in this function, but if the function returns false the drag operation is aborted. The effect is that the user can not move that counter.

I call these Luau functions for hooks or “event handlers”.

We can have a hook for before drag, during drag and after drag. In general there is no limit to how many such hooks there can be. They can be placed anywhere in the C++ code where something is about to happen, is happening or has happened. The hooks may not only return true or false but all simple data types (int, string). What is returned can be used to modify the execution of the C++ binary in many different ways.

This is the means that the module developer has in defining the functionality of traits!


In addition to “event handlers” we can also offer the Luau script a “GUI library”, calls to the C++ part that can draw text or images. I will come back to this later.


As mentioned I have implemented some traits in the C++ part. I defined what I called the state of a counter by this C++ struct.

struct State
{
	int x;					
	int y;
	bool moved;				// only true if trait		
	int degrees;			// only not zero if trait
	std::string image;		// name of current (flipped) image
	int zorder;				// position in a stack	
};

Actually this is not correct. The real state of a counter is defined in the Luau part. It is the Lua table associated with each counter. If the id of a counter is say 6, then the counter’s state is the table

Counter[6]

This table contains all the traits with all the fields with all the values that make up the state of the counter, including fields that the C++ part knows nothing about, like Side.

In order for the engine to work, it must have a C++ representation of this state. This is especially important when writing code that saves (and loads) a game.

The state of a counter in C++ must be dynamic. It can not be a fixed struct.

Some traits can not in any meaningful way be implemented in the Luau part. A good example is the Delete trait. A balance must be found between “dynamic” and “fixed” traits. I do not have all the answers yet.

First some definitions. A field is a key/value pair.

o.y = 0

Here the key is y and the value is zero.

A trait is a set of fields. The MarkMoved trait looks like this.

MarkMoved = {}
    
function MarkMoved:new ()
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.mask = "MovedMarker"
    o.moved = false 	
    o.menuname = "Moved"
    o.menuclick = "actionMoved"
    o.movetrigger = function(id) Counter:markMoved(id) end	
    return o
end

The value of a field can be a string (key=mask key=menuclick), a boolean (key=moved) or a function (key=movetrigger). A value can also be an integer (say 4) or a float (say 4.5).

A counter may have many traits. The state of a counter is the current value of all its fields in all its traits. In practice what is interesting about the state of a counter is the value of the fields that may have different values during a game.

The traits of a counter is assigned in the repository, for example the trait with resource-id “German/12R-51R”:

Repository["German/12R-51R"] = Repository:new({"German/12R-51R", "German/12R-51R_f"})
Repository:addTraits("German/12R-51R", German:new())

I have up to now regarded the state of a counter as something that exists fixed in the C++ part of the engine. This is of course wrong. Obviously the state is defined in the script since the module developer is free to define whatever traits and fields he wishes.

Therefore I introduced the notion of dynamic state. A dynamic state is not fixed but is transferred from the script to C++ when needed. You may recall that I defined the state of a counter in C++ as

struct State				
{
	int x;					
	int y;
	bool moved;						
	int degrees;			
	std::string image;		
	int zorder;		
};

This is a fixed state. It needs to be replaced by a dynamic state. An example was the Side trait consisting of one field.

o.Side = ""

There is no std::string side; in struct State. There can not be, because the module developer is free to define whatever trait with whatever field he needs.

The problem then is to pass a Lua table to C++. A Lua table must have a representation in C++. Note that it is not necessary to have a full representation of a Lua table, only a representation of:

  • a field’s key
  • a field’s value
  • the type of the key
  • the type of the value
  • a way of representing nested tables (tables within tables)

In the Image trait:

Image = {}
    
function Image:new (images, imageIndex)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    --- fields
    o.images = images
    o.imageIndex = imageIndex
    if #images > 1 then
	    o.menuname = "Flip"
	    o.menuclick = "actionFlip"
    end
    return o
end

o.images is a table within the Image table. The C++ representation must have a way of representing this nested table correctly.


In making this C++ representation I had to create the most complex data structure yet. But thanks to the great new C++ classes the coding was in the end quite straight-forward. I used a combination of std::map and std::variant with std::get.

The type definitions in counter.h is:

class Table;
	    
typedef std::variant<int, std::string> Leftside;
typedef std::variant<int, double, bool, std::string, Table> Rightside;

class Table : public std::map<Leftside, Rightside> {};

Think of class Table more as a typedef than a class (it needs no constructor or rather the constructor is the default std::map constructor).

When getting the table from Lua I first use lua_gettable (in luau.cpp).

static int create_class(lua_State *L)
{	
	
	(void)lua_gettable(L, -2);
	
	if (lua_istable(L, -1))
	{			
		Counter::Table state = getTable();

		(void)new Counter(&state);
	}	
	
	lua_pop(L, 2);
	
	return 0;
}

Then I pop the table off the stack and assign the table key (Leftside) to the value (Rightside):

Counter::Table getTable()
{
    
    Counter::Leftside left;
    Counter::Rightside right;
    Counter::Table table;
    
    
    lua_pushnil(L);
    
    while(lua_next(L, -2) != 0) 
    {

        .
        .
        .

        table[left] = right;
	    
	    
	    lua_pop(L, 1);
	    
    }
    
    return table;
    
}

I do not include values that are functions in the returned table because they do not change during the game.


Going to a dynamic state meant quite a few changes in the code. Note that I retain struct State
because it is a handy way of accessing those fields that are needed for rendering a counter. New code is found in the new branch prototype2.

struct State is the subset of fields that are needed to render a counter. The keys of these fields can not change. They need to be available and accessed in a known way by C++.


I hope I have not left the impression that the module developer need to code a lot of Lua. First off I will go through all the VASSAL traits and develop equivalent for this engine. Second, any coding in Lua will only be necessary when developing features not present in the already implemented traits. Simple modules will just need a replacement of the art work and they are done.

Next is implementing two central traits, Mask and Label.

Read first my GitHub wiki.

Simple masks and labels are now implemented. They can be rendered on counters in a sequence defined by the module developer.

The mask trait is equal to the Mask trait in VASSAL. The label trait is equal to the TextLabel trait in VASSAL. Many more fields can of course be added to masks and labels. I go forward step by step. A mask can also be a filled area with a given color.


I wanted to use the Side trait more. Inspired by the thread Want to hide text label from one side or other, I looked into how this could be done on this engine. You can overwrite the area you wish to hide with a mask. Since both masks and labels have an apply field, you can set this field (a boolean) depending on what side you are. Note that PlayerSide is for the time being just set in the script. Also note that there is no fixed way to hide a text label. I realized how different it is to work with this engine compared to VASSAL.

One thing lost with script are legal values for a field. There is no drop-down menu where you can select possible values for a given key. For example: when you specify a label you only have a few fonts (or font families) to chose from. I tested times, sans-serif and arial. You are free to write what you want but you may crash the engine.

This is actually a sub-topic of input verification. I will not use time on this now. Good documentation on legal values will help, but illegal values must be filtered away.


A few words about what an “empty” module is. Imagine you are a module developer who want to implement a given game. What do you start with? There will be a number of templates to choose from. I can think of templates for hex-grid games, non-grid games, card games and more. The module developer will choose the template that is closest to the game he develops. A template will be a full and working dummy game. For simple games it will only be necessary to exchange that art work in the dummy game with your own.


I will look more at traits. I will go through all the VASSAL traits, implement some of them (in a rudimentary way) and discuss the others. Right now I am not sure what traits need to be implemented. I am not familiar with all the traits. Some of the them may not be relevant for this engine.

This part will look at the traits in VASSAL. A few of them will be partly implemented.

Note: Remember that the goal of this prototype is to implement the module Verdun: A generation Lost. In this module only a handful of traits are needed.

Note: The approach to traits in this engine will be different from VASSAL. In VASSAL there are a fixed number of traits to choose from. In this engine the module developer should be free to make new traits, given the possibilities offered by the C API (as defined in luau.cpp under extern "C").

Note: It has been some years since I made my VASSAL module for Verdun: A generation Lost. Therefore I am a bit rusty on VASSAL traits. Some of them I don’t know. Be patient with that. The main issue below is to give some pointers on how traits are implemented on this engine.


I downloaded the latest version 3.7.14. My first impression was how well the documentation has become. Neat, tidy, with a lot of images and examples. I see other changes too. VASSAL seems more polished and better.

There are 45 traits starting from Action Button and going down to Trigger action. In discussing them remember what I said above, that it is not my ambition to implement them or understand all of them now.

Action Button - Buttons on counters. Buttons that can be activated under certain conditions. A simple way of activation a right-click menu item.

This made me address the ability to make GUI elements with script. GUI elements are limited to what the C API can offer. So I made two new API calls: create_button and the corresponding delete_button.

create_button and delete_button are not traits in themselves, but they can be used in traits.

The call in the script looks like this:

create_button("button 1", "text", "buttonHandler", 100, 100, 70, 30)
  • “button 1” - unique id

  • “text” - button text

  • “buttonHandler” - the name of the Luau function to execute when button clicked

  • 100, 100 - x, y on screen

  • 70, 30 - width, height of button

    delete_button("button 1")
    
  • “button 1” - unique id

Additional parameters may be added that define color, background, border, etc.

The function “buttonHandler” can look like this

function buttonHandler(id)	
    delete_button(id)
    create_button(id, id, "buttonHandler", 200+math.random(100), 100+math.random(100), 70, 30)	
end

In frame.cpp the C++ side of the C API call looks like this:

void CentralFrame::createButton(const char *id, const char *text, const char *handler, int x, int y, int w, int h)
{
    CentralFrame::Button *btn;
    
    btn = new CentralFrame::Button(text, buttonParent);
    
    QObject::connect(btn, &QPushButton::clicked, [=]() { Luau::handlers(id, handler); });
    
    
    btn->setVisible(true);
    
    btn->move(x, y);
    btn->resize(w, h);
    
    buttons[id] = btn;
    
}

On my GitHub wiki there is an image of what the buttons look like. Run the code, push the buttons and see what happens :slight_smile:

Area Of Effect - To draw filled polygons around counters to signify that the counter has effects on its surroundings.

We start with the definition of a new trait in traits.luau.

AreaOfEffect = {}

function AreaOfEffect:new (radius, color, opacity)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.radius = radius
    o.color = color
    o.opacity = opacity
    o.apply = true
    return o
end

o.radius is a number, in this case just a multiplier of the counter size. o.raduis can also be a table with vertices for a custom polygon. On a grid-map o.radius is the hex radius of the polygon. Readers may recall that I did make a grid-map by calculating the snap-to points, see here. It is possible to calculate the vertices of a polygon with a given hex grid radius with the same kind of calculation.

You add this trait to a counter in repository.luau like this:

Repository:addTrait("Markers/GAS", "AreaOfEffect", AreaOfEffect:new(0.5, "red", 0.2))

In the paintEvent handler for the map (frame.cpp) you paint area of effects below the counters.

for (auto const& [point, stack] : stacks)		
	for ( auto obj = stack.begin(); obj != stack.end(); ++obj  )
	{
		
		if ((obj->second->table).find("AreaOfEffect") != (obj->second->table).end())
		{
			
			Counter::Table trait = std::get<Counter::Table>(obj->second->table["AreaOfEffect"]);
			
			if (std::get<bool>(trait["apply"]))	
			{
				
				int x = obj->second->state.x + obj->second->margin;
				int y = obj->second->state.y + obj->second->margin;
			
				float radius = (float)std::get<double>(trait["radius"]);
				
				const QPoint points[4] = {
				QPoint(x - (int)(radius*obj->second->width), y - (int)(radius*obj->second->height)),
				QPoint(x - (int)(radius*obj->second->width), y + (int)((1 + radius)*obj->second->height)),
				QPoint(x + (int)((1 + radius)*obj->second->width), y + (int)((1 + radius)*obj->second->height)),
				QPoint(x + (int)((1 + radius)*obj->second->width), y - (int)(radius*obj->second->height))
				};

				
				string str = std::get<std::string>(trait["color"]);
				QColor color(QString::fromStdString(str));	
				painter.setBrush(color);

				float opacity = (float)std::get<double>(trait["opacity"]);	
				painter.setOpacity(opacity);
				
				painter.drawConvexPolygon(points, 4);

				// reset
				painter.setOpacity(1.0);
				
				// draw only once per stack
				break;
			}
		}
	}

The marker GAS ATTACK now will have drawn a red transparent area around it, one-and-a-half size the counter, when placed on map, see illustration on GitHub wiki.

Attachment - It seems that this trait is about reading/writing values for different game pieces at the same time (sorry if I am mistaken). This kind of functionality is implicit when you have script.

Counter[id][key]

You can set the value like this:

Counter[id][key] = new_value

You are of course free to put whatever condition you want on reading and writing. It is easy to loop
through all counters to find those that need updating under a certain condition.

Note: VASSAL defines Key Commands to name functionality. In a script this is just the name of functions.

Note: it is possible for both the user and the C++ part to trigger functions under certain conditions. I have already implemented one such C++ trigger when a counter has moved and needs the Moved marker. The user can also trigger this function by right-clicking.

Again sorry if I have not comprehended all of the functionality of the Attachment trait. Still I am pretty sure that a trait like this can be implemented (or is an inherent feature) of script.

Basic Name - As I see it, this trait is only necessary because the tool you build VASSAL modules with has as default Basic Piece. It can’t be added. I understand why you can’t add it, because a game piece without an image would be meaningless. Also, a game piece without a coordinate on the map would be meaningless. The root of every trait in this engine is defined like this (in counter.luau):

function Repository:new (images)
    local o = {}	
    setmetatable(o, self)
    Repository.__index = self
    o.Image = Image:new(images, 1)
    o.x = 0
    o.y = 0
    return o
end

Now, Image is a trait (see traits.luau). Likewise x,y (and any additional coordinate that define the position of the counter in 3D space, like level, floor, inside/outside and so on) can be looked upon as a trait, the Location trait. You don’t really need to define Image and Location in the root. They can be defined elsewhere, under the condition that the C++ part can find them. “Finding them” means that this call gives the x coordinate of the game piece (in counter.cpp):

 this->state.x = (int)std::get<double>((*state)["x"]);

‘state’ is the C++ representation of the Lua table associated with the game piece.

Border Outline - Something similar to this has already been implemented, but not as a trait and only for square counters. This is the border drawn when a counter is selected, see counter.cpp line 623. Non-square counters need their own implementation. But note: all counters are images that are always square, they only have transparent areas.

Calculated Property - This is a property that becomes superfluous in a script. You have the whole expressive power of the Lua language in addition to the functions of the C API. You have access the values, fields, traits and so on.

Can Pivot - This is like the Rotate trait but not around the center of the counter. I note rotation can be interactive, difficult to implement.

Can Rotate - This trait has been implemented. CCW (counter clock-wise) is easy to add, just rotate with a negative angle. In the right-click menu you now see both Rotate cw and Rotate ccw. Options like random rotation has not been implemented. The Rotate trait now has two menu entries with two different actions, see Traits.luau.

Clone - The ability to duplicate a piece with a right-click command is not difficult to implement. Again, let’s start with the trait definition.

Clone = {}

function Clone:new ()
    local o = {}
    setmetatable(o, self)
    self.__index = self	
    o.menuname = "Clone"
    o.menuclick = "actionClone"
    return o
end

actionClone is

local function actionClone (window, id)
    if window == 'Map' then
	    if Counter[id].Clone ~= nil then
		    toId = next_id()
		    zorder = top_zorder()
		    cloneCounter(id, toId, zorder)
	    end
    end
end

Cloning a counter means programmatically adding a copy of the counter. The cloned counter must
have a new id and zorder ( just the next higher integer; all new counters are always on top of all others). cloneCounter looks like this

function cloneCounter(fromId, toId, zorder)
    Counter[toId] = Counter:clone(fromId, zorder)
    create_class_copy(Counter, toId)
end

We set the table of the cloned counter to be the table of what we clone and add a new zorder.

function Counter:clone (id, zorder)	
    local o = Counter:duplicate(Counter[id])	
    setmetatable(o, Counter)
    Counter.__index = Counter
    o.zorder = zorder
    return o
end

Then we just call the usual create_class_copy with the new id (toId). We add this new Clone trait to a counter like this:

Repository:addTrait("German/12R-51R", "Clone", Clone:new())

If you want all counters to be able to be cloned, you just add o.Clone = Clone:new() to Base (for example). Note it was not necessary to do anything with the C++ part to implement this cloning.

Comment - Obviously not applicable to this engine.

Delete - Implemented.

Deselect - In my opinion it’s more useful to toggle the selection status (like Mark When Moved).

We start off as usual with the trait definition.

Select = {}

function Select:new ()
    local o = {}
    setmetatable(o, self)
    self.__index = self	
    o.menuname = "Select"
    o.menuclick = "actionSelect"
    return o
end

The action is:

local function actionSelect (window, id)
    if window == 'Map' then
	    if Counter[id].Select ~= nil then
		    Counter:select(id)
	    end
    end
end

function Counter:select (id)
    if Counter[id].Select ~= nil then	
	    toggle_select_status(id)
    end
end

The C API toggle_select_status is:

static int toggle_select_status(lua_State *L)
{		
	const char *id = lua_tostring(L, -1); 	
	lua_pop(L, 1);	
	Counter::toggleSelect(id);
	
	return 0;
}

void Counter::toggleSelect(const char *name)
{
    
    Counter *counter = findObj(name);
    
    if (counter != nullptr)
    {
	    counter->selected = !counter->selected;
	    counter->setImage();
    }
	    
}

The C API is far from finished. New calls will be added, not least for the “GUI Library”, the means the module developer has for painting and rendering GUI elements on the map board.

Does Not Stack - This trait has a number of additional options that control selection, dragging and more. I will only implement a simple way of disallowing a counter to be stacked.

The point is to not allow a counter to be dropped where another counter is. Dropping is done in frame.cpp. You can not drop a counter on a Does Not Stack and you can not drop a Does Not Stack on a counter.

The trait definition is very simple.

DoesNotStack = {
}

You add this trait to a counter like this.

Repository:addTrait("French/VII-40", "DoesNotStack", DoesNotStack)

In CentralFrame::dropEvent (frame.cpp)

bool ok = Counter::snaptoDefaultGrid (obj->second, x, y);
				
if (!ok)			// does not stack
    continue;
else
    .
    .
    .

In counter.cpp

bool Counter::snaptoDefaultGrid (Counter *counter, int &x, int &y)
{
    
    // find the first (if any) counter close enough to snap to
    // returns true if a snap found and no doesnotstack 

    int maxDistance = 8;
	    
    for ( auto obj = counters.begin(); obj != counters.end(); ++obj  )
    {
	    if (counter->name != obj->second->name)
	    {
		    if (abs(x - obj->second->state.x) < maxDistance && 
			    abs(y - obj->second->state.y) < maxDistance)
		    {	
			    if (counter->doesNotStack)
				    return false;
			    
			    if (obj->second->doesNotStack)
				    return false;
			    
			    x = obj->second->state.x;
			    y = obj->second->state.y;
			    
			    return true;
		    }
	    }
    }
    
    return true;
}

Note: The default grid just reflects that you must be a certain distance away from any other counter for it to drop without stacking. When the map has an actual grid, you are only allowed to drop on grid points.

Note: I implemented a hex grid before (see 04. Grid ¡ RhettTR/Alben Wiki ¡ GitHub). Verdun: A generation Lost has no grid, only the default grid, the minimum distance between counters.

Hmmm. If there is a hook before drop

if (!Luau::beforeDrag(child->owner->name.c_str()))
	return;

why not implement a hook after drop

if (!Luau::afterDrag(obj->second->name.c_str(), x, y))
	continue;
else
    .
    .
    .

If Luau::afterDrag returns false (for whatever reason) then the drop does not take place. What we need then is to implement a Lua version of Counter::snaptoDefaultGrid. And here it is (in module.luau):

function afterDrag(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' then
    
		    if k ~= id then
			    
			    if math.abs(v["x"] - x) < maxDistance and math.abs(v["y"] - y) < maxDistance then
			       				
				    if counter["DoesNotStack"] ~= nil then
					    return false, x , y
				    end
				    if v["DoesNotStack"] ~= 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

This is the complete equivalent to Counter::snaptoDefaultGrid written in Lua. Isn’t that cool !!

By the way, I discovered a terrible bug in the previous version of Counter::snaptoDefaultGrid. Can you see what it is? Also note the return true, x , y. Lua can return more than one value which is very handy.

Dynamic Property - This trait I believe is superfluous in this engine.

Global Hotkey, Global Key Command - A few global hot keys and commands have been implemented in main.cpp. They can not be set by the user. This is CTRL-R (reload script), CTRL-Z (undo) and CTRL-Y (redo). The ESC button closes pop-up windows showing stack. As I understand it, besides allowing the user to activate certain functions, key commands were also used to identify functions internally in VASSAL.

It is obvious that certain functionality should have key commands for ease of use. I do not think it is a good idea to let the module developer be free to override any key command. I argue that many key commands should have the same functionality regardless of module (and for that matter regardless of application). CTRL-Z/Y is used in many applications, likewise CTRL-C/V (copy/paste). CTRL-D should always be Delete and CTRL-M should always be toggle move status. There should also be fixed key commands for rotate cw/ccw and so on.

I need to get back to this.

Invisible - Setting visibility is an important trait. The Visibility trait has (for the time being) two fields.

Visibility = {}

function Visibility:new (opacity)
    local o = {}
    setmetatable(o, self)
    self.__index = self	
    o.opacity = opacity
    o.apply = true
    return o
end

It is the usual apply flag and a field for opacity (1.0 = totally opaque). Visibility needs to be part of the rendering of the counter. Therefore it must be included in the state (state.h)

struct State				// the subset of fields needed to render the counter/card
{
	int x;					
	int y;
	bool moved;				// only true if trait		
	int degrees;			// only not zero if trait
	std::string image;		// name of current (flipped) image
	int zorder;				// position in a stack
	Table *overlays;		// masks & labels if any
	float opacity;			// opacity 0.0 to 1.0 (full)
							
};

In counter.cpp

// visibility 
    
if (state.opacity < 1.0)
{
    auto itr = this->table.find("Visibility");
    
    if (itr != this->table.end()) 
    
	    if (std::get<bool>(std::get<Table>((this->table)["Visibility"])["apply"]))
	    {
		    
		    QImage temp(image.size(), QImage::Format_ARGB32_Premultiplied);
		    temp.fill(Qt::transparent);
		    
		    QPainter *paint = new QPainter(&temp);
		    paint->setOpacity(this->state.opacity);
		    paint->drawImage(0, 0, image);
		    delete paint;
		    
		    image = temp;
	    }
}

A counter has a given visibility like this:

Repository:addTrait("German/38-94", "Visibility", Visibility:new(0.3))

Note that the apply flag can be turned on and off to show visibility. Also note that hidden units are not rendered at all for the side that can not see them. Setting opacity to 0.0 is not enough.

Layer - Implemented, not as a trait but inherent in the definition of a counter as the table of alternative images.

Mark When Moved - Implemented as the trait MarkMoved.

Marker - Just a key/value field, implicit in the script (like o.Side = "").

Mask - Implemented as trait Mask.

Mat, Mat Cargo - I regard mats as a third type of playing piece besides counters and cards. They may be in their own window. For later.

Menu Separator - To make a large right-click menu easier to read. For later.

Move Fixed Distance - This is about programmatically moving a counter. Note I do not implement this as a trait now, only show how moving a counter is done with script.

I created a counter on board (in module.luau).

copyCounter("German/38-94", "1002", -3, 300, 100)

The script that moves this counter from 300,200 to 37,27 is:

local ok = beforeDrag("1002")
if ok then
    local x,y
    ok, x, y = afterDrag("1002", 37, 27)
    if ok then
	    updatePos("1002", x, y)
	    Counter:generateGUI()
    end
end 

Note the calls to beforeDrag and afterDrag to make sure the move is legal.

function updatePos(id, x, y)
    Counter[id]["x"] = x
    Counter[id]["y"] = y
end

One additional thing I forgot (not related to Move Fixed Distance) is to update the Luau table with a new position after drag. This is important because when you clone a counter you duplicate the Luau table associated with the counter, and if this table is not updated the counter will have a wrong position. In frame.cpp:

Luau::updatePos(obj->second->name.c_str(), obj->second->state.x, obj->second->state.y);

Movement Trail - A complicated trait witch obviously belongs in the C++ part. For later.

Multi-Location Command - I suppose this in another trait that becomes unnecessary when you script.

Non-Rectangular - This is default. A mask is always set when you render a counter, whether rotated or not. I am not sure if being able to select a counter on its transparent parts has any meaning?

Place Marker - A handy trait for placing often placed status markers. For later.

Play Sound - Later

Property Sheet - Again I have a suspicion that this trait is not needed when you script. It seems superfluous. Have to look at it more.

Prototype - This trait is only needed when you can’t script. In Lua any trait can be inherited by any other trait, see traits.luau.

Replace With Other - Isn’t this just about being able to flip a counter?

Report Action - This trait is important. Mandatory reporting will be sent to the chat window (and also maybe to a log file). In addition it must be a possibility to customize reporting for custom actions.

Restrict Commands - A little of this has been done. It is for example totally meaningless to be able to delete a counter in the repository. In counter.cpp

if (strcmp(window, "Repository") == 0 && 
   (e.entryname == "Delete" || e.entryname == "Moved"))
	action->setEnabled(false);

Even if the functionality is there I see no “graying out” of the menu entries in Qt. This is an issue that needs more attention later.

Restricted Access - This is pretty much about can-not-move-if-not-your-side functionality. I did implement this in beforeDrag.

Return To Deck - An important right-click action concerning cards. Later.

Send To Location - This trait is not unlike Move Fixed Distance (?).

Set Global Property, Set Piece Property - This is done easily with script.

Spreadsheet - Wow, VASSAL has become quite sophisticated. You open a small window which allows you to change the values of fields. For later.

Sub-Menu - Important for large right-click menus. For later.

Text Label - Implemented as the Label trait.

Translatable Message - All engine text in local language, a huge sub-topic.

Trigger Action - Likely easy to implement in a script.


Ok, that was my going through the VASSAL traits. Sorry if it was rather rudimentary (or even wrong). The topic will of course come up again. For now I focus on going on with the design of Verdun: A generation Lost.

1 Like

I have now gone through the GUI quite thoroughly. I have implemented a host of new features, which include:

  • Opening a stack with increased offset. This is how VASSAL opens a stack.
  • Reordering a stack.
  • Moving selected counters from one or more stacks to one (or more) stacks.
  • Right-click menu actions on all selected counters in a stack.
  • The ability to disable a counter (no movement, right-click or selection allowed).
  • Opening a stack in the opposite direction to the usual up-right direction.
  • Use of the ESC button to close all stacks.
  • Reintroducing the double-click as an option to open stacks (how VASSAL opens stacks).
  • Ability to drag map and retain stack-size markers (corrected a terrible bug regarding
    the top overlay not scrolling in sync with the map, also setting correct size on top overlay).

On my wiki I have gone through all the changes with illustrations which I will not repeat here, see GitHub.

A GUI should be configurable. There are two main reasons for this.

  • Different users have different habits and preferences.
  • Games have different requirements. For example: if a game features many tall stacks close together it may be more useful to have offset-less stacks.

A GUI is never “finished”. It may approach a finished state if it has been used by many users
over many years with many different games. A GUI can always be polished and improved.

I will not go into technical details about the new code. Interested can study the difference between prototype4 and prototype5. There is one good rule in coding: if the code becomes difficult to understand and write, it may very well be that the design is wrong. Find a better way of doing it.

Plan ahead:

  • Implementing the toolbar with buttons for UNDO/REDO/scaling/rotation and removal of moved-markers. After this the engine will start to resemble VASSAL.
  • Saving and loading of games.
  • The artillery tables for Verdun: A Generation Lost. I will in time describe in detail what this is.
  • Cards. New prototype. I will likely chose Paths of Glory, a great design.
  • Hex grid. New prototype. Likely PanzergrĂźppe Guderian, also a great design.

I will not post again until new year. Happy holidays.

2 Likes