⏴ Back to all articles

Published on 2024-06-20

Let's write a video game from scratch like it's 1987

Table of contents

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?

The game running on Linux (XWayland)

The game executable running unmodified on FreeBSD (X11) through Linux binary compatibility

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.

What we're making

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?".

Authentication

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:

Text
1 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:

Odin
1 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:

Odin
1 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:

Odin
1 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:

Odin
1 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.

Opening a window

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:

Odin
1 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:

Odin
1 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:

Odin
1 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:

Odin
1 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:

Odin
1 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:

Odin
1 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:

Odin
1 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:

We also pick an arbitrary background color, yellow. It does not matter because we will always cover every part of the window with our assets.

Odin
1 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:

Odin
1 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:

Odin
1 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:

Empty yellow window

Time to start programming the game itself!

Loading assets

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:

Our sprite

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:

Odin
1 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:

Odin
1 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:

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:

Odin
1 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:

Odin
1 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:

First images on the screen

We are now ready to focus on the game entities.

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:

Odin
1 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:

Odin
1 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.

Odin
1 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:

Odin
1 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:

Odin
1 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:

First scene

The next step is to respond to events.

Reacting to keyboard and mouse 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:

Odin
1 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!

Game logic: uncover a cell

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:

Odin
1 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:

Odin
1 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:

Odin
1 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.

Screenshot

Conclusion

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!

Addendum: the full code

The full code
Odin
1 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 }

⏴ Back to all articles