Some new design thoughts

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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


A summary of the engine so far:

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

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

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

1 Like

Some thoughts on your critique of this feature:

It’s sometimes useful to record an online session.

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

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

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

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

1 Like

Thank you for your comment!

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

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

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

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

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

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

2 Likes

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

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

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

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

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


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

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

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


dev.luau:

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

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


local callback = "toggle_window()"

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

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

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

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

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

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

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

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

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


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

-- define and set custom grid coordinates

local grid = {[1] = {}}

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

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


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

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


set_grid(grid, 1)

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

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

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

--- define fire location, key = target

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

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

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

-- create boxes for target coordinate and calculated firepower

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

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

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

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

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


-- create check box for setting counter battery fire

callback = "recalculate()"

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

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

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


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

-- create Luau tokens


local Token = {}

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


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


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

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

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

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

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

Artillery = {}

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


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

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

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


function printCoordinates(x, y)

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


function xyCoordinates(x, y)

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

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

Here is the function that does the calculation itself.

function recalculate()

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

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

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

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

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

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

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

I made an additional trait. In traits.luau

OnlyOneInstance = {
}

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


New code is on GitHub.

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

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

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

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

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

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

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

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

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