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.