Showing posts with label games. Show all posts
Showing posts with label games. Show all posts

Saturday, 6 January 2024

Modern C# & Performance (in brief)

This video came up in my youtube feed the other day (it's only 12 mins long, go check it out): 


In it the presenter shows some code written in classic, old-school imperative C#, and then progressively factors it into his ideal of modern C#.  

The algorithm he is implementing takes a sequence of strings, looks at all the ones which can be parsed as an int, and returns the total of the two largest values.  It returns -1 as an error code (so we can assume that the numbers passed in are all >= 0).

So, he starts with this:

 
 int GetMagicSum0(IEnumerable<string> items) {
     List<int> all = new List<int>();
     using (IEnumerator<string> enumerator = items.GetEnumerator()) {
         while (enumerator.MoveNext()) {
             int number;
             if (int.TryParse(enumerator.Current, out number))
               all.Add(number);
         }
     }

     if (all.Count < 2)
       return -1;

     int[] greatestTwo = {all[0], all[1]};
     if (greatestTwo[1] > greatestTwo[0]) {
         greatestTwo[0] = all[1];
         greatestTwo[1] = all[0];
     }

     for (int i = 2; i < all.Count; i++) {
         if (all[i] > greatestTwo[0]) {
             greatestTwo[1] = greatestTwo[0];
             greatestTwo[0] = all[i];
         }
         else if (all[i] > greatestTwo[1]) {
             greatestTwo[1] = all[i];
         }
     }

     return greatestTwo[0] + greatestTwo[1];
 }


And over a few iterations ends up with this:


 int GetMagicSum3(IEnumerable<string> items) =>
     items.Aggregate((max: 0, next: 0, count: 0), AdvanceRaw, ProduceSum);

 int ProduceSum((int max, int next, int count) tuple) =>
     tuple.count == 2 ? tuple.max + tuple.next : -1;

 (int max, int next, int count) AdvanceRaw((int max, int next, int count) tuple, string item) =>
     int.TryParse(item, out int number) ? Advance(tuple, number) : tuple;

 (int max, int next, int count) Advance((int max, int next, int count) tuple, int number) => tuple switch
 {
     (_, _, 0) => (number, 0, 1),
     (var max, _, 1) when number > max => (number, max, 2),
     (var max, _, 1) => (max, number, 2),
     (var max, _, 2) when number > max => (number, max, 2),
     (var max, var next, 2) when number > next => (max, number, 2),
     _ => tuple,
 };


...my blog page is set up pretty narrow, so here's an image of the code that you can zoom (I have nothing against long lines, I wanted to present it as text above so it was copy-pastable):


Now, we can argue about which code is better or more readable, but my first thought was that the performance is going to suffer.  I was then galvanized by this comment on the video:


This seemed super unlikely to me, so I decided to measure it, yielding these initial results:


Which bear out his response: the functional GetMagicSum3 does indeed run faster than the imperative GetMagicSum0... but you knew there was a "but" coming, right?  Did you notice the problem with GetMagicSum0?  Go back and look at it and see if you can spot it.

This is the setup for the test program btw: 


 string CSharpPerformanceTest(int wordCount = 100000000) {
     if (wordCount < 2) return "wordCount must be >= 2";

     System.Random rng = new((int)System.DateTime.Now.Ticks);
     int randomInt() => rng.Next() / 2; // halved so don't overflow

     List<string> numbers = new ();
     for (int i = 0; i < wordCount; i++)
         numbers.Add(randomInt().ToString());

     // Then time a call to each GetMagicSumX(numbers);


Did you see what was wrong with GetMagicSum0?  The problem with is is it generates a List<string> of the IEnumerable, completely unnecessarily!  Horvat does explicitly say that the algorithm isn't the point of the video - the style differences are - but I don't think pointing out that it's making a completely unneeded copy of the data really counts as algorithmic nit-picking!

If we're comparing imperative vs functional then I think it's fair that we use the best possible form of each.  We can assume the functional GetMagicSum3 is as good as can be, as the point of the video is to advocate for it, so let's make a good version of GetMagicSum0, we'll call it GetMagicSumA.  

Mostly we're simply going to remove the List generated at the start.  However, because of how IEnumerable works, setting up the greatestTwo array and then iterating from index 2 would be a pain: you'd need to make an IEnumerator, check Current was there and parsed, MoveNext(), check Current again, until you got two numbers...  sounds kind of loop-y.  Happily we already have a loop that does all that logic, so we'll simply use it to do all the work.  We set up greatestTwo so that it will always be filled out by the first two numbers, by setting it to { -1, -1 }. (If we wanted to handle negative numbers we'd use int.MinValue instead, but as we're only dealing with positive numbers, -1 is more readable).  This gets us:


 int _GetMagicSumA(IEnumerable<string> items) {
     int[] greatestTwo = { -1, -1 };

     foreach (string item in items) {
         if (int.TryParse(item, out int number)) {
             if (number > greatestTwo[0]) {
                 greatestTwo[1] = greatestTwo[0];
                 greatestTwo[0] = number;
             }
             else if (number > greatestTwo[1]) {
                 greatestTwo[1] = number;
             }
         }
     }

     if (greatestTwo[1] == -1) // didn't find two numbers
         return -1;

     return greatestTwo[0] + greatestTwo[1];
 }


Now, clearly I'm partisan; Horvat is bringing his best functional code to the fight, so in balance I'm going to represent imperative as best as I can.  To me, that greatestTwo array isn't the clearest thing to reason about in the inner if block, and I'd prefer not to nest quite so deep, so I'm going to refine this to the following semantically identical function: 


 int GetMagicSumA(IEnumerable<string> items) {
     int ultimate = -1;
     int penultimate = -1;

     foreach (string item in items) {
         if (!int.TryParse(item, out int number))
             continue;

         if (number > ultimate) {
             penultimate = ultimate;
             ultimate = number;
         }
         else if (number > penultimate) {
             penultimate = number;
         }
     }

     if (penultimate == -1) // didn't find two numbers
         return -1;

     return ultimate + penultimate;
 }


Comparing the readability of the imperative vs functional code is subjective, you can decide which version you find easier to reason about for yourself: below is GetMagicSum3 again for comparison.  It takes up 18 lines on the screen, while the above GetMagicSumA takes up 22 (I do tend to space my code out a fair bit).


However, the point of this post is not to argue about style, but about performance, and here are the results:


With the unnecessary List generation removed, the imperative code is indeed faster: GetMagicSum3 takes 1.26x as long as GetMagicSumA.

Follow-up for game developers


This section is for people who really care about performance, so if that's not you please don't get your knickers in a twist.
You probably noticed that in addition to GetMagicSumA, there are two successively faster versions of the function profiled above.  If you're using C# for writing games then this bit is for you: DO NOT USE IENUMERABLE.  I have never constructed an IEnumerable in any game, ever.  For sequences of homogenous data you need two types: List<T> and Array<T>.  If your data is sitting in memory there is no reason to use an IEnumerable.  If it's being generated an item at a time, then you need to be dealing with that asynchronicity with code that reflects it.
There is a very handy interface called IList, which will accept both List and Array, so is the perfect replacement here.  In essence this is the only change between GetMagicSumB and GetMagicSumA:


 int GetMagicSumB(IList<string> items) {
     int ultimate = -1;
     int penultimate = -1;

     for (int i = 0; i < items.Count; i++) {
         if (!int.TryParse(items[i], out int number))
             continue;

         if (number > ultimate) {
             penultimate = ultimate;
             ultimate = number;
         }
         else if (number > penultimate) {
             penultimate = number;
         }
     }

     if (penultimate == -1) // didn't find two numbers
         return -1;

     return ultimate + penultimate;
 }


As you can see the two difference in the code are the parameter type, and using a for instead of a foreach; the result is substantially faster code.  Using GetMagicSumB as the baseline, GetMagicSumA takes 1.19x as long as it, and GetMagicSum3 takes 1.5x as long.

Bonus Round


The table of timings contains a final form of the function, GetMagicSumC, which is marginally faster again.  It's clear in the original video that the point is not about optimising the algorithm, so this isn't relevant to the post in general.  Therefor, an exercise for the reader: how do you modify GetMagicSumB to get GetMagicSumC, and that small gain in performance? Clue: .

Saturday, 2 December 2017

Debugging your TTS mods with console++ : Odds & Ends

Let's wrap up this series (which started here) with some miscellaneous features of console++.

The console module, which console++ is built on top of, includes a handy command: the '=' command gives you access to Lua's dynamic.eval function; in other words it works as a simple expression calculator.  Type '= ' then any mathematical expression and it will display the resulting value.  You can refer to any of the variables in your program when you are a table admin, but all players can use the '=' command for simple maths functions.  Handy if your game is anything like Power Grid!



There are also a couple of commands for interacting with the chat window output:

'echo' will print its parameter into the chat output (just for you); useful to include some output in a script you wish to 'exec' with the -q or -s parameters.

'cls' will clear the chat window.


Finally, console++ gives all players a 'shout' command: this will broadcast its parameter as a message to all players, popping up at the top of their screen as well as in the chat log.  It will appear in your player colour.


I hope if you are using console++ that this series of posts has been helpful, or if you're not that it might have tempted you into trying it.  Any feedback is greatly appreciated, especially bug reports.

Monday, 27 November 2017

Debugging your TTS mods with console++ : Watch List

This will be the final installment of the tutorial series about console++ which began here.

In this enthralling episode we'll look at the 'watch' command; a very useful debugging tool.  'watch' allows you to monitor a variable or object in your game (or even a function: more on that later), displaying a message in the chat window whenever it changes.

In order to use this feature of console++ you must call 'console.update()' in an 'onUpdate' event.  The example module already includes this command:



If you've followed this series so far then after firing up the example module in Tabletop Simulator you'll already be in command mode (thanks to console.autoexec);  type 'slow' to enable the card display.  Now if we type 'ls' a few times, waiting a short delay between each, we'll see the next_check variable increasing:


This is a pretty awful way to check its progress however; lets use 'watch' instead:
Type 'watch /next_check'.  Every two seconds the next_check variable will change and we'll see it do so.  This is because we currently have check_delay set to 2 seconds; type 'fast' to change check_delay to 0.2, and you'll see the variable change every 0.2 seconds instead.  Switch it back to 2 seconds with 'slow'.  You wouldn't want this running after the point where you get the info you want; it's filling the chat window.  To clear the watch list type 'watch -c'.  The '-c' parameter clears the watch list if it's used on its own.  You may instead supply it with a variable name to only clear that variable from the watch list: here we could have typed 'watch -c /next_check'.


We can also watch objects: type 'watch /dice/d6' then roll the dice.


You can see it will track the objects rotation and position.  This is fine for an object which you have used in your program, but not all objects will be stored in a variable.  Happily we can also track objects via their GUID; right click the chess rook and in the scripting menu click on its GUID to copy it to the clipboard.  Now type 'watch -g ' and hit ctrl-v  to paste in the rook's GUID.  Pick up the rook and move it around to see it being watched.

You can see what variables you are watching by typing 'watch' without any parameters:


You can also disable the watch list temporarily by pausing it with the '-p' parameter; this lets you stop the watch list outputting to the chat window without clearing it (and losing everything you are tracking).  Try it now: type 'watch -p' and move the rook around; no updates! Do it again to unpause (use up-arrow to fetch the previous command).

Grab the rook and move it around; the watch list really spams the chat window, since every tick the position changes.  In a lot of cases this will be too much information, so the watch command allows you to throttle its output for each given item.  Use the up-arrow key to fetch the command that added the rook to the watch list ('watch -g b27933', unless your GUID is different) and then add to the end of it '-t2'.  This will add a 2 second throttle to the watch.  Now drag the rook around and notice the difference.  Notice also that this command has replaced the previous version of the item in the watch list, overwriting the old, unthrottled item; each variable or object may generally only appear once in the watch list.



Now clear the watch list  with 'watch -c'.

As well as tracking variables and objects, we can also watch their member functions.  Type 'watch /dice/d6/getValue', and then roll the dice:


Clear the list, and then try to do the same for the dice's position with 'watch /dice/d6/getPosition'.  Argh, what's happening here?


What is happening is that every tick the watch list is calling the getPosition function of the dice, and every tick it is getting a new table.  Even though the table's member values are identical, the table itself is changing, so the watch list is outputting it each frame. This is definitely not useful!  We can mitigate this by using the '-/' parameter; this parameter specifies a member of the returned table to watch, instead of the table itself.  Type 'watch /dice/d6/getPosition -/x'; phew, the spam has stopped.   Move the dice around to see the 'x' value being tracked, then clear the watch list.



As well as variables and objects, we can also watch functions. To test this out we'll need one so let's see what functions we have available with 'ls -f'.  Ah: 'dice_total' is nice and simple as it has no parameters; lets watch it with 'watch /dice_total'.  Roll the dice to see the function changing.

We can also use functions with parameters; let's do that, but lets make things a little more complex, more like a situation you might find yourself in while actually debugging your mod.

What does the near function do?  Here is its source:


Not too complicated a function; it checks if two objects are near each other on the x and z axis.  We're going to use it to check when the rook is near the d4.

Our first problem is that we do not have a variable for the rook in our program; remember when we were watching it before we were doing so via its GUID.  That's not an option here: the near function does not take a GUID as a parameter, it takes two object variables.  Therefor the first thing we have to do is make one up for the rook.  We'll do so using the 'call' command: type 'call getObjectFromGUID 'b27933'', and then 'add /rook ~'.  (the '~' special variable holds the last result).


Now we have a variable holding our rook we can use it as a parameter to the near function; type 'watch /near /dice/d4 /rook', and then pick up the rook and move it around, over the d4 and away from it.



The near function is fairly simplistic, but I hope you can see how we can write debug functions in our code, and then hook them up to problematic objects during a play session, using them to work out what is going wrong with the mod.  More persistently, if you know you want to track certain variables or objects you can add 'watch' commands to your console.autoexec, giving you feedback on events which you want to know happen (but which don't necessarily throw error messages) every time you play your mod.

Thursday, 23 November 2017

Debugging your TTS mods with console++ : Batching and Aliasing

This continues the series which started here.

The first thing we'll do in this session (after loading the example mod in TTS and loading it into Atom) is use console++'s autoexec feature: this allows you to set a series of commands which will be executed when your mod is loaded.  In order for this feature to work you need to call console.load() in your onLoad function; the example mod already does this as the last line:


With that line present all we need to do to make console++ execute a script on startup is set it in the console.autoexec variable.  Add this block of code above the onLoad function:



console.autoexec = [[
    cmd
    cd /console
]]



Hit Save And Play; now when we hit enter in TTS and type a command we don't need to prefix it with '>' and don't need to remember to switch to command mode - the 'cmd' command has already done that for us.  Type 'ls' to test this out:


You can see we are already in command mode (the command worked without a '>'), and are indeed inside the console table.

Now, the autoexec feature isn't groundbreaking: it's not doing anything we couldn't simply do with Lua commands.  Instead of making an autoexec script we could have written this code:


This accomplishes the same thing: sets you to command mode and changes your current location.  It was simpler to just type the console++ commands though, no?  Setting up the autoexec has an additional benefit; we can invoke it manually using the 'exec' command.  Type 'cd /' to return to the root table, then 'exec console/autoexec', then 'ls': you'll see we are back in the console table.  

Thus you can set up batches of instruction in strings in your program, and then run them with the 'exec' command; console.autoexec isn't special in this (it's only different because it is run automatically on loading) - any string variable can be executed.  

As with any command which takes a variable as an argument, we can make it work on a literal string too: thus we can use 'exec' to run a sequence of commands entered as one line.  To do this we need to know the use of these three punctuation marks in console++:
  • A string parameter prefixed with a back-tick (`) is treated as a literal (instead of a path)
  • Double- and single-quotes (" and ') can be used to surround a parameter containing spaces in order to stop the spaces delimiting the parameter.
  • The semi-colon (;) can be used to separate commands in a script instead of a new line.
So to duplicate the above autoexec script on a single line we type this:

exec "`cmd; cd /console"

The command line sees the surrounding double-quotes and treats everything inside them as one parameter. It then sees that that parameter starts with a back-tick and so knows it is a string literal, and then the 'exec' command splits that string up by its semi-colons.



We'll come back to 'exec' later; right now we're going to look at the 'alias' command.  'alias' allows you to create your own commands from any existing commands.  At its simplest this simply lets you rename commands to suit yourself: console++'s 'ls' and 'dir' commands are actually the same command: one is simply an alias of the other.  If you wanted to add your own name you can use 'alias' to do so.  Let's say you are especially verbose, and you want to type 'list' instead of 'ls': simply enter 'alias list ls'.

This is a pretty silly example; lets make it actually useful.  By default the 'ls' command only displays variables and tables - it does not display game objects or functions. To display everything in your current location you use the '-a' option: 'ls -a'.  Let's set up an alias for that: at the command prompt type 'cd /' to go to the root then 'alias list ls -a', and then 'list':


I typed 'ls' afterwards to compare the results: you can see our 'list' command displayed the functions as well as the rest, while 'ls' did not.  If you like this behaviour you can include this alias command in your autoexec to make it available whenever you run your mod.  You could even take it a step further, and alias the 'ls' command over the top of itself with 'alias ls ls -a', which would replace the default ls behaviour, allowing you to just type 'ls' to see everything (though a better alias for this would be 'alias ls ls -fov', as this will allow you to toggle them off again; '-a' overrides any other parameters)

You can check what a command does with the 'help' or '?' command (as with ls/dir, one of these is just an alias of the other). Type '? list' and you'll see:




Now we're going to combine these two commands, 'exec' and 'alias', to create some commands for controlling our mod.  Enter these commands:

alias slow exec -q "`set /update_cards true; set /check_delay 2.0"
alias fast exec -q "`set /update_cards true; set /check_delay 0.2"
alias off set /update_cards false

After entering these aliases we will have three new commands: 'slow', 'fast' and 'off'.  Typing 'slow' or 'fast' will enable the card display updating to match the dice total; one being more responsive than the other.  Typing 'off' will disable the card updating.  Try them out!

The '-q' (quiet) option tells the 'exec' command not to display the result of each command it is executing; just to display the final result.  If we used the '-s' (silent) option instead then the final result would also be suppressed.  Alternatively we could have used the '-v' (verbose) option to make 'exec' display all the commands it is executing as well as their results.  If you want to you can add your own custom output by using the 'echo' command, which prints its arguments in the chat window.

Note we don't need a back-tick before true and false - these are special cases.  If we were assigning a string we would need the back-tick, or it would try to look up the string as if it were a path.

These are nice, but no good if you have to type them in every time you open your mod in TTS.  That's OK though; we can add them to our autoexec!  Remove the 'cd /console' command and add them in like this:


Now whenever you load your mod you will start in command mode, and have these commands at your disposal.

Monday, 18 September 2017

Debugging your TTS mods with console++ : Functions

Don't just jump in here: start at the beginning!  Once you've loaded the module in Tabletop Simulator remember to switch to the Game tab and turn on command mode with '>'.

In the previous posts we've seen how we can display variables with 'ls' (or 'ls -v') and objects with 'ls -o'; the final option available is to display functions with 'ls -f'.


Here you can see the functions defined in the example mod.  We can do more than just display them, however; we can also call them.  Type 'call dice_total', then roll the dice and call it again.


What if we want to store the value a function outputs?  console++ defines a special path for the last returned value: the '~' character.  You can see this if you display it with 'ls ~', and all other commands can use it like they would any other path; this includes 'add' and 'set'.  Add a variable called total using the '~' path and check it's set to the right value:


The 'add' and 'set' commands can also be used to store a result directly, by calling them with no value argument before the 'call' command.  If you do so the the next 'call' command will store its result where you have specified.  Roll the dice again and then try it by typing 'set total' and then 'call dice_total':



Note that console++ also has a short-cut command to display the last result: simply type the result character on its own to display its contents: '~'


We can pass parameters to functions just as you would expect, by adding them to the 'call' command.  Let's use the is_face_up function on one of the cards.  Remembers, the card objects are stores in the cards table; you can see them by typing 'ls -o cards'.
Type 'call is_face_up cards/2' to call is_face_up on the 2 card, then flip the card and issue the command again (use up-arrow to get the last command typed instead of typing it again).



Note how when we 'ls -f' we see only the functions defined in the mod; we don't see all the built-in Lua functions.  That's not because they are not present; it's because console++ hides them under the 'sys' label.  If you do an 'ls' you will see two things at the bottom of the tables that look like tables, except they are in a salmon colour instead of yellow; these are labels used to hide globals to keep things tidy.  You can see what they are hiding by calling 'ls' on them;
try it now: 'ls sys -f'


At the point in the Lua code where you #include console++ it automatically scans all the globals and hides them under the `sys` label, thus keeping your mod easy to interact with in the console.  All these things still exist in the root table, though they may also be accessed via the label they are hidden under. i.e. 'ls /math' and 'ls /sys/math' are the same thing:



You can also create your own labels to hide things under by calling console.hide_globals in the mod's Lua code.  Type 'ls' and you'll see that as well as the 'sys' label there's also a 'guids' label; if you look at the end of the GUIDS block in the Lua code you'll see the command that created it; whenever you call console.hide_globals it will scan for global variables/functions etc and hide them under the label you specify (provided they haven't already been hidden under another label).



To come back to the point: all the built-in functions are available should you wish to call them.  For example, we can get a color using stringColorToRGB then display the RGB values:
'call stringColorToRGB Blue' then '~':



We can then store it in a variable and use it to highlight an object:
'add blue ~' then 'call /decks/b/highlightOn blue 100'