Hashiwokakero ( Hashi Bridges )
Source Code
Play the Web Version Here
A zig implementation of the popular japanese game “Hashiwokakero”, using the raylib-zig binding for the graphics. This is my first project using zig, I’m much more familiar with C/C++, for example. I wanted something about equal in scope for Zig so I could learn it.
How To Play
- All islands have a number in the center representing how many bridges are attached to it
- Some connections can be double bridges, some can be single
- All islands connect to each other, but there are no loops in the final “graph”
- Drag from one node in a direction to cycle between 0-2 bridges on that connection.
Tips
- Start from the outside going inwards
- Identify key islands which can reduce the amount of possible connections (for example a 7 in the middle of the field would have to at least singly connect to all neighbors)
- Discover the game (hints and a reveal can help you in case you’re really stuck)
Supported Zig versions
Zig Version 0.16.0
Raylib-Zig Version 6.0.0
EMSDK Version 4.0.9
AI / LLM usage disclosure
The only AI used in this project was Claude to quickly search through documentation.
Real Prompt I Used: In zig how would I make a pointer to different sizes of strings, and possibly null?
The code is purely my own handwriting. Not even zig fmt was used.
First Impressions on the Language
TL;DR: I liked the Centralization, File Organization, Debugging, and Simplicity of Zig Syntax.
I was frustrated by the Type-Casting, ‘raylib-zig’ changing API calls, weird differences with C syntax, and friction due to early language state.
What I Liked
Centralization
Having everything, package manager, build system, and compiler, in one spot definitely makes life easier. While I am very confident in my Makefile writing skills, not having to pull it out and instead just continue learning the language does keep the flow going towards learning the language.
My only complaint would be how weak the documentation of the build system is compared to everything else, especially for this project. What would be a simple matter of linking libraries and using emcc to build things, like here, turns into a mess of searching through what ‘raylib-zig’ has done, dealing with compiler errors in an unfamiliar tooling, and a lot of googling/llm-ing. Shout out this reply in a post for helping me out with a build issue I was having with emscripten.
Though, it does bother me that an equivalent makefile is half the size of the build scripts you need to write in zig. I reference this towards the example makefile I linked and the build.zig I needed to write for this project.
Intuitive/Freedom in File Organization
One of the main reasons I disliked Go-Lang wasn’t the syntax or error handling but the file management. Having to label things as “package” was confusing to me, and being forced to label public members with the first letter capitalized really bothered me. Here in zig, the default file organization is the same as how I’ve evolved to organize files in my personal projects. And public members just have pub attached to them, much clearer and leaves freedom for my shenanigans in naming or placing functions the way I want in files.
Debugging
Zig was able to catch an uninitialized error I hadn’t even noticed. I intentionally didn’t initialize a node so that the budget during generation would have room so I could bridge unconnected subgraphs later on. However, when I did connect them, some uninitialized data was getting used somewhere I hadn’t accounted for. Zig properly reported that, and I was able to fix that.
The equivalent error in C most likely would’ve been a day of deep gdb usage, followed by 12 printfs. In zig, it was maybe 5m and 0 prints.
Simplicity of Syntax
In basically a day of reading the language documentation I was up and running getting initial versions of this project working. Captures (|item|) are intuitive, though sometimes clunky. Several times the compiler complained to me about using old && instead of and, I think zig’s boolean operators are clearer than C’s it’s just that habits are hard to change, I do the same in Python.
A really really nice code technique Zig enables and I abused was:
for ([_]struct{arg1:type, arg2:type, ...}{.{.arg1=thing,.arg2=thing2, ...}) |args| ...
I avoided needing to make a helper function and was able to reduce code duplication a ton.
Though, I remain confused on whether or not I should inline the loop or not in this case.
Frustrations
Type-Casting, and @ built-ins
The main reason some features aren’t as polished as I’d like them to be is that I didn’t want to deal with type casting anymore. I was often trying to cast something that I thought was obvious and the compiler would complain.
For example: i32 = u32 + i32, an @intCast on the u32 is insufficient… you need an extra @as…
Or you want a negative number from i32 = u32 - u32 * u32, guess you have to add @as(i32, @intCast()) to all of those.
If you want an i16, better add @truncate to everything with an included @as. ![]()
The equivalent is a lot less typing in C: signed = (int)a - (int)b * (int)c; And while I know that I’m leaving a lot up to the compiler to truncate or cast or to simplify that I might incorrectly predict, at least I don’t have to deal with Zig’s compiler stopping me in my tracks and get confused on what it wants from me.
This could be solved by simply assuming you want the same type as the destination variable. That way you can just add the truncate and int cast builtins without clogging the line with as builtins unless you need to do specific type/bit casting before the final value.
Also, why does a division between two signed integers require builtins? Can we not have a default @divFloor when / is used?! I have never in my life needed a @divExact or @divTrunc, so why can’t you just default to divFloor?
I imagine this stems from the want to be as verbose as possible, or at least more pragmatic, but it ends up clogging code when you need to type-cast because the compiler doesn’t want to assume anything. It also makes it incredibly frustrating when you made an early on decision with the type of a variable, only to be punished for it later on when you realize different needs but still have many lines of code using previous type assumptions (namely unsigned).
I think I genuinely spent about 1-2 hours fighting with the compiler overall to get it accept my conversions since it also doesn’t have that great of error messages. Imagine my confusion when forgetting a type qualifer on variable definition (var dist = 0) returns me the error variable of type 'comptime_int' must be const or comptime. Huh? ![]()
And sometimes it isn’t even consistent, sometimes requiring a type-cast only when you change the value of a variable. Huh?
C Libraries and Raylib-Zig
For the first half of this project I simply used @cImport and corresponding @cInclude for raylib. With only an issue finding specific functions, which I could just declare myself with extern with mild confusion on why it was missing.
But when I wanted to use raygui, the build was reportedly having trouble finding raygui’s src directory. I didn’t really find any solution on the internet that wasn’t “Just use raylib-zig bro”, which is fair. And it was fairly simply using the package manager to include and it replace my previous build method.
That being said, and this is more a problem for the package maintainers than zig itself, why would you change every single function name, macro name, and scoping of macros? I don’t know if it’s because the package maintainers run zig fmt on it, but basically all of my previous raylib knowledge went out the door and I needed to find what function in raylib-zig was binded to the function I wanted from raylib. Especially macros for color and icons like WHITE and ICON_RESTART that are placed into scopes.
While not a show-stopper, and much less of an issue than Type-Casting, my only thought is “Why?”
What summoned you to change every name? It was wholly unnecessary.
The next time I want to use raylib with zig, I don’t think I’m going to use the binding. I’m just going to sit down and figure out how to fix the build error.
Weird C Differences
This is more a nitpick.
Like, this causes an error:
if (condition) break; // <- extra semi-colon causes else-sttmnt to disconnect
else continue; //would be fine in C, but not in Zig
Or sometimes my one-line for statements would cause a compiler complaint. Only to then fix themselves when I was doing something else.
Early Language State
A lot of my frustrations that I’ve outlined primarily stem from the fact that we aren’t in a 1.0/stable state yet. Like for example some conditional compiler complaints, or unclear compiler errors, or having to label blocks so that I can return values from them to variables assigned to them.
While not all friction is wholly negative, the borrow checker is why Rust is popular, I don’t think these frictions are helping with adoption.
Overall Thoughts
I am tepidly looking forward to the 1.0 release of this language. I liked programming in Zig (except for type casting), and the ease of adding a dependency was very nice. Unfortunately, I don’t really know if I’d just yet choose Zig over C right now even for personal projects.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.