Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Messages - Simon

Pages: [1] 2 3 ... 201
Tech & Research / Re: Object-oriented design with Simon
« on: April 04, 2020, 06:04:29 pm »
(In a future post, I'll write about mixing std::unique_ptr with covariant return types in C++. But it's necessary to introduce covariance first. Thus, here goes.)

Covariance, Contravariance

Let us have class Animal and subclass Cat. Thus, each Cat is an Animal; wherever an Animal is expected, we can submit a Cat. But not every Animal is a Cat.

Now we have a Source<Cat> that can provide a Cat on demand, e.g., a pregnant female cat, or a crusty old cat-hoarding lady.

Wherever Source<Animal> is expected, we can successfully submit the lady. The lady provides a Cat which is also an Animal, thus she fulfils the requirements to be a Source<Animal>.

We say that Source<T> is covariant in its type argument T. Both of these arrows point into the same direction:

Cat ------------- is a ------------> Animal
Source<Cat> ----- is a ----> Source<Animal>

Now we have a Sink<Animal>, e.g., an animal shelter.

When somebody cannot care anymore for their Cat and asks where the next Sink<Cat> is, we can point them to the animal shelter. The shelter will accept any Animal, in particular, it will accept a Cat.

We say that Sink<T> is contravariant in its type argument T. These arrows point into opposite directions:

Cat ------------- is a ------------> Animal
Sink<Cat> <------ is a ------- Sink<Animal>

Now consider the types List<Animal> and List<Cat> that allow insertion and lookup. Assuming a strong static type system, List<Cat> guarantees that only a Cat may get inserted, and also guarantees that anything inside is a Cat.

Where a List<Animal> is expected, can we submit a List<Cat>? No, because the code might then try to insert Dog into List<Cat> because that code believes it has a List<Animal> where insertion of Dog would be allowed.

Where a List<Cat> is expected, can we submit a List<Animal>? No, because the code might then try to look up the first Cat of the List<Animal> because the code believes it has a List<Cat>, but instead might get a Dog from the List<Animal>.

We say that List<T> is invariant in its type argument T. There is no subtype relationship between List<Animal> and List<Cat>.

Example. A machine that clones an object, i.e., gives back the same object and an equal copy, is covariant: An animal cloner gives back an animal and a copy, a cat cloner also gives back an animal and a copy. Really, a T cloner is a Source<T>.

Example. A Sink<Sink<T>> is covariant in T. The arrow gets inverted twice.

Cat ---------------- is a ---------------> Animal
Sink<Cat> <--------- is a ---------- Sink<Animal>
Sink<Sink<Cat>> ---- is a ---> Sink<Sink<Animal>>

Real-world examples of Sink<Sink<T>> are convoluted and misleading, e.g., burger stores that only cater to zoo keepers who feed only cats? This occurs more naturally in software, e.g., an algorithm might be best described as a higher-order function that takes as argument a function that takes T.

C++ and D allow covariant return types when you override a function that returns a pointer. Thus, in the following C++ code,

class Animal {
    virtual Animal* clone() { return new Animal(); }

class Cat : public Animal {
    virtual Cat* clone() override { return new Cat(); }

... it is legal to override Animal* clone() to have the stricter return type Cat* which converts implicitly to Animal*. Virtual dispatch behaves the same as usual, i.e., calling animal->clone() gives you an Animal* that might well come from Cat::clone.

This is good for two reasons.
  • The type system can check more rigorously what you implement in Cat::clone while class Cat gets compiled, even if the method call later will only ever happen via virtual dispatch from an Animal*.
  • Nonvirtual callers who definitely have a Cat will be guaranteed to get Cat*, not Animal*, which is useful in a world that deals only with Cat.
My plan is to write another post on how this fails with std::unique_ptr<Animal> instead of Animal*, and discuss workarounds. :lix-grin:

-- Simon

NeoLemmix Main / Hidden objects in NL, part 2
« on: April 03, 2020, 02:51:20 pm »
Split off from 3 things I don't like about neolemmix.
Related: Hidden Objects in Backroutes: OK?

Original statemet by WillLem: I'd say the biggest irony of the NeoLemmix philosophy is that, via its player-assist tools, it makes things like precise skill placement and hidden objects a complete non-issue, and yet it still vehemently rejects these things as being an occasionally valid part of the game.

I (Simon) happened to reply first:

precise skill placement [...] a complete non-issue, and yet it still vehemently rejects these things as being an occasionally valid part of the game.

When a design absolutely requires precision, well, let the solution be precise.

When a design doesn't need precision, why force the player to be overly precise?

and hidden objects

The puzzle will never gain anything from hiding, thus don't hide.

-- Simon

General Discussion / Re: Simon blogs
« on: March 31, 2020, 08:13:01 pm »
Thanks for the replies! :lix-blush: Yes, Lix has brought me far. Still, there won't be 12 colors in Lix. :lix-grin:

Magic Cave
(hobbies, time management)

I miss the magic cave from doctoral studies where you would work on a creative project for days, then on the thesis for days, then again on hobbies. Lots of Lix bugs got fixed in the magic cave, many posts were written here on the forums.

The downside of that magic cave is that you naturally let the thesis slip. Any time spent on hobbies could be spent on thesis, thus, even in the magic cave, one will never feel truly free. Likely, that feeling is impossible to re-attain after the age of 5. Still, it's the guiding light to what you should do with your time.

I still want to write a paper with my professor, now that math feels much more like a neglected hobby. Already behind schedule, but it's fine since prof isn't rushing either. It's a side project for both of us.

I have spent time on other hobbies, thus I shouldn't feel like having wasted all too much time.

I have loose ideas for a tile-grid-based Lemmings-inspired puzzle game with many different gadgets, to let my inner five-year-old bloat features. Rules must be really sharply defined, turn order and tile-based movement must be clearly specified. Proxima would be delighted. :lix-grin: No time these days, though. :lix-winktongue:

When you don’t create things, you become defined by your tastes rather than ability. Your tastes only narrow and exclude people. So create. -- Jonathan Gillette

(software engineering)

We had a three-day seminar on Clean Code, the classic object-oriented guidelines à la Uncle Bob. I've always enjoyed OO and soaked much of this wisdom already in the years past, during magic cave. Thus, the co-workers might have gotten more out of it than I did. Nonetheless, good rehearsal.

According to the seminar, I should make more interfaces and fewer abstract base classes. This seems hard to follow when you roll your own GUI as in Lix: We have GUI Element, an abstract base class that already has important behavior, e.g., decision whether the Element should be redrawn. Then class Button inherits from Element, then TextButton and BitmapButton inherit from Button. How to avoid inheriting from classes here, and instead only inherit from interfaces? Even if TextButton and BitmapButton became decorators, Button would still inherit from Element. Anybody want to give it a shot?

I've ordered a book on UML, Kecher's UML 2.5, a German classic. Allegedly, UML is the driest thing in the software engineering universe. Doesn't matter, it's handy in real life and will fix a sizable knowledge gap.

Error Handling
(software engineering)

I'm interested in error handling strategies. Reason: In C++, everything sucks or is nonstandard. :devil:

Ye olde C errore propagation is by return values: Everything returns int, negative values mean errors, zero or positive are good results. Functions return the error code and write other useful output into a buffer that you pass to the function by reference.

int foo(int x, int y, unsigned char* outBuf, size_t outBufLen)

This permeates the codebase at work. Call sites then look like: Make buffer, call the function, if the function returns an error, print something to the global log stream, and abort, or maybe continue if you don't deem the error that bad. The downside is the ton of checking boilerplate around every call. And you can't declare the output const because you have to declare its variable first, then fill it in the call. (Also some people don't like to return early and write 10 nested ifs, but that wouldn't be contemporary C anyway.)

Well, we have C++ exceptions, but they aren't part of the type system. noexcept is a feeble attempt to fix that, it's nowhere used in legacy code. For every method, you must remember whether it throws, and what it throws, to decide whether you want to handle the error. You must read the API docs or hope that it doesn't throw.

As long as you call even one maybe-throwing method, the compiler must generate exception handling around your call. I haven't studied compiled binaries to how fast or slow it is. Even if this were no speed problem -- I don't like exceptions, they feel wrong, it can't be good design that the easiest thing to write is to let the program crash by unhandled exception.

Really the most fitting for the work codebase would be Zig-style error handling: Your functions return either an error code or a useful value, and there are special language features to abort execution and propagate the error. With such features, you may write the call site as if the callee returned no error; on error, it behaves like an exception that is baked into the return type. Or you can explicitly handle the error, again with syntactic sugar because handling is common.

Now, Java had checked exceptions that sound like much the same, baking the possible error into the method signature. But modern Java is moving away from them. Shall we heed this as a warning? Hmm, Zig has no virtual dispatch, not even type inheritance. And Java had some unchecked exceptions from the beginning by design: Nullpointer dereference, or out of memory. While I still firmly believe that nullpointer exception is a glaring misdesign -- the type system should handle whether your pointer may be null or not, and force you to check if applicable -- I have no strong opinion against the out-of-memory Java exception.

In C++, you can define your own error-handling type: A tagged union of error code and useful result. Then all your functions return this tagged union. You can call methods on the tagged union and the call only goes to the useful value if there hasn't been an error yet.

template <typename T>
struct OrError {
    int error;
    T value;
// use only if no error
/* ... maybe methods to chain calls on these as long as no error ... */

The downside of the tagged union is that this is impossible to retrofit over the C-style error handling: You'd have to write a wrapper for every int-returning buffer-filling method -- there is no way to generate all the wrappers with a template because the out-buffer parameter is in a different location for each C function. Also, the entire codebase would have to agree on one such error union type, otherwise it would split into worlds that can't call each other's functions without wrapping.

If the wrappers could be autogenerated by the language, we would have a full-fledged error monad. :lix-grin:

You could write code in the reactive paradigm where your method call gets you a value stream and a separate error stream. Again, massive boilerplate in languages that don't support it easily.

Thus, there is no clear winner. Every time, it itches enough to find a good solution, but in the end, it's least painful to stick to the convention.

The easiest code to call is still code that cannot fail. :lix-mystery:

-- Simon

Tech & Research / Re: Object-oriented design with Simon
« on: February 18, 2020, 08:28:40 pm »

Help & Guides / Re: Barns, animals, feeding: Software class design
« on: February 18, 2020, 08:16:08 pm »
Restored this thread from Recycling in accordance with Dullstar. Enjoy!

-- Simon

Tech & Research / Re: Rendering speed in Lemmings-like games
« on: February 18, 2020, 06:53:29 am »
The pointer chasing was 1 % or 2 % at best when I let the replay verifier run without graphics. This loaded the level, but created no savestates or only a single one. Then it ran the physics. Emplacing the job within the lix was only a small gain. I'd have to test more cleanly to isolate for pointer chasing, or for savestating.

Avoiding two allocations per lix, and instead implementing a single allocation for all lixes in one go, this was probably a bigger win than 1 %, but I can't tell. This is very hard to measure because the important use case is human framestepping. (There is no need for savestating when you verify replays automatically.)

The lion's share is always with the graphics: Choice of graphical libraries, and the particular usage of the library within the game. Hardware graphics are faster than software graphics in some ways and slower in others. As lone developer, with no special training in graphics, we're optimizing on hardware designed for 3D games.

Then there are the Lemmings 1 drawing engine or DOS video memory, I don't know anything about either. :lix-grin:

-- Simon

Help & Guides / Re: Barns, animals, feeding: Software class design
« on: February 18, 2020, 06:19:11 am »
Visibility of objects. This is the heart of the design problem: An animal in the barn shall access the food in the same barn, but the animal shall not see the food in other animals or anywhere else.

There are several ways to have an animal access the food:
  • The animal knows permanently about its barn. (An animal has a field that is a reference to its barn, and we set this field, e.g., when the animal is constructed.) The barn has a method that provides food. The animal can then get food from its barn.
  • The animal knows about the barn only at feeding time: When the barn calls the animal, the barn gives itself as an argument, so that the animal can get food from there.
  • At feeding time, the barn asks the animal how much food it wants. The barn then deducts this much food from itself and passes that food to the animal.
  • The barn contains not only the animals, but also a food source, an object of a new class to design. At feeding time, the barn points each animal to the food source, and lets the animal interact with the food source. The animals need not now about barns, but only about food sources.
None of these is best in all cases. It's up to taste, and maybe we will reorganize this in a couple months when the system expands.

Asking the animal for the exact amount of food, then giving it that much food, violates Tell, don't ask: Hunger is a decision of the animal, not of the barn, therefore we should put the code in the animal, not into the barn. The barn shall tell the animal to get food, not ask the animal about food.

Decoupling the food source from the barn is the most extra code to write, but is good once we herd animals outside a barn. The developer of the system can tell best how much decoupling/generalization is appropriate. :lix-grin:

It also depends on how much other work the barn is doing. If the barn has tractors, hay, ..., that's a reason to delegate the animal-keeping/feeding to another part of the program: separation of concerns. If the barn became empty after we removed its keeping/feeding, concerns were probably separated well in the original design already.

Modularity. (Inner classes, moving types to different files.) This example is small enough such that every type may still see every other type, and everything can go in the same source file.

Nonetheless, I'll go into detail here to show why inner classes will not help solve our problem.

In Python, classes are Python objects because everything is a Python object. A class Y can be a member of another object x of class X. This doesn't gain much from the classic OO viewpoint: An object y instantiated from that inner class Y has no special relationship anymore with x. Any association must be set up as normal, e.g., when y is constructed, x might pass itself as a parameter to Y's constructor.

In Java, classes can be inner classes and access the outer classes' private members. I don't believe they are commonly used. If some classes only make sense within a part of the Java project, it's natural to express this via the package system (code in same file, or at least in a file in same directory).

In many languages, you would move classes, types, functions, ..., that loosely belong to each other into different files (modules) in the same directory (package), and import modules to use the types. Whether a single file becomes too big is again a matter of taste and convention.

Data hiding, invariants. This is typically the first mantra that one learns about OO, that one shall hide the raw data. It's merely not the heart of our design problem here; in a way, the objects themselves were hidden too well already, and we're trying to find the best associations between the objects.

For encapsulation, a class hides its data and instead exposes methods that enforce invariants. E.g., the barn or the food source tracks an amount of food, and allows the animals access to the food via methods. These methods enforce that animals cannot take more food than the barn has, and that animals cannot add food. The animals have no direct access to any integer that tracks food, they cannot set it negative.

The benefit of this is that if we ever have a bug with negative food, the encapsulation tells us that the bug must be within the barn.

-- Simon

Tech & Research / Re: graphics of lemmings 3
« on: February 09, 2020, 07:39:44 am »
L3 level editor by kieranmillar, continued from Mindless

Class Style in src/style.* loads terrain and gadgets from files. Style::load() first sorts the binary data from files into object and block, then draws them. I haven't understood the drawing from first glance.

NL has L3 styles, right. They might be only 99 % accurate. Reason: I remember how I manually painted correct shading on some of NL's L3 tiles that were merely horizontal flips of other tiles.

Maybe 99 % accuracy is good enough. What are you trying to do?

Lemming sprites, I don't believe anybody has extracted those. Spriters Resource has nothing on Lemmings 3.

-- Simon

Contests / Re: The Level Solving Contest #7
« on: January 04, 2020, 05:32:44 pm »
Highly interesting that ccexplore's route finishes with the exact same stats than geoo's/mine. Merely from watching, I guessed that ccexplore's stats had to be higher, and I looked at the text replay to confirm that it's the same. Nice find!

ccexplore uses that the basher (that makes an 18-pixel-high tunnel) has 2 pixels of steel lenience at the top (basher will not cancel if steel falls within these 2 pixels). This is pixel-precise, but allows the basher to continue towards the outer wall. This avoids the brick that minim places.

My and geoo's solution is extremely fiddly and requires lots of framestepping. The tweaker (filmstrip button) to timeshift assignments was helpful. There is a leftover bug that the tweaker always covers the right-hand side of the screen/map, the map doesn't know whether the tweaker covers the map, and the hacky action happens near the right. Inclines me to fix this. :lix-winktongue:

Batting the infinite fallers on this map is fiddly unless the brick-layer is coincidentally well timed. Floating everybody would have been easier, but the floating nukes the total skills score that takes precedence over SAOBL.

I'll have busy weeks ahead -- moving out of my student's flat -- and can't run an immediate follow-up contest. But I've already gotten first ideas for when it's time. I'm contemplating to add/multiply/take-maximum-of the different scores instead of deciding on a lexicographical order.

-- Simon

Contests / Re: The Level Solving Contest #7
« on: January 04, 2020, 04:23:49 pm »
Thanks for hosting!

Only today, I find time to adequately reply. I'm about to watch the solutions in Lix.

Geoo's txt file I found harder to read than the others probably because he was using a considerably older version of Lix

geoo directionally forced some assignments. For example, the middle two lines here are directionally forced assignments; the first and last lines here are unforced:

! 1305 0 ASSIGN=DIGGER 10

In singleplayer with heavy framestepping, it has become less common to force direction.

I made directional force part of the replay format for networking games about 5 years ago. This was to combat latency between several players on the same team. E.g., you have a lix walking towards an abyss, and want to assign walker to turn her. If your teammate also assigns walker to turn her away, and both of you directionally force your walker assignments, the lix will only turn once, away from the abyss, and the second assignment becomes illegal as desired.

I've even considered to write every single assignment into the replay with directional force, not merely those where the player actively forces. I.e., for an unforced assignment, I would look at the highlighted lix's direction, and write the assignment forced with that direction. I don't know if that would be helpful or a hindrance, in any of the three interesting scenarios: a level's terrain changes, the physics change, and networking lag.

Anyway, that's rambling and getting technical; if anybody has strong feelings on this, we can happily make a thread from this idea. On to watching replays.

-- Simon

64-bit physics updates would be a physics change because the binary networking protocol hardcodes 32 bits for this field. :lix-evil:

But yes, it's sound to upgrade phyus to 64-bit or to stop physics at at 2^31 - 1. The stop is sounder game-design-wise but sadder mathematically.

-- Simon

Yes, it's an overflow bug in Lix. Good find, yes, Forestidia had the idea before, too.

It should be patched for 0.10.0, whenever that comes, as namida suggests, fallers that have achieved splat fall distance should remain splat-prone until the end of the fall.

In D, signed ints are defined to overflow by two's complement like an unsigned int, and only during interpretation treat the most significant bit as a sign bit. (In C and C++, signed int overflow would be undefined, even though implementations on contemporary machines often treat them like D has to.)

-- Simon

Lix Multiplayer Dates / Re: Lix Multiplayer: Sun, Dec 29th, 18:00 UTC
« on: December 29, 2019, 02:08:58 pm »
Sorry, no stream by me. I'm not at my home computer, I'm with my parents during the holidays and haven't set up the stream on my notebook. I'll try to stream the next sessions again!

IchoTolot usually records for Youtube, but he's also with his parents over the holidays, therefore I don't know if he will record.

In a pinch, if the asker has Lix, they can sit in our game room on the central server as observer.

-- Simon

Lix Main / Re: Lix 0.9.30 released
« on: December 29, 2019, 11:12:30 am »
Yes, any 0.9.x is good for multiplayer, in particular 0.9.29 and 0.9.30.

-- Simon

Lix Main / Re: Lix 0.9.30 released
« on: December 28, 2019, 10:01:36 pm »
Lix 0.9.30 released.

:lix-cool: Download for Windows
:lix: Download for Linux 64-bit
:lix-evil: Source code
:8(): Changelog
:8:()[: Issue tracker

How to update (click to show/hide)

It's been a while! In August 2019, I got a fulltime job as a C++ engineer. Naturally, I've begun to relax with software-unrelated things during weekends. But as promised, here's a Lix release still in 2019.

  • Fix #339: Imploder animation: Now, purple stars and vortexes circle around the center, then get sucked into the center. The old animation was too slow and subtle.
  • Fix #388: Oblivion sound was too noisy and high-pitched. Replaced this sound effect with meow-like voice acting that is not shrill.
  • lemforum: ccexplore fixed a backroute in Brickout.
  • Always print hotkeys on buttons' lower-right corner. Removed user option that toggled whether hotkeys would be printed on buttons.
  • Code: Remove body encounters from physics implementation; these happened at (foot - 4), (foot - 8), (foot - 12). Always check foot encounters instead. Before, only fire hazards checked for body encounters. Draw larger trigger area (in the editor) for fire to compensate. No changes to file formats. No changes to physics.
  • Multiplayer maps: Added Down with Frogs by Flopsy. Added remake of Humps (2-player map from 2010) using Lix's earth terrain. Removed endless falls at the side of Honeypots.
-- Simon

Pages: [1] 2 3 ... 201