Published on 2024-06-20
Discussions: Hacker News, /r/programming
In a previous article I've done the 'Hello, world!' of GUIs in assembly: A black window with a white text, using X11 without any libraries, just talking directly over a socket.
In a later article I've done the same with Wayland in C, displaying a static image.
I showed that this is not complex and results in a very lean and small application.
Recently, I stumbled upon this Hacker News post:
Microsoft's official Minesweeper app has ads, pay-to-win, and is hundreds of MBs
And I thought it would be fun to make with the same principles a full-fledged GUI application: the cult video game Minesweeper.
Will it be hundred of megabytes when we finish? How much work is it really? Can a hobbyist make this in a few hours?
Press enter to reset and press any mouse button to uncover the cell under the mouse cursor.
Here is a Youtube link in case the video does not play (I tried lots of things so that it plays on iOS to no avail).
The result is a ~300 KiB statically linked executable, that requires no libraries, and uses a constant ~1 MiB of resident heap memory (allocated at the start, to hold the assets). That's roughly a thousand times smaller in size than Microsoft's. And it only is a few hundred lines of code.
The advantage of this approach is that the application is tiny and stand-alone: statically linked with the few bits of libC it uses (and that's it), it can be trivially compiled on every Unix, and copied around, and it will work on every machine (with the same OS/architecture that is). Even on ancient Linuxes from 20 years ago.
I remember playing this game as a kid (must have been on Windows 98). It was a lot of fun! I don't exactly remember the rules though so it's a best approximation.
If you spot an error, please open a Github issue! And the source code repository for the game is here.
The 11th version of the X protocol was born in 1987 and has not changed since. Since it predates GPUs by a decade or so, its model does not really fit the hardware of today. Still, it's everywhere. Any Unix has a X server, even macOS with XQuartz, and now Windows supports running GUI Linux applications inside WSL! X11 has never been so ubiquitous. The protocol is relatively simple and the entry bar is low: we only need to create a socket and we're off the races. And for 2D applications, there's no need to be a Vulkan wizard or even interact with the GPU. Hell, it will work even without any GPU!
Everyone writing GUIs these days use a giant pile of libraries, starting with the overly complicated venerable libX11
and libxcb
libraries, to Qt and SDL.
Here are the steps we need to take:
And that's it. Spoiler alert: every step is 1-3 X11 messages that we need to craft and send. The only messages that we receive are the keyboard and mouse events. It's really not much at all!
We will implement this in the Odin programming language which I really enjoy. But if you want to follow along with C or anything really, go for it. All we need is to be able to open a Unix socket, send and receive data on it, and load an image into memory. We will use PNG for that, since Odin has in its standard library support for PNGs, but we could also very easily use a simple format like PPM (like I did in the linked Wayland article) that is trivial to parse. Since Odin has support for both in its standard library, it does not really matter, and I stuck with PNG since it's more space-efficient.
Finally, if you're into writing X11 applications even with libraries, lots of things in X11 are undocumented or underdocumented, and this article can be a good learning resource. As a bonus, you can also follow along with pure Wayland, using my previous Wayland article.
Or perhaps you simply enjoy, like me, peeking behind the curtain to understand the magician's tricks. It almost always ends up with: "That's it? That's all there is to it?".
In previous articles, we connected to the X server without any authentication.
Let's be a bit more refined: we now also support the X authentication protocol.
That's because when running under Wayland with XWayland in some desktop environments like Gnome, we have to use authentication.
This requires our application to read a 16 bytes long token that's present in a file in the user's home directory, and include it in the handshake we send to the X server.
This mechanism is called MIT-MAGIC-COOKIE-1
.
The catch is that this file contains multiple tokens for various authentication mechanisms, and network hosts. Remember, X11 is designed to work over the network. However we only care here about the entry for localhost.
So we need to parse a little bit. It's basically what libXau
does. From its docs:
Text1 The .Xauthority file is a binary file consisting of a sequence of entries 2 in the following format: 3 2 bytes Family value (second byte is as in protocol HOST) 4 2 bytes address length (always MSB first) 5 A bytes host address (as in protocol HOST) 6 2 bytes display "number" length (always MSB first) 7 S bytes display "number" string 8 2 bytes name length (always MSB first) 9 N bytes authorization name string 10 2 bytes data length (always MSB first) 11 D bytes authorization data string
First let's define some types and constants:
Odin1 AUTH_ENTRY_FAMILY_LOCAL: u16 : 1
2 AUTH_ENTRY_MAGIC_COOKIE: string : "MIT-MAGIC-COOKIE-1"
3
4 AuthToken :: [16]u8
5
6 AuthEntry :: struct {
7 family: u16,
8 auth_name: []u8,
9 auth_data: []u8,
10 }
We only define fields we are interested in.
Let's now parse each entry accordingly:
Odin1 read_x11_auth_entry :: proc(buffer: ^bytes.Buffer) -> (AuthEntry, bool) {
2 entry := AuthEntry{}
3
4 {
5 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&entry.family))
6 if err == .EOF {return {}, false}
7
8 assert(err == .None)
9 assert(n_read == size_of(entry.family))
10 }
11
12 address_len: u16 = 0
13 {
14 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&address_len))
15 assert(err == .None)
16
17 address_len = bits.byte_swap(address_len)
18 assert(n_read == size_of(address_len))
19 }
20
21 address := make([]u8, address_len)
22 {
23 n_read, err := bytes.buffer_read(buffer, address)
24 assert(err == .None)
25 assert(n_read == cast(int)address_len)
26 }
27
28 display_number_len: u16 = 0
29 {
30 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&display_number_len))
31 assert(err == .None)
32
33 display_number_len = bits.byte_swap(display_number_len)
34 assert(n_read == size_of(display_number_len))
35 }
36
37 display_number := make([]u8, display_number_len)
38 {
39 n_read, err := bytes.buffer_read(buffer, display_number)
40 assert(err == .None)
41 assert(n_read == cast(int)display_number_len)
42 }
43
44 auth_name_len: u16 = 0
45 {
46 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_name_len))
47 assert(err == .None)
48
49 auth_name_len = bits.byte_swap(auth_name_len)
50 assert(n_read == size_of(auth_name_len))
51 }
52
53 entry.auth_name = make([]u8, auth_name_len)
54 {
55 n_read, err := bytes.buffer_read(buffer, entry.auth_name)
56 assert(err == .None)
57 assert(n_read == cast(int)auth_name_len)
58 }
59
60 auth_data_len: u16 = 0
61 {
62 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_data_len))
63 assert(err == .None)
64
65 auth_data_len = bits.byte_swap(auth_data_len)
66 assert(n_read == size_of(auth_data_len))
67 }
68
69 entry.auth_data = make([]u8, auth_data_len)
70 {
71 n_read, err := bytes.buffer_read(buffer, entry.auth_data)
72 assert(err == .None)
73 assert(n_read == cast(int)auth_data_len)
74 }
75
76 return entry, true
77 }
Now we can sift through the different entries in the file to find the one we are after:
Odin1 load_x11_auth_token :: proc(allocator := context.allocator) -> (token: AuthToken, ok: bool) {
2 context.allocator = allocator
3 defer free_all(allocator)
4
5 filename_env := os.get_env("XAUTHORITY")
6
7 filename :=
8 len(filename_env) != 0 \
9 ? filename_env \
10 : filepath.join([]string{os.get_env("HOME"), ".Xauthority"})
11
12 data := os.read_entire_file_from_filename(filename) or_return
13
14 buffer := bytes.Buffer{}
15 bytes.buffer_init(&buffer, data[:])
16
17
18 for {
19 auth_entry := read_x11_auth_entry(&buffer) or_break
20
21 if auth_entry.family == AUTH_ENTRY_FAMILY_LOCAL &&
22 slice.equal(auth_entry.auth_name, transmute([]u8)AUTH_ENTRY_MAGIC_COOKIE) &&
23 len(auth_entry.auth_data) == size_of(AuthToken) {
24
25 mem.copy_non_overlapping(
26 raw_data(&token),
27 raw_data(auth_entry.auth_data),
28 size_of(AuthToken),
29 )
30 return token, true
31 }
32 }
33
34 // Did not find a fitting token.
35 return {}, false
36 }
Odin has a nice shorthand to return early on errors: or_return
, which is the equivalent of ?
in Rust or try
in Zig. Same thing with or_break
.
And we use it in this manner in main
:
Odin1 main :: proc() {
2 auth_token, _ := load_x11_auth_token(context.temp_allocator)
3 }
If we did not find a fitting token, no matter, we will simply carry on with an empty one.
One interesting thing: in Odin, similarly to Zig, allocators are passed to functions wishing to allocate memory. Contrary to Zig though, Odin has a mechanism to make that less tedious (and more implicit as a result) by essentially passing the allocator as the last function argument which is optional.
Odin is nice enough to also provide us two allocators that we can use right away: A general purpose allocator, and a temporary allocator that uses an arena.
Since authentication entries can be large, we have to allocate - the stack is only so big. It would be unfortunate to stack overflow because a hostname is a tiny bit too long in this file.
Some readers have pointed out that it is likely it would all fit on the stack here, but this was also a perfect opportunity to describe Odin's approach to memory management.
However, we do not want to retain the parsed entries from the file in memory after finding the 16 bytes token, so we defer free_all(allocator)
. This is much better than going through each entry and freeing individually each field. We simply free the whole arena in one swoop (but the backing memory remains around to be reused later).
Furthermore, using this arena places an upper bound (a few MiBs) on the allocations we can do. So if one entry in the file is huge, or malformed, we verifyingly cannot allocate many GiBs of memory. This is good news, because otherwise, the OS will start swapping like crazy and start killing random programs. In my experience it usually kills the window/desktop manager which kills all open windows. Very efficient from the OS perspective, and awful from the user perspective. So it's always good to place an upper bound on all resources including heap memory usage of your program.
All in all I find Odin's approach very elegant. I usually want the ability to use a different allocator in a given function, but also if I don't care, it will do the right thing and use the standard allocator.
This part is almost exactly the same as the first linked article so I'll speed run this.
First we open a UNIX domain socket:
Odin1 connect_x11_socket :: proc() -> os.Socket {
2 SockaddrUn :: struct #packed {
3 sa_family: os.ADDRESS_FAMILY,
4 sa_data: [108]u8,
5 }
6
7 socket, err := os.socket(os.AF_UNIX, os.SOCK_STREAM, 0)
8 assert(err == os.ERROR_NONE)
9
10 possible_socket_paths := [2]string{"/tmp/.X11-unix/X0", "/tmp/.X11-unix/X1"}
11 for &socket_path in possible_socket_paths {
12 addr := SockaddrUn {
13 sa_family = cast(u16)os.AF_UNIX,
14 }
15 mem.copy_non_overlapping(&addr.sa_data, raw_data(socket_path), len(socket_path))
16
17 err = os.connect(socket, cast(^os.SOCKADDR)&addr, size_of(addr))
18 if (err == os.ERROR_NONE) {return socket}
19 }
20
21 os.exit(1)
22 }
We try a few possible paths for the socket, that can vary a bit from distribution to distribution.
We now can send the handshake, and receive general information from the server. Let's define some structs for that per the X11 protocol:
Odin1 Screen :: struct #packed {
2 id: u32,
3 colormap: u32,
4 white: u32,
5 black: u32,
6 input_mask: u32,
7 width: u16,
8 height: u16,
9 width_mm: u16,
10 height_mm: u16,
11 maps_min: u16,
12 maps_max: u16,
13 root_visual_id: u32,
14 backing_store: u8,
15 save_unders: u8,
16 root_depth: u8,
17 depths_count: u8,
18 }
19
20 ConnectionInformation :: struct {
21 root_screen: Screen,
22 resource_id_base: u32,
23 resource_id_mask: u32,
24 }
The structs are #packed
to match the network protocol format, otherwise the compiler may insert padding between fields.
One thing to know about X11: Everything we send has to be padded to a multiple of 4 bytes. We define a helper to do that by using the formula ((i32)x + 3) & -4
along with a unit test for good measure:
Odin1 round_up_4 :: #force_inline proc(x: u32) -> u32 {
2 mask: i32 = -4
3 return transmute(u32)((transmute(i32)x + 3) & mask)
4 }
5
6 @(test)
7 test_round_up_4 :: proc(_: ^testing.T) {
8 assert(round_up_4(0) == 0)
9 assert(round_up_4(1) == 4)
10 assert(round_up_4(2) == 4)
11 assert(round_up_4(3) == 4)
12 assert(round_up_4(4) == 4)
13 assert(round_up_4(5) == 8)
14 assert(round_up_4(6) == 8)
15 assert(round_up_4(7) == 8)
16 assert(round_up_4(8) == 8)
17 }
We can now send the handshake with the authentication token inside. We leverage the writev
system call to send multiple separate buffers of different lengths in one call.
We skip over most of the information the server sends us, since we only are after a few fields:
Odin1 x11_handshake :: proc(socket: os.Socket, auth_token: ^AuthToken) -> ConnectionInformation {
2 Request :: struct #packed {
3 endianness: u8,
4 pad1: u8,
5 major_version: u16,
6 minor_version: u16,
7 authorization_len: u16,
8 authorization_data_len: u16,
9 pad2: u16,
10 }
11
12 request := Request {
13 endianness = 'l',
14 major_version = 11,
15 authorization_len = len(AUTH_ENTRY_MAGIC_COOKIE),
16 authorization_data_len = size_of(AuthToken),
17 }
18
19
20 {
21 padding := [2]u8{0, 0}
22 n_sent, err := linux.writev(
23 cast(linux.Fd)socket,
24 []linux.IO_Vec {
25 {base = &request, len = size_of(Request)},
26 {base = raw_data(AUTH_ENTRY_MAGIC_COOKIE), len = len(AUTH_ENTRY_MAGIC_COOKIE)},
27 {base = raw_data(padding[:]), len = len(padding)},
28 {base = raw_data(auth_token[:]), len = len(auth_token)},
29 },
30 )
31 assert(err == .NONE)
32 assert(
33 n_sent ==
34 size_of(Request) + len(AUTH_ENTRY_MAGIC_COOKIE) + len(padding) + len(auth_token),
35 )
36 }
37
38 StaticResponse :: struct #packed {
39 success: u8,
40 pad1: u8,
41 major_version: u16,
42 minor_version: u16,
43 length: u16,
44 }
45
46 static_response := StaticResponse{}
47 {
48 n_recv, err := os.recv(socket, mem.ptr_to_bytes(&static_response), 0)
49 assert(err == os.ERROR_NONE)
50 assert(n_recv == size_of(StaticResponse))
51 assert(static_response.success == 1)
52 }
53
54
55 recv_buf: [1 << 15]u8 = {}
56 {
57 assert(len(recv_buf) >= cast(u32)static_response.length * 4)
58
59 n_recv, err := os.recv(socket, recv_buf[:], 0)
60 assert(err == os.ERROR_NONE)
61 assert(n_recv == cast(u32)static_response.length * 4)
62 }
63
64
65 DynamicResponse :: struct #packed {
66 release_number: u32,
67 resource_id_base: u32,
68 resource_id_mask: u32,
69 motion_buffer_size: u32,
70 vendor_length: u16,
71 maximum_request_length: u16,
72 screens_in_root_count: u8,
73 formats_count: u8,
74 image_byte_order: u8,
75 bitmap_format_bit_order: u8,
76 bitmap_format_scanline_unit: u8,
77 bitmap_format_scanline_pad: u8,
78 min_keycode: u8,
79 max_keycode: u8,
80 pad2: u32,
81 }
82
83 read_buffer := bytes.Buffer{}
84 bytes.buffer_init(&read_buffer, recv_buf[:])
85
86 dynamic_response := DynamicResponse{}
87 {
88 n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&dynamic_response))
89 assert(err == .None)
90 assert(n_read == size_of(DynamicResponse))
91 }
92
93
94 // Skip over the vendor information.
95 bytes.buffer_next(&read_buffer, cast(int)round_up_4(cast(u32)dynamic_response.vendor_length))
96 // Skip over the format information (each 8 bytes long).
97 bytes.buffer_next(&read_buffer, 8 * cast(int)dynamic_response.formats_count)
98
99 screen := Screen{}
100 {
101 n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&screen))
102 assert(err == .None)
103 assert(n_read == size_of(screen))
104 }
105
106 return(
107 ConnectionInformation {
108 resource_id_base = dynamic_response.resource_id_base,
109 resource_id_mask = dynamic_response.resource_id_mask,
110 root_screen = screen,
111 } \
112 )
113 }
Our main
now becomes:
Odin1 main :: proc() {
2 auth_token, _ := load_x11_auth_token(context.temp_allocator)
3 socket := connect_x11_socket()
4 connection_information := x11_handshake(socket, &auth_token)
5 }
The next step is to create a graphical context. When creating a new entity, we generate an id for it, and send that in the create request. Afterwards, we can refer to the entity by this id:
Odin1 next_x11_id :: proc(current_id: u32, info: ConnectionInformation) -> u32 {
2 return 1 + ((info.resource_id_mask & (current_id)) | info.resource_id_base)
3 }
Time to create a graphical context:
Odin1 x11_create_graphical_context :: proc(socket: os.Socket, gc_id: u32, root_id: u32) {
2 opcode: u8 : 55
3 FLAG_GC_BG: u32 : 8
4 BITMASK: u32 : FLAG_GC_BG
5 VALUE1: u32 : 0x00_00_ff_00
6
7 Request :: struct #packed {
8 opcode: u8,
9 pad1: u8,
10 length: u16,
11 id: u32,
12 drawable: u32,
13 bitmask: u32,
14 value1: u32,
15 }
16 request := Request {
17 opcode = opcode,
18 length = 5,
19 id = gc_id,
20 drawable = root_id,
21 bitmask = BITMASK,
22 value1 = VALUE1,
23 }
24
25 {
26 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
27 assert(err == os.ERROR_NONE)
28 assert(n_sent == size_of(Request))
29 }
30 }
Finally we create a window. We subscribe to a few events as well:
Exposure
: when our window becomes visibleKEY_PRESS
: when a keyboard key is pressedKEY_RELEASE
: when a keyboard key is releasedBUTTON_PRESS
: when a mouse button is pressedBUTTON_RELEASE
: when a mouse button is releasedWe also pick an arbitrary background color, yellow. It does not matter because we will always cover every part of the window with our assets.
Odin1 x11_create_window :: proc(
2 socket: os.Socket,
3 window_id: u32,
4 parent_id: u32,
5 x: u16,
6 y: u16,
7 width: u16,
8 height: u16,
9 root_visual_id: u32,
10 ) {
11 FLAG_WIN_BG_PIXEL: u32 : 2
12 FLAG_WIN_EVENT: u32 : 0x800
13 FLAG_COUNT: u16 : 2
14 EVENT_FLAG_EXPOSURE: u32 = 0x80_00
15 EVENT_FLAG_KEY_PRESS: u32 = 0x1
16 EVENT_FLAG_KEY_RELEASE: u32 = 0x2
17 EVENT_FLAG_BUTTON_PRESS: u32 = 0x4
18 EVENT_FLAG_BUTTON_RELEASE: u32 = 0x8
19 flags: u32 : FLAG_WIN_BG_PIXEL | FLAG_WIN_EVENT
20 depth: u8 : 24
21 border_width: u16 : 0
22 CLASS_INPUT_OUTPUT: u16 : 1
23 opcode: u8 : 1
24 BACKGROUND_PIXEL_COLOR: u32 : 0x00_ff_ff_00
25
26 Request :: struct #packed {
27 opcode: u8,
28 depth: u8,
29 request_length: u16,
30 window_id: u32,
31 parent_id: u32,
32 x: u16,
33 y: u16,
34 width: u16,
35 height: u16,
36 border_width: u16,
37 class: u16,
38 root_visual_id: u32,
39 bitmask: u32,
40 value1: u32,
41 value2: u32,
42 }
43 request := Request {
44 opcode = opcode,
45 depth = depth,
46 request_length = 8 + FLAG_COUNT,
47 window_id = window_id,
48 parent_id = parent_id,
49 x = x,
50 y = y,
51 width = width,
52 height = height,
53 border_width = border_width,
54 class = CLASS_INPUT_OUTPUT,
55 root_visual_id = root_visual_id,
56 bitmask = flags,
57 value1 = BACKGROUND_PIXEL_COLOR,
58 value2 = EVENT_FLAG_EXPOSURE | EVENT_FLAG_BUTTON_RELEASE | EVENT_FLAG_BUTTON_PRESS | EVENT_FLAG_KEY_PRESS | EVENT_FLAG_KEY_RELEASE,
59 }
60
61 {
62 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
63 assert(err == os.ERROR_NONE)
64 assert(n_sent == size_of(Request))
65 }
66 }
We decide that our game will have 16 rows and 16 columns, and each asset is 16x16 pixels.
main
is now:
Odin1 ENTITIES_ROW_COUNT :: 16
2 ENTITIES_COLUMN_COUNT :: 16
3 ENTITIES_WIDTH :: 16
4 ENTITIES_HEIGHT :: 16
5
6 main :: proc() {
7 auth_token, _ := load_x11_auth_token(context.temp_allocator)
8 socket := connect_x11_socket()
9 connection_information := x11_handshake(socket, &auth_token)
10
11 gc_id := next_x11_id(0, connection_information)
12 x11_create_graphical_context(socket, gc_id, connection_information.root_screen.id)
13
14 window_id := next_x11_id(gc_id, connection_information)
15 x11_create_window(
16 socket,
17 window_id,
18 connection_information.root_screen.id,
19 200,
20 200,
21 ENTITIES_COLUMN_COUNT * ENTITIES_WIDTH,
22 ENTITIES_ROW_COUNT * ENTITIES_HEIGHT,
23 connection_information.root_screen.root_visual_id,
24 )
25 }
Note that the window dimensions are a hint, they might now be respected, for example in a tiling window manager. We do not handle this case here since the assets are fixed size.
If you have followed along, you will now see... nothing. That's because we need to tell X11 to show our window with the map_window
call:
Odin1 x11_map_window :: proc(socket: os.Socket, window_id: u32) {
2 opcode: u8 : 8
3
4 Request :: struct #packed {
5 opcode: u8,
6 pad1: u8,
7 request_length: u16,
8 window_id: u32,
9 }
10 request := Request {
11 opcode = opcode,
12 request_length = 2,
13 window_id = window_id,
14 }
15 {
16 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
17 assert(err == os.ERROR_NONE)
18 assert(n_sent == size_of(Request))
19 }
20
21 }
We now see:
Time to start programming the game itself!
What's a game without nice looking pictures stolen from somewhere on the internet ?
Here is our sprite, the one image containing all our assets:
Odin has a nice feature to embed the image file in our executable which makes redistribution a breeze and startup a bit faster, so we'll do that:
Odin1 png_data := #load("sprite.png")
2 sprite, err := png.load_from_bytes(png_data, {})
3 assert(err == nil)
Now here is the catch: The X11 image format is different from the one in the sprite so we have to swap the bytes around:
Odin1 sprite_data := make([]u8, sprite.height * sprite.width * 4)
2
3 // Convert the image format from the sprite (RGB) into the X11 image format (BGRX).
4 for i := 0; i < sprite.height * sprite.width - 3; i += 1 {
5 sprite_data[i * 4 + 0] = sprite.pixels.buf[i * 3 + 2] // R -> B
6 sprite_data[i * 4 + 1] = sprite.pixels.buf[i * 3 + 1] // G -> G
7 sprite_data[i * 4 + 2] = sprite.pixels.buf[i * 3 + 0] // B -> R
8 sprite_data[i * 4 + 3] = 0 // pad
9 }
The A
component is actually unused since we do not have transparency.
Now that our image is in (client) memory, how to make it available to the server? Which, again, in the X11 model, might be running on a totally different machine across the world!
X11 has 3 useful calls for images: CreatePixmap
and PutImage
. A Pixmap
is an off-screen image buffer. PutImage
uploads image data either to a pixmap or to the window directly (a 'drawable' in X11 parlance). CopyArea
copies one rectangle in one drawable to another drawable.
In my humble opinion, these are complete misnomers. CreatePixmap
should have been called CreateOffscreenImageBuffer
and PutImage
should have been UploadImageData
. CopyArea
: you're fine buddy, carry on.
We cannot simply use PutImage
here since that would show the whole sprite on the screen (there are no fields to specify that only part of the image should be displayed). We could show only parts of it, with separate PutImage
calls for each entity, but that would mean uploading the image data to the server each time.
What we want is to upload the image data once, off-screen, with one PutImage
call, and then copy parts of it onto the window. Here is the dance we need to do:
CreatePixmap
PutImage
to upload the image data to the pixmap - at that point nothing is shown on the window, everything is still off-screenCopyArea
call which copies parts of the pixmap onto the window - now it's visible!The X server can actually upload the image data to the GPU on a PutImage
call (this is implementation dependent). After that, CopyArea
calls can be translated by the X server to GPU commands to copy the image data from one GPU buffer to another: that's really performant! The image data is only uploaded once to the GPU and then resides there for the remainder of the program.
Unfortunately, the X standard does not enforce that (it says: "may or may not [...]"), but that's a useful model to have in mind.
Another useful model is to think of what happens when the X server is running across the network: We only want to send the image data once because that's time-consuming, and afterwards issue cheap CopyArea
commands that are only a few bytes each.
Ok, let's implement that then:
Odin1 x11_create_pixmap :: proc(
2 socket: os.Socket,
3 window_id: u32,
4 pixmap_id: u32,
5 width: u16,
6 height: u16,
7 depth: u8,
8 ) {
9 opcode: u8 : 53
10
11 Request :: struct #packed {
12 opcode: u8,
13 depth: u8,
14 request_length: u16,
15 pixmap_id: u32,
16 drawable_id: u32,
17 width: u16,
18 height: u16,
19 }
20
21 request := Request {
22 opcode = opcode,
23 depth = depth,
24 request_length = 4,
25 pixmap_id = pixmap_id,
26 drawable_id = window_id,
27 width = width,
28 height = height,
29 }
30
31 {
32 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
33 assert(err == os.ERROR_NONE)
34 assert(n_sent == size_of(Request))
35 }
36 }
37
38 x11_put_image :: proc(
39 socket: os.Socket,
40 drawable_id: u32,
41 gc_id: u32,
42 width: u16,
43 height: u16,
44 dst_x: u16,
45 dst_y: u16,
46 depth: u8,
47 data: []u8,
48 ) {
49 opcode: u8 : 72
50
51 Request :: struct #packed {
52 opcode: u8,
53 format: u8,
54 request_length: u16,
55 drawable_id: u32,
56 gc_id: u32,
57 width: u16,
58 height: u16,
59 dst_x: u16,
60 dst_y: u16,
61 left_pad: u8,
62 depth: u8,
63 pad1: u16,
64 }
65
66 data_length_padded := round_up_4(cast(u32)len(data))
67
68 request := Request {
69 opcode = opcode,
70 format = 2, // ZPixmap
71 request_length = cast(u16)(6 + data_length_padded / 4),
72 drawable_id = drawable_id,
73 gc_id = gc_id,
74 width = width,
75 height = height,
76 dst_x = dst_x,
77 dst_y = dst_y,
78 depth = depth,
79 }
80 {
81 padding_len := data_length_padded - cast(u32)len(data)
82
83 n_sent, err := linux.writev(
84 cast(linux.Fd)socket,
85 []linux.IO_Vec {
86 {base = &request, len = size_of(Request)},
87 {base = raw_data(data), len = len(data)},
88 {base = raw_data(data), len = cast(uint)padding_len},
89 },
90 )
91 assert(err == .NONE)
92 assert(n_sent == size_of(Request) + len(data) + cast(int)padding_len)
93 }
94 }
95
96 x11_copy_area :: proc(
97 socket: os.Socket,
98 src_id: u32,
99 dst_id: u32,
100 gc_id: u32,
101 src_x: u16,
102 src_y: u16,
103 dst_x: u16,
104 dst_y: u16,
105 width: u16,
106 height: u16,
107 ) {
108 opcode: u8 : 62
109 Request :: struct #packed {
110 opcode: u8,
111 pad1: u8,
112 request_length: u16,
113 src_id: u32,
114 dst_id: u32,
115 gc_id: u32,
116 src_x: u16,
117 src_y: u16,
118 dst_x: u16,
119 dst_y: u16,
120 width: u16,
121 height: u16,
122 }
123
124 request := Request {
125 opcode = opcode,
126 request_length = 7,
127 src_id = src_id,
128 dst_id = dst_id,
129 gc_id = gc_id,
130 src_x = src_x,
131 src_y = src_y,
132 dst_x = dst_x,
133 dst_y = dst_y,
134 width = width,
135 height = height,
136 }
137 {
138 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
139 assert(err == os.ERROR_NONE)
140 assert(n_sent == size_of(Request))
141 }
142 }
Let's try in main
:
Odin1 img_depth: u8 = 24
2 pixmap_id := next_x11_id(window_id, connection_information)
3 x11_create_pixmap(
4 socket,
5 window_id,
6 pixmap_id,
7 cast(u16)sprite.width,
8 cast(u16)sprite.height,
9 img_depth,
10 )
11
12 x11_put_image(
13 socket,
14 pixmap_id,
15 gc_id,
16 sprite_width,
17 sprite_height,
18 0,
19 0,
20 img_depth,
21 sprite_data,
22 )
23
24 // Let's render two different assets: an exploded mine and an idle mine.
25 x11_copy_area(
26 socket,
27 pixmap_id,
28 window_id,
29 gc_id,
30 32, // X coordinate on the sprite sheet.
31 40, // Y coordinate on the sprite sheet.
32 0, // X coordinate on the window.
33 0, // Y coordinate on the window.
34 16, // Width.
35 16, // Height.
36 )
37 x11_copy_area(
38 socket,
39 pixmap_id,
40 window_id,
41 gc_id,
42 64,
43 40,
44 16,
45 0,
46 16,
47 16,
48 )
Result:
We are now ready to focus on the game entities.
We have a few different entities we want to show, each is a 16x16 section of the sprite sheet. Let's define their coordinates to be readable:
Odin1 Position :: struct {
2 x: u16,
3 y: u16,
4 }
5
6 Entity_kind :: enum {
7 Covered,
8 Uncovered_0,
9 Uncovered_1,
10 Uncovered_2,
11 Uncovered_3,
12 Uncovered_4,
13 Uncovered_5,
14 Uncovered_6,
15 Uncovered_7,
16 Uncovered_8,
17 Mine_exploded,
18 Mine_idle,
19 }
20
21 ASSET_COORDINATES: [Entity_kind]Position = {
22 .Uncovered_0 = {x = 0 * 16, y = 22},
23 .Uncovered_1 = {x = 1 * 16, y = 22},
24 .Uncovered_2 = {x = 2 * 16, y = 22},
25 .Uncovered_3 = {x = 3 * 16, y = 22},
26 .Uncovered_4 = {x = 4 * 16, y = 22},
27 .Uncovered_5 = {x = 5 * 16, y = 22},
28 .Uncovered_6 = {x = 6 * 16, y = 22},
29 .Uncovered_7 = {x = 7 * 16, y = 22},
30 .Uncovered_8 = {x = 8 * 16, y = 22},
31 .Covered = {x = 0, y = 38},
32 .Mine_exploded = {x = 32, y = 40},
33 .Mine_idle = {x = 64, y = 40},
34 }
And we'll group everything we need in one struct called Scene
:
Odin1 Scene :: struct {
2 window_id: u32,
3 gc_id: u32,
4 sprite_pixmap_id: u32,
5 displayed_entities: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]Entity_kind,
6 mines: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
7 }
The first interesting field is displayed_entities
which keeps track of which assets are shown. For example, a mine is either covered, uncovered and exploded if the player clicked on it, or uncovered and idle if the player won).
The second one is mines
which simply keeps track of where mines are. It could be a bitfield to optimize space but I did not bother.
In main
we create a new scene and plant mines randomly.
Odin1 scene := Scene { 2 window_id = window_id, 3 gc_id = gc_id, 4 sprite_pixmap_id = pixmap_id, 5 } 6 reset(&scene)
We put this logic in the reset
helper so that the player can easily restart the game with one keystroke:
Odin1 reset :: proc(scene: ^Scene) {
2 for &entity in scene.displayed_entities {
3 entity = .Covered
4 }
5
6 for &mine in scene.mines {
7 mine = rand.choice([]bool{true, false, false, false})
8 }
9 }
Here I used a 1/4 chance that a cell has a mine.
We are now ready to render our (static for now) scene:
Odin1 render :: proc(socket: os.Socket, scene: ^Scene) {
2 for entity, i in scene.displayed_entities {
3 rect := ASSET_COORDINATES[entity]
4 row, column := idx_to_row_column(i)
5
6 x11_copy_area(
7 socket,
8 scene.sprite_pixmap_id,
9 scene.window_id,
10 scene.gc_id,
11 rect.x,
12 rect.y,
13 cast(u16)column * ENTITIES_WIDTH,
14 cast(u16)row * ENTITIES_HEIGHT,
15 ENTITIES_WIDTH,
16 ENTITIES_HEIGHT,
17 )
18 }
19 }
And here is what we get:
The next step is to respond to events.
This is very straightforward. Since the only messages we expect are for keyboard and mouse events, with a fixed size of 32 bytes, we simply read 32 bytes exactly in a blocking fashion. The first byte indicates which kind of event it is:
Odin1 wait_for_x11_events :: proc(socket: os.Socket, scene: ^Scene) {
2 GenericEvent :: struct #packed {
3 code: u8,
4 pad: [31]u8,
5 }
6 assert(size_of(GenericEvent) == 32)
7
8 KeyReleaseEvent :: struct #packed {
9 code: u8,
10 detail: u8,
11 sequence_number: u16,
12 time: u32,
13 root_id: u32,
14 event: u32,
15 child_id: u32,
16 root_x: u16,
17 root_y: u16,
18 event_x: u16,
19 event_y: u16,
20 state: u16,
21 same_screen: bool,
22 pad1: u8,
23 }
24 assert(size_of(KeyReleaseEvent) == 32)
25
26 ButtonReleaseEvent :: struct #packed {
27 code: u8,
28 detail: u8,
29 seq_number: u16,
30 timestamp: u32,
31 root: u32,
32 event: u32,
33 child: u32,
34 root_x: u16,
35 root_y: u16,
36 event_x: u16,
37 event_y: u16,
38 state: u16,
39 same_screen: bool,
40 pad1: u8,
41 }
42 assert(size_of(ButtonReleaseEvent) == 32)
43
44 EVENT_EXPOSURE: u8 : 0xc
45 EVENT_KEY_RELEASE: u8 : 0x3
46 EVENT_BUTTON_RELEASE: u8 : 0x5
47
48 KEYCODE_ENTER: u8 : 36
49
50 for {
51 generic_event := GenericEvent{}
52 n_recv, err := os.recv(socket, mem.ptr_to_bytes(&generic_event), 0)
53 if err == os.EPIPE || n_recv == 0 {
54 os.exit(0) // The end.
55 }
56
57 assert(err == os.ERROR_NONE)
58 assert(n_recv == size_of(GenericEvent))
59
60 switch generic_event.code {
61 case EVENT_EXPOSURE:
62 render(socket, scene)
63
64 case EVENT_KEY_RELEASE:
65 event := transmute(KeyReleaseEvent)generic_event
66 if event.detail == KEYCODE_ENTER {
67 reset(scene)
68 render(socket, scene)
69 }
70
71 case EVENT_BUTTON_RELEASE:
72 event := transmute(ButtonReleaseEvent)generic_event
73 on_cell_clicked(event.event_x, event.event_y, scene)
74 render(socket, scene)
75 }
76 }
77 }
If the event is Exposed
, we simply render (that's our first render when the window becomes visible - or if the window was minimized and then made visible again).
If the event is the Enter
key, we reset the state of the game and render. X11 differentiates between physical and logical keys on the keyboard but that does not matter here (or I would argue in most games: we are interested in the physical location of the key, not what the user mapped it to).
If the event is (pressing and) releasing a mouse button, we run the game logic to uncover a cell and render.
That's it!
The last thing to do is implementing the game rules.
From my faint memory, when uncovering a cell, we have two cases:
The one thing that tripped me is that we inspect all 8 neighboring cells to count mines, but when doing the flood fill, we only visit the 4 neighboring cells: up, right, down, left - not the diagonal neighbors. Otherwise the flood fill ends up uncovering all cells in the game at once.
First, we need to translate the mouse position in the window to a cell index/row/column in our grid:
Odin1 row_column_to_idx :: #force_inline proc(row: int, column: int) -> int {
2 return cast(int)row * ENTITIES_COLUMN_COUNT + cast(int)column
3 }
4
5 locate_entity_by_coordinate :: proc(win_x: u16, win_y: u16) -> (idx: int, row: int, column: int) {
6 column = cast(int)win_x / ENTITIES_WIDTH
7 row = cast(int)win_y / ENTITIES_HEIGHT
8
9 idx = row_column_to_idx(row, column)
10
11 return idx, row, column
12 }
Then the game logic:
Odin1 on_cell_clicked :: proc(x: u16, y: u16, scene: ^Scene) {
2 idx, row, column := locate_entity_by_coordinate(x, y)
3
4 mined := scene.mines[idx]
5
6 if mined {
7 scene.displayed_entities[idx] = .Mine_exploded
8 // Lose.
9 uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_exploded)
10 } else {
11 visited := [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool{}
12 uncover_cells_flood_fill(row, column, &scene.displayed_entities, &scene.mines, &visited)
13
14 // Win.
15 if count_remaining_goals(scene.displayed_entities, scene.mines) == 0 {
16 uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_idle)
17 }
18 }
19 }
The objective is to uncover all cells without mines. We could keep a counter around and decrement it each time, but I wanted to make it idiot-proof, so I simply scan the grid to count how many uncovered cells without a mine underneath remain (in count_remaining_goals
). No risk that way to have a desync between the game state and what is shown on the screen, because we did not decrement the counter in one edge case.
uncover_all_cells
unconditionally reveals the whole grid when the player won or lost. We just need to show the mines exploded when they lost, and idle when they won.
uncover_cells_flood_fill
is the interesting one. We use recursion, and to avoid visiting the same cells multiple times and potentially getting into infinite recursion, we track which cells were visited:
Odin1 uncover_cells_flood_fill :: proc(
2 row: int,
3 column: int,
4 displayed_entities: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
5 mines: ^[ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
6 visited: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool,
7 ) {
8 i := row_column_to_idx(row, column)
9 if visited[i] {return}
10
11 visited[i] = true
12
13 // Do not uncover covered mines.
14 if mines[i] {return}
15
16 if displayed_entities[i] != .Covered {return}
17
18 // Uncover cell.
19
20 mines_around_count := count_mines_around_cell(row, column, mines[:])
21 assert(mines_around_count <= 8)
22
23 displayed_entities[i] =
24 cast(Entity_kind)(cast(int)Entity_kind.Uncovered_0 + mines_around_count)
25
26 // Uncover neighbors.
27
28 // Up.
29 if !(row == 0) {
30 uncover_cells_flood_fill(row - 1, column, displayed_entities, mines, visited)
31 }
32
33 // Right
34 if !(column == (ENTITIES_COLUMN_COUNT - 1)) {
35 uncover_cells_flood_fill(row, column + 1, displayed_entities, mines, visited)
36 }
37
38 // Bottom.
39 if !(row == (ENTITIES_ROW_COUNT - 1)) {
40 uncover_cells_flood_fill(row + 1, column, displayed_entities, mines, visited)
41 }
42
43 // Left.
44 if !(column == 0) {
45 uncover_cells_flood_fill(row, column - 1, displayed_entities, mines, visited)
46 }
47 }
There are a few helpers here and there that are simple, but otherwise... that's it, that's the end. We're done! All under 1000 lines of code without any tricks or clever things.
X11 is old and crufty, but also gets out of the way. Once a few utility functions to open the window, receive events, etc have been implemented, it can be forgotten and we can focus all our attention on the game. That's very valuable. How many libraries, frameworks and development environments can say the same?
I also enjoy that it works with any programming language, any tech stack. Don't need no bindings, no FFI, just send some bytes over the socket. You can even do that in Bash (don't tempt me!).
I did not implement a few accessory things from the original game, like planting a flag on a cell you suspect has a mine. Feel free to do this at home, it's not much work.
Finally, give Odin a try, it's great! It's this weird mix of a sane C with a Go-ish syntax and a good standard library.
I hope that you had as much fun as I did!
Odin1 package main
2
3 import "core:bytes"
4 import "core:image/png"
5 import "core:math/bits"
6 import "core:math/rand"
7 import "core:mem"
8 import "core:os"
9 import "core:path/filepath"
10 import "core:slice"
11 import "core:sys/linux"
12 import "core:testing"
13
14 TILE_WIDTH :: 16
15 TILE_HEIGHT :: 16
16
17 Position :: struct {
18 x: u16,
19 y: u16,
20 }
21
22 Entity_kind :: enum {
23 Covered,
24 Uncovered_0,
25 Uncovered_1,
26 Uncovered_2,
27 Uncovered_3,
28 Uncovered_4,
29 Uncovered_5,
30 Uncovered_6,
31 Uncovered_7,
32 Uncovered_8,
33 Mine_exploded,
34 Mine_idle,
35 }
36
37 ASSET_COORDINATES: [Entity_kind]Position = {
38 .Uncovered_0 = {x = 0 * 16, y = 22},
39 .Uncovered_1 = {x = 1 * 16, y = 22},
40 .Uncovered_2 = {x = 2 * 16, y = 22},
41 .Uncovered_3 = {x = 3 * 16, y = 22},
42 .Uncovered_4 = {x = 4 * 16, y = 22},
43 .Uncovered_5 = {x = 5 * 16, y = 22},
44 .Uncovered_6 = {x = 6 * 16, y = 22},
45 .Uncovered_7 = {x = 7 * 16, y = 22},
46 .Uncovered_8 = {x = 8 * 16, y = 22},
47 .Covered = {x = 0, y = 38},
48 .Mine_exploded = {x = 32, y = 40},
49 .Mine_idle = {x = 64, y = 40},
50 }
51
52 AuthToken :: [16]u8
53
54 AuthEntry :: struct {
55 family: u16,
56 auth_name: []u8,
57 auth_data: []u8,
58 }
59
60 Screen :: struct #packed {
61 id: u32,
62 colormap: u32,
63 white: u32,
64 black: u32,
65 input_mask: u32,
66 width: u16,
67 height: u16,
68 width_mm: u16,
69 height_mm: u16,
70 maps_min: u16,
71 maps_max: u16,
72 root_visual_id: u32,
73 backing_store: u8,
74 save_unders: u8,
75 root_depth: u8,
76 depths_count: u8,
77 }
78
79 ConnectionInformation :: struct {
80 root_screen: Screen,
81 resource_id_base: u32,
82 resource_id_mask: u32,
83 }
84
85
86 AUTH_ENTRY_FAMILY_LOCAL: u16 : 1
87 AUTH_ENTRY_MAGIC_COOKIE: string : "MIT-MAGIC-COOKIE-1"
88
89 round_up_4 :: #force_inline proc(x: u32) -> u32 {
90 mask: i32 = -4
91 return transmute(u32)((transmute(i32)x + 3) & mask)
92 }
93
94 read_x11_auth_entry :: proc(buffer: ^bytes.Buffer) -> (AuthEntry, bool) {
95 entry := AuthEntry{}
96
97 {
98 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&entry.family))
99 if err == .EOF {return {}, false}
100
101 assert(err == .None)
102 assert(n_read == size_of(entry.family))
103 }
104
105 address_len: u16 = 0
106 {
107 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&address_len))
108 assert(err == .None)
109
110 address_len = bits.byte_swap(address_len)
111 assert(n_read == size_of(address_len))
112 }
113
114 address := make([]u8, address_len)
115 {
116 n_read, err := bytes.buffer_read(buffer, address)
117 assert(err == .None)
118 assert(n_read == cast(int)address_len)
119 }
120
121 display_number_len: u16 = 0
122 {
123 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&display_number_len))
124 assert(err == .None)
125
126 display_number_len = bits.byte_swap(display_number_len)
127 assert(n_read == size_of(display_number_len))
128 }
129
130 display_number := make([]u8, display_number_len)
131 {
132 n_read, err := bytes.buffer_read(buffer, display_number)
133 assert(err == .None)
134 assert(n_read == cast(int)display_number_len)
135 }
136
137 auth_name_len: u16 = 0
138 {
139 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_name_len))
140 assert(err == .None)
141
142 auth_name_len = bits.byte_swap(auth_name_len)
143 assert(n_read == size_of(auth_name_len))
144 }
145
146 entry.auth_name = make([]u8, auth_name_len)
147 {
148 n_read, err := bytes.buffer_read(buffer, entry.auth_name)
149 assert(err == .None)
150 assert(n_read == cast(int)auth_name_len)
151 }
152
153 auth_data_len: u16 = 0
154 {
155 n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_data_len))
156 assert(err == .None)
157
158 auth_data_len = bits.byte_swap(auth_data_len)
159 assert(n_read == size_of(auth_data_len))
160 }
161
162 entry.auth_data = make([]u8, auth_data_len)
163 {
164 n_read, err := bytes.buffer_read(buffer, entry.auth_data)
165 assert(err == .None)
166 assert(n_read == cast(int)auth_data_len)
167 }
168
169
170 return entry, true
171 }
172
173 load_x11_auth_token :: proc(allocator := context.allocator) -> (token: AuthToken, ok: bool) {
174 context.allocator = allocator
175 defer free_all(allocator)
176
177 filename_env := os.get_env("XAUTHORITY")
178
179 filename :=
180 len(filename_env) != 0 \
181 ? filename_env \
182 : filepath.join([]string{os.get_env("HOME"), ".Xauthority"})
183
184 data := os.read_entire_file_from_filename(filename) or_return
185
186 buffer := bytes.Buffer{}
187 bytes.buffer_init(&buffer, data[:])
188
189
190 for {
191 auth_entry := read_x11_auth_entry(&buffer) or_break
192
193 if auth_entry.family == AUTH_ENTRY_FAMILY_LOCAL &&
194 slice.equal(auth_entry.auth_name, transmute([]u8)AUTH_ENTRY_MAGIC_COOKIE) &&
195 len(auth_entry.auth_data) == size_of(AuthToken) {
196
197 mem.copy_non_overlapping(
198 raw_data(&token),
199 raw_data(auth_entry.auth_data),
200 size_of(AuthToken),
201 )
202 return token, true
203 }
204 }
205
206 // Did not find a fitting token.
207 return {}, false
208 }
209
210 connect_x11_socket :: proc() -> os.Socket {
211 SockaddrUn :: struct #packed {
212 sa_family: os.ADDRESS_FAMILY,
213 sa_data: [108]u8,
214 }
215
216 socket, err := os.socket(os.AF_UNIX, os.SOCK_STREAM, 0)
217 assert(err == os.ERROR_NONE)
218
219 possible_socket_paths := [2]string{"/tmp/.X11-unix/X0", "/tmp/.X11-unix/X1"}
220 for &socket_path in possible_socket_paths {
221 addr := SockaddrUn {
222 sa_family = cast(u16)os.AF_UNIX,
223 }
224 mem.copy_non_overlapping(&addr.sa_data, raw_data(socket_path), len(socket_path))
225
226 err = os.connect(socket, cast(^os.SOCKADDR)&addr, size_of(addr))
227 if (err == os.ERROR_NONE) {return socket}
228 }
229
230 os.exit(1)
231 }
232
233
234 x11_handshake :: proc(socket: os.Socket, auth_token: ^AuthToken) -> ConnectionInformation {
235
236 Request :: struct #packed {
237 endianness: u8,
238 pad1: u8,
239 major_version: u16,
240 minor_version: u16,
241 authorization_len: u16,
242 authorization_data_len: u16,
243 pad2: u16,
244 }
245
246 request := Request {
247 endianness = 'l',
248 major_version = 11,
249 authorization_len = len(AUTH_ENTRY_MAGIC_COOKIE),
250 authorization_data_len = size_of(AuthToken),
251 }
252
253
254 {
255 padding := [2]u8{0, 0}
256 n_sent, err := linux.writev(
257 cast(linux.Fd)socket,
258 []linux.IO_Vec {
259 {base = &request, len = size_of(Request)},
260 {base = raw_data(AUTH_ENTRY_MAGIC_COOKIE), len = len(AUTH_ENTRY_MAGIC_COOKIE)},
261 {base = raw_data(padding[:]), len = len(padding)},
262 {base = raw_data(auth_token[:]), len = len(auth_token)},
263 },
264 )
265 assert(err == .NONE)
266 assert(
267 n_sent ==
268 size_of(Request) + len(AUTH_ENTRY_MAGIC_COOKIE) + len(padding) + len(auth_token),
269 )
270 }
271
272 StaticResponse :: struct #packed {
273 success: u8,
274 pad1: u8,
275 major_version: u16,
276 minor_version: u16,
277 length: u16,
278 }
279
280 static_response := StaticResponse{}
281 {
282 n_recv, err := os.recv(socket, mem.ptr_to_bytes(&static_response), 0)
283 assert(err == os.ERROR_NONE)
284 assert(n_recv == size_of(StaticResponse))
285 assert(static_response.success == 1)
286 }
287
288
289 recv_buf: [1 << 15]u8 = {}
290 {
291 assert(len(recv_buf) >= cast(u32)static_response.length * 4)
292
293 n_recv, err := os.recv(socket, recv_buf[:], 0)
294 assert(err == os.ERROR_NONE)
295 assert(n_recv == cast(u32)static_response.length * 4)
296 }
297
298
299 DynamicResponse :: struct #packed {
300 release_number: u32,
301 resource_id_base: u32,
302 resource_id_mask: u32,
303 motion_buffer_size: u32,
304 vendor_length: u16,
305 maximum_request_length: u16,
306 screens_in_root_count: u8,
307 formats_count: u8,
308 image_byte_order: u8,
309 bitmap_format_bit_order: u8,
310 bitmap_format_scanline_unit: u8,
311 bitmap_format_scanline_pad: u8,
312 min_keycode: u8,
313 max_keycode: u8,
314 pad2: u32,
315 }
316
317 read_buffer := bytes.Buffer{}
318 bytes.buffer_init(&read_buffer, recv_buf[:])
319
320 dynamic_response := DynamicResponse{}
321 {
322 n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&dynamic_response))
323 assert(err == .None)
324 assert(n_read == size_of(DynamicResponse))
325 }
326
327
328 // Skip over the vendor information.
329 bytes.buffer_next(&read_buffer, cast(int)round_up_4(cast(u32)dynamic_response.vendor_length))
330 // Skip over the format information (each 8 bytes long).
331 bytes.buffer_next(&read_buffer, 8 * cast(int)dynamic_response.formats_count)
332
333 screen := Screen{}
334 {
335 n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&screen))
336 assert(err == .None)
337 assert(n_read == size_of(screen))
338 }
339
340 return (ConnectionInformation {
341 resource_id_base = dynamic_response.resource_id_base,
342 resource_id_mask = dynamic_response.resource_id_mask,
343 root_screen = screen,
344 })
345 }
346
347 next_x11_id :: proc(current_id: u32, info: ConnectionInformation) -> u32 {
348 return 1 + ((info.resource_id_mask & (current_id)) | info.resource_id_base)
349 }
350
351 x11_create_graphical_context :: proc(socket: os.Socket, gc_id: u32, root_id: u32) {
352 opcode: u8 : 55
353 FLAG_GC_BG: u32 : 8
354 BITMASK: u32 : FLAG_GC_BG
355 VALUE1: u32 : 0x00_00_ff_00
356
357 Request :: struct #packed {
358 opcode: u8,
359 pad1: u8,
360 length: u16,
361 id: u32,
362 drawable: u32,
363 bitmask: u32,
364 value1: u32,
365 }
366 request := Request {
367 opcode = opcode,
368 length = 5,
369 id = gc_id,
370 drawable = root_id,
371 bitmask = BITMASK,
372 value1 = VALUE1,
373 }
374
375 {
376 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
377 assert(err == os.ERROR_NONE)
378 assert(n_sent == size_of(Request))
379 }
380 }
381
382 x11_create_window :: proc(
383 socket: os.Socket,
384 window_id: u32,
385 parent_id: u32,
386 x: u16,
387 y: u16,
388 width: u16,
389 height: u16,
390 root_visual_id: u32,
391 ) {
392 FLAG_WIN_BG_PIXEL: u32 : 2
393 FLAG_WIN_EVENT: u32 : 0x800
394 FLAG_COUNT: u16 : 2
395 EVENT_FLAG_EXPOSURE: u32 = 0x80_00
396 EVENT_FLAG_KEY_PRESS: u32 = 0x1
397 EVENT_FLAG_KEY_RELEASE: u32 = 0x2
398 EVENT_FLAG_BUTTON_PRESS: u32 = 0x4
399 EVENT_FLAG_BUTTON_RELEASE: u32 = 0x8
400 flags: u32 : FLAG_WIN_BG_PIXEL | FLAG_WIN_EVENT
401 depth: u8 : 24
402 border_width: u16 : 0
403 CLASS_INPUT_OUTPUT: u16 : 1
404 opcode: u8 : 1
405 BACKGROUND_PIXEL_COLOR: u32 : 0x00_ff_ff_00
406
407 Request :: struct #packed {
408 opcode: u8,
409 depth: u8,
410 request_length: u16,
411 window_id: u32,
412 parent_id: u32,
413 x: u16,
414 y: u16,
415 width: u16,
416 height: u16,
417 border_width: u16,
418 class: u16,
419 root_visual_id: u32,
420 bitmask: u32,
421 value1: u32,
422 value2: u32,
423 }
424 request := Request {
425 opcode = opcode,
426 depth = depth,
427 request_length = 8 + FLAG_COUNT,
428 window_id = window_id,
429 parent_id = parent_id,
430 x = x,
431 y = y,
432 width = width,
433 height = height,
434 border_width = border_width,
435 class = CLASS_INPUT_OUTPUT,
436 root_visual_id = root_visual_id,
437 bitmask = flags,
438 value1 = BACKGROUND_PIXEL_COLOR,
439 value2 = EVENT_FLAG_EXPOSURE | EVENT_FLAG_BUTTON_RELEASE | EVENT_FLAG_BUTTON_PRESS
440 | EVENT_FLAG_KEY_PRESS | EVENT_FLAG_KEY_RELEASE,
441 }
442
443 {
444 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
445 assert(err == os.ERROR_NONE)
446 assert(n_sent == size_of(Request))
447 }
448 }
449
450 x11_map_window :: proc(socket: os.Socket, window_id: u32) {
451 opcode: u8 : 8
452
453 Request :: struct #packed {
454 opcode: u8,
455 pad1: u8,
456 request_length: u16,
457 window_id: u32,
458 }
459 request := Request {
460 opcode = opcode,
461 request_length = 2,
462 window_id = window_id,
463 }
464 {
465 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
466 assert(err == os.ERROR_NONE)
467 assert(n_sent == size_of(Request))
468 }
469
470 }
471
472 x11_put_image :: proc(
473 socket: os.Socket,
474 drawable_id: u32,
475 gc_id: u32,
476 width: u16,
477 height: u16,
478 dst_x: u16,
479 dst_y: u16,
480 depth: u8,
481 data: []u8,
482 ) {
483 opcode: u8 : 72
484
485 Request :: struct #packed {
486 opcode: u8,
487 format: u8,
488 request_length: u16,
489 drawable_id: u32,
490 gc_id: u32,
491 width: u16,
492 height: u16,
493 dst_x: u16,
494 dst_y: u16,
495 left_pad: u8,
496 depth: u8,
497 pad1: u16,
498 }
499
500 data_length_padded := round_up_4(cast(u32)len(data))
501
502 request := Request {
503 opcode = opcode,
504 format = 2, // ZPixmap
505 request_length = cast(u16)(6 + data_length_padded / 4),
506 drawable_id = drawable_id,
507 gc_id = gc_id,
508 width = width,
509 height = height,
510 dst_x = dst_x,
511 dst_y = dst_y,
512 depth = depth,
513 }
514 {
515 padding_len := data_length_padded - cast(u32)len(data)
516
517 n_sent, err := linux.writev(
518 cast(linux.Fd)socket,
519 []linux.IO_Vec {
520 {base = &request, len = size_of(Request)},
521 {base = raw_data(data), len = len(data)},
522 {base = raw_data(data), len = cast(uint)padding_len},
523 },
524 )
525 assert(err == .NONE)
526 assert(n_sent == size_of(Request) + len(data) + cast(int)padding_len)
527 }
528 }
529
530 render :: proc(socket: os.Socket, scene: ^Scene) {
531 for entity, i in scene.displayed_entities {
532 rect := ASSET_COORDINATES[entity]
533 row, column := idx_to_row_column(i)
534
535 x11_copy_area(
536 socket,
537 scene.sprite_pixmap_id,
538 scene.window_id,
539 scene.gc_id,
540 rect.x,
541 rect.y,
542 cast(u16)column * ENTITIES_WIDTH,
543 cast(u16)row * ENTITIES_HEIGHT,
544 ENTITIES_WIDTH,
545 ENTITIES_HEIGHT,
546 )
547 }
548 }
549
550 ENTITIES_ROW_COUNT :: 16
551 ENTITIES_COLUMN_COUNT :: 16
552 ENTITIES_WIDTH :: 16
553 ENTITIES_HEIGHT :: 16
554
555 Scene :: struct {
556 window_id: u32,
557 gc_id: u32,
558 sprite_pixmap_id: u32,
559 displayed_entities: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]Entity_kind,
560 // TODO: Bitfield?
561 mines: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
562 }
563
564 wait_for_x11_events :: proc(socket: os.Socket, scene: ^Scene) {
565 GenericEvent :: struct #packed {
566 code: u8,
567 pad: [31]u8,
568 }
569 assert(size_of(GenericEvent) == 32)
570
571 KeyReleaseEvent :: struct #packed {
572 code: u8,
573 detail: u8,
574 sequence_number: u16,
575 time: u32,
576 root_id: u32,
577 event: u32,
578 child_id: u32,
579 root_x: u16,
580 root_y: u16,
581 event_x: u16,
582 event_y: u16,
583 state: u16,
584 same_screen: bool,
585 pad1: u8,
586 }
587 assert(size_of(KeyReleaseEvent) == 32)
588
589 ButtonReleaseEvent :: struct #packed {
590 code: u8,
591 detail: u8,
592 seq_number: u16,
593 timestamp: u32,
594 root: u32,
595 event: u32,
596 child: u32,
597 root_x: u16,
598 root_y: u16,
599 event_x: u16,
600 event_y: u16,
601 state: u16,
602 same_screen: bool,
603 pad1: u8,
604 }
605 assert(size_of(ButtonReleaseEvent) == 32)
606
607 EVENT_EXPOSURE: u8 : 0xc
608 EVENT_KEY_RELEASE: u8 : 0x3
609 EVENT_BUTTON_RELEASE: u8 : 0x5
610
611 KEYCODE_ENTER: u8 : 36
612
613 for {
614 generic_event := GenericEvent{}
615 n_recv, err := os.recv(socket, mem.ptr_to_bytes(&generic_event), 0)
616 if err == os.EPIPE || n_recv == 0 {
617 os.exit(0) // The end.
618 }
619
620 assert(err == os.ERROR_NONE)
621 assert(n_recv == size_of(GenericEvent))
622
623 switch generic_event.code {
624 case EVENT_EXPOSURE:
625 render(socket, scene)
626
627 case EVENT_KEY_RELEASE:
628 event := transmute(KeyReleaseEvent)generic_event
629 if event.detail == KEYCODE_ENTER {
630 reset(scene)
631 render(socket, scene)
632 }
633
634 case EVENT_BUTTON_RELEASE:
635 event := transmute(ButtonReleaseEvent)generic_event
636 on_cell_clicked(event.event_x, event.event_y, scene)
637 render(socket, scene)
638 }
639 }
640 }
641
642 reset :: proc(scene: ^Scene) {
643 for &entity in scene.displayed_entities {
644 entity = .Covered
645 }
646
647 for &mine in scene.mines {
648 mine = rand.choice([]bool{true, false, false, false})
649 }
650 }
651
652 x11_copy_area :: proc(
653 socket: os.Socket,
654 src_id: u32,
655 dst_id: u32,
656 gc_id: u32,
657 src_x: u16,
658 src_y: u16,
659 dst_x: u16,
660 dst_y: u16,
661 width: u16,
662 height: u16,
663 ) {
664 opcode: u8 : 62
665 Request :: struct #packed {
666 opcode: u8,
667 pad1: u8,
668 request_length: u16,
669 src_id: u32,
670 dst_id: u32,
671 gc_id: u32,
672 src_x: u16,
673 src_y: u16,
674 dst_x: u16,
675 dst_y: u16,
676 width: u16,
677 height: u16,
678 }
679
680 request := Request {
681 opcode = opcode,
682 request_length = 7,
683 src_id = src_id,
684 dst_id = dst_id,
685 gc_id = gc_id,
686 src_x = src_x,
687 src_y = src_y,
688 dst_x = dst_x,
689 dst_y = dst_y,
690 width = width,
691 height = height,
692 }
693 {
694 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
695 assert(err == os.ERROR_NONE)
696 assert(n_sent == size_of(Request))
697 }
698 }
699
700 on_cell_clicked :: proc(x: u16, y: u16, scene: ^Scene) {
701 idx, row, column := locate_entity_by_coordinate(x, y)
702
703 mined := scene.mines[idx]
704
705 if mined {
706 scene.displayed_entities[idx] = .Mine_exploded
707 // Lose.
708 uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_exploded)
709 } else {
710 visited := [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool{}
711 uncover_cells_flood_fill(row, column, &scene.displayed_entities, &scene.mines, &visited)
712
713 // Win.
714 if count_remaining_goals(scene.displayed_entities, scene.mines) == 0 {
715 uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_idle)
716 }
717 }
718 }
719
720 count_remaining_goals :: proc(
721 displayed_entities: [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
722 mines: [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool,
723 ) -> int {
724
725 covered := 0
726
727 for entity in displayed_entities {
728 covered += cast(int)(entity == .Covered)
729 }
730
731 mines_count := 0
732
733 for mine in mines {
734 mines_count += cast(int)mine
735 }
736
737 return covered - mines_count
738 }
739
740 uncover_all_cells :: proc(
741 displayed_entities: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
742 mines: ^[ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
743 shown_mine: Entity_kind,
744 ) {
745 for &entity, i in displayed_entities {
746 if mines[i] {
747 entity = shown_mine
748 } else {
749 row, column := idx_to_row_column(i)
750 mines_around_count := count_mines_around_cell(row, column, mines[:])
751 assert(mines_around_count <= 8)
752
753 entity = cast(Entity_kind)(cast(int)Entity_kind.Uncovered_0 + mines_around_count)
754 }
755 }
756 }
757
758 uncover_cells_flood_fill :: proc(
759 row: int,
760 column: int,
761 displayed_entities: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
762 mines: ^[ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
763 visited: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool,
764 ) {
765 i := row_column_to_idx(row, column)
766 if visited[i] {return}
767
768 visited[i] = true
769
770 // Do not uncover covered mines.
771 if mines[i] {return}
772
773 if displayed_entities[i] != .Covered {return}
774
775 // Uncover cell.
776
777 mines_around_count := count_mines_around_cell(row, column, mines[:])
778 assert(mines_around_count <= 8)
779
780 displayed_entities[i] =
781 cast(Entity_kind)(cast(int)Entity_kind.Uncovered_0 + mines_around_count)
782
783 // Uncover neighbors.
784
785 // Up.
786 if !(row == 0) {
787 uncover_cells_flood_fill(row - 1, column, displayed_entities, mines, visited)
788 }
789
790 // Right
791 if !(column == (ENTITIES_COLUMN_COUNT - 1)) {
792 uncover_cells_flood_fill(row, column + 1, displayed_entities, mines, visited)
793 }
794
795 // Bottom.
796 if !(row == (ENTITIES_ROW_COUNT - 1)) {
797 uncover_cells_flood_fill(row + 1, column, displayed_entities, mines, visited)
798 }
799
800 // Left.
801 if !(column == 0) {
802 uncover_cells_flood_fill(row, column - 1, displayed_entities, mines, visited)
803 }
804 }
805
806 idx_to_row_column :: #force_inline proc(i: int) -> (int, int) {
807 column := i % ENTITIES_COLUMN_COUNT
808 row := i / ENTITIES_ROW_COUNT
809
810 return row, column
811 }
812
813 row_column_to_idx :: #force_inline proc(row: int, column: int) -> int {
814 return cast(int)row * ENTITIES_COLUMN_COUNT + cast(int)column
815 }
816
817 count_mines_around_cell :: proc(row: int, column: int, displayed_entities: []bool) -> int {
818 // TODO: Pad the border to elide all bound checks?
819
820 up_left :=
821 row == 0 || column == 0 \
822 ? false \
823 : displayed_entities[row_column_to_idx(row - 1, column - 1)]
824 up := row == 0 ? false : displayed_entities[row_column_to_idx(row - 1, column)]
825 up_right :=
826 row == 0 || column == (ENTITIES_COLUMN_COUNT - 1) \
827 ? false \
828 : displayed_entities[row_column_to_idx(row - 1, column + 1)]
829 right :=
830 column == (ENTITIES_COLUMN_COUNT - 1) \
831 ? false \
832 : displayed_entities[row_column_to_idx(row, column + 1)]
833 bottom_right :=
834 row == (ENTITIES_ROW_COUNT - 1) || column == (ENTITIES_COLUMN_COUNT - 1) \
835 ? false \
836 : displayed_entities[row_column_to_idx(row + 1, column + 1)]
837 bottom :=
838 row == (ENTITIES_ROW_COUNT - 1) \
839 ? false \
840 : displayed_entities[row_column_to_idx(row + 1, column)]
841 bottom_left :=
842 column == 0 || row == (ENTITIES_COLUMN_COUNT - 1) \
843 ? false \
844 : displayed_entities[row_column_to_idx(row + 1, column - 1)]
845 left := column == 0 ? false : displayed_entities[row_column_to_idx(row, column - 1)]
846
847
848 return(
849 cast(int)up_left +
850 cast(int)up +
851 cast(int)up_right +
852 cast(int)right +
853 cast(int)bottom_right +
854 cast(int)bottom +
855 cast(int)bottom_left +
856 cast(int)left \
857 )
858 }
859
860 locate_entity_by_coordinate :: proc(win_x: u16, win_y: u16) -> (idx: int, row: int, column: int) {
861 column = cast(int)win_x / ENTITIES_WIDTH
862 row = cast(int)win_y / ENTITIES_HEIGHT
863
864 idx = row_column_to_idx(row, column)
865
866 return idx, row, column
867 }
868
869 x11_create_pixmap :: proc(
870 socket: os.Socket,
871 window_id: u32,
872 pixmap_id: u32,
873 width: u16,
874 height: u16,
875 depth: u8,
876 ) {
877 opcode: u8 : 53
878
879 Request :: struct #packed {
880 opcode: u8,
881 depth: u8,
882 request_length: u16,
883 pixmap_id: u32,
884 drawable_id: u32,
885 width: u16,
886 height: u16,
887 }
888
889 request := Request {
890 opcode = opcode,
891 depth = depth,
892 request_length = 4,
893 pixmap_id = pixmap_id,
894 drawable_id = window_id,
895 width = width,
896 height = height,
897 }
898
899 {
900 n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
901 assert(err == os.ERROR_NONE)
902 assert(n_sent == size_of(Request))
903 }
904 }
905
906 main :: proc() {
907 png_data := #load("sprite.png")
908 sprite, err := png.load_from_bytes(png_data, {})
909 assert(err == nil)
910 sprite_data := make([]u8, sprite.height * sprite.width * 4)
911
912 // Convert the image format from the sprite (RGB) into the X11 image format (BGRX).
913 for i := 0; i < sprite.height * sprite.width - 3; i += 1 {
914 sprite_data[i * 4 + 0] = sprite.pixels.buf[i * 3 + 2] // R -> B
915 sprite_data[i * 4 + 1] = sprite.pixels.buf[i * 3 + 1] // G -> G
916 sprite_data[i * 4 + 2] = sprite.pixels.buf[i * 3 + 0] // B -> R
917 sprite_data[i * 4 + 3] = 0 // pad
918 }
919
920 auth_token, _ := load_x11_auth_token(context.temp_allocator)
921
922 socket := connect_x11_socket()
923 connection_information := x11_handshake(socket, &auth_token)
924
925 gc_id := next_x11_id(0, connection_information)
926 x11_create_graphical_context(socket, gc_id, connection_information.root_screen.id)
927
928 window_id := next_x11_id(gc_id, connection_information)
929 x11_create_window(
930 socket,
931 window_id,
932 connection_information.root_screen.id,
933 200,
934 200,
935 ENTITIES_COLUMN_COUNT * ENTITIES_WIDTH,
936 ENTITIES_ROW_COUNT * ENTITIES_HEIGHT,
937 connection_information.root_screen.root_visual_id,
938 )
939
940 img_depth: u8 = 24
941 pixmap_id := next_x11_id(window_id, connection_information)
942 x11_create_pixmap(
943 socket,
944 window_id,
945 pixmap_id,
946 cast(u16)sprite.width,
947 cast(u16)sprite.height,
948 img_depth,
949 )
950 scene := Scene {
951 window_id = window_id,
952 gc_id = gc_id,
953 sprite_pixmap_id = pixmap_id,
954 }
955 reset(&scene)
956
957 x11_put_image(
958 socket,
959 scene.sprite_pixmap_id,
960 scene.gc_id,
961 cast(u16)sprite.width,
962 cast(u16)sprite.height,
963 0,
964 0,
965 img_depth,
966 sprite_data,
967 )
968
969 x11_map_window(socket, window_id)
970
971 wait_for_x11_events(socket, &scene)
972 }
973
974
975 @(test)
976 test_round_up_4 :: proc(_: ^testing.T) {
977 assert(round_up_4(0) == 0)
978 assert(round_up_4(1) == 4)
979 assert(round_up_4(2) == 4)
980 assert(round_up_4(3) == 4)
981 assert(round_up_4(4) == 4)
982 assert(round_up_4(5) == 8)
983 assert(round_up_4(6) == 8)
984 assert(round_up_4(7) == 8)
985 assert(round_up_4(8) == 8)
986 }
987
988 @(test)
989 test_count_mines_around_cell :: proc(_: ^testing.T) {
990 {
991 mines := [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool{}
992 mines[row_column_to_idx(0, 0)] = true
993 mines[row_column_to_idx(0, 1)] = true
994 mines[row_column_to_idx(0, 2)] = true
995 mines[row_column_to_idx(1, 2)] = true
996 mines[row_column_to_idx(2, 2)] = true
997 mines[row_column_to_idx(2, 1)] = true
998 mines[row_column_to_idx(2, 0)] = true
999 mines[row_column_to_idx(1, 0)] = true
1000
1001 assert(count_mines_around_cell(1, 1, mines[:]) == 8)
1002 }
1003 }
If you enjoy what you're reading, you want to support me, and can afford it: Support me. That allows me to write more cool articles!
This blog is open-source! If you find a problem, please open a Github issue. The content of this blog as well as the code snippets are under the BSD-3 License which I also usually use for all my personal projects. It's basically free for every use but you have to mention me as the original author.