1package game_of_life
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import "base:intrinsics"
29import "base:runtime"
30import "core:fmt"
31import "core:math/rand"
32import "core:os"
33import win32 "core:sys/windows"
34import "core:time"
35
36
37Color :: [4]u8
38Int2 :: [2]i32
39
40
41TITLE :: "Game of Life"
42CLASS_NAME :: "OdinMainClass"
43WORLD_SIZE :: Int2 {128, 64}
44ZOOM :: 12
45FPS :: 20
46
47BLACK :: Color{0, 0, 0, 255}
48WHITE :: Color{255, 255, 255, 255}
49
50HELP_TEXT :: `ESC - Quit
51P - Toggle Pause
52R - Restart
53H - Toggle Help`
54HELP_RECT :: win32.RECT {20, 20, 160, 105}
55show_help := true
56
57
58IDT_TIMER1 :: 10001
59
60COLOR_BITS :: 1
61PALETTE_COUNT :: 1 << COLOR_BITS
62Color_Palette :: [PALETTE_COUNT]Color
63
64Bitmap_Info :: struct {
65 bmiHeader: win32.BITMAPINFOHEADER,
66 bmiColors: Color_Palette,
67}
68
69Screen_Buffer :: [^]u8
70
71Config_Flag :: enum u32 {
72 CENTER = 1,
73}
74Config_Flags :: distinct bit_set[Config_Flag;u32]
75
76Window :: struct {
77 name: win32.wstring,
78 size: Int2,
79 fps: i32,
80 control_flags: Config_Flags,
81}
82
83Game :: struct {
84 tick_rate: time.Duration,
85 last_tick: time.Time,
86 pause: bool,
87 colors: Color_Palette,
88 size: Int2,
89 zoom: i32,
90 world, next_world: ^World,
91 timer_id: win32.UINT_PTR,
92 tick: u32,
93 hbitmap: win32.HBITMAP,
94 pvBits: Screen_Buffer,
95 window: Window,
96}
97
98World :: struct {
99 width: i32,
100 height: i32,
101 alive: []u8,
102}
103
104Cell :: struct {
105 width: f32,
106 height: f32,
107}
108
109
110
111
112
113
114
115
116
117
118update_world :: #force_inline proc(world: ^World, next_world: ^World) {
119 for x: i32 = 0; x < world.width; x += 1 {
120 for y: i32 = 0; y < world.height; y += 1 {
121 neighbors := count_neighbors(world, x, y)
122 index := y * world.width + x
123
124 switch neighbors {
125 case 2: next_world.alive[index] = world.alive[index]
126 case 3: next_world.alive[index] = 1
127 case: next_world.alive[index] = 0
128 }
129 }
130 }
131}
132
133
134
135
136count_neighbors :: #force_inline proc(w: ^World, x: i32, y: i32) -> u8 {
137
138 left := (x - 1) %% w.width
139 right := (x + 1) %% w.width
140 up := (y - 1) %% w.height
141 down := (y + 1) %% w.height
142
143 top_left := w.alive[up * w.width + left ]
144 top := w.alive[up * w.width + x ]
145 top_right := w.alive[up * w.width + right]
146
147 mid_left := w.alive[y * w.width + left ]
148 mid_right := w.alive[y * w.width + right]
149
150 bottom_left := w.alive[down * w.width + left ]
151 bottom := w.alive[down * w.width + x ]
152 bottom_right := w.alive[down * w.width + right]
153
154 top_row := top_left + top + top_right
155 mid_row := mid_left + mid_right
156 bottom_row := bottom_left + bottom + bottom_right
157
158 total := top_row + mid_row + bottom_row
159 return total
160}
161
162
163
164
165draw_world :: #force_inline proc(app: ^Game) {
166 cnt := int(app.world.width * app.world.height)
167 runtime.mem_copy(app.pvBits, &app.world.alive[0], cnt)
168}
169
170message_box :: #force_inline proc(text, caption: string, loc := #caller_location) {
171 win32.MessageBoxW(nil, win32.utf8_to_wstring(text), win32.utf8_to_wstring(caption), win32.MB_ICONSTOP | win32.MB_OK)
172}
173
174show_error_and_panic :: proc(msg: string, loc := #caller_location) {
175 message_box(fmt.tprintf("%s\nLast error: %x\n", msg, win32.GetLastError()), "Panic")
176 panic(msg, loc = loc)
177}
178
179get_rect_size :: #force_inline proc(rect: ^win32.RECT) -> Int2 {return {(rect.right - rect.left), (rect.bottom - rect.top)}}
180
181set_app :: #force_inline proc(hwnd: win32.HWND, app: ^Game) {win32.SetWindowLongPtrW(hwnd, win32.GWLP_USERDATA, win32.LONG_PTR(uintptr(app)))}
182
183get_app :: #force_inline proc(hwnd: win32.HWND) -> ^Game {return (^Game)(rawptr(uintptr(win32.GetWindowLongPtrW(hwnd, win32.GWLP_USERDATA))))}
184
185randomize_world :: proc(world: ^World) {
186 cc := world.width * world.height
187 for i in 0 ..< cc {world.alive[i] = u8(rand.int31_max(2))}
188}
189
190WM_CREATE :: proc(hwnd: win32.HWND, lparam: win32.LPARAM) -> win32.LRESULT {
191 pcs := (^win32.CREATESTRUCTW)(rawptr(uintptr(lparam)))
192 if pcs == nil {show_error_and_panic("lparam is nil")}
193 app := (^Game)(pcs.lpCreateParams)
194 if app == nil {show_error_and_panic("lpCreateParams is nil")}
195 set_app(hwnd, app)
196
197 app.timer_id = win32.SetTimer(hwnd, IDT_TIMER1, 1000 / FPS, nil)
198 if app.timer_id == 0 {show_error_and_panic("No timer")}
199
200 hdc := win32.GetDC(hwnd)
201 defer win32.ReleaseDC(hwnd, hdc)
202
203 bitmap_info := Bitmap_Info {
204 bmiHeader = win32.BITMAPINFOHEADER {
205 biSize = size_of(win32.BITMAPINFOHEADER),
206 biWidth = app.size.x,
207 biHeight = -app.size.y,
208 biPlanes = 1,
209 biBitCount = 8,
210 biCompression = win32.BI_RGB,
211 biClrUsed = len(app.colors),
212 },
213 bmiColors = app.colors,
214 }
215 app.hbitmap = win32.CreateDIBSection(hdc, cast(^win32.BITMAPINFO)&bitmap_info, win32.DIB_RGB_COLORS, (^rawptr)(&app.pvBits), nil, 0)
216
217 if app.world != nil {
218 randomize_world(app.world)
219 }
220 app.pause = false
221
222 return 0
223}
224
225WM_DESTROY :: proc(hwnd: win32.HWND) -> win32.LRESULT {
226 app := get_app(hwnd)
227 if app == nil {show_error_and_panic("Missing app!")}
228 if app.timer_id != 0 {
229 if !win32.KillTimer(hwnd, app.timer_id) {
230 message_box("Unable to kill timer", "Error")
231 }
232 app.timer_id = 0
233 }
234 if app.hbitmap != nil {
235 if !win32.DeleteObject(win32.HGDIOBJ(app.hbitmap)) {
236 message_box("Unable to delete hbitmap", "Error")
237 }
238 app.hbitmap = nil
239 }
240 exit_code :: 0
241 win32.PostQuitMessage(exit_code)
242 return 0
243}
244
245WM_PAINT :: proc(hwnd: win32.HWND) -> win32.LRESULT {
246 app := get_app(hwnd)
247 if app == nil {return 0}
248
249 ps: win32.PAINTSTRUCT
250 hdc := win32.BeginPaint(hwnd, &ps)
251 defer win32.EndPaint(hwnd, &ps)
252
253 if app.hbitmap != nil {
254 hdc_source := win32.CreateCompatibleDC(hdc)
255 defer win32.DeleteDC(hdc_source)
256
257 win32.SelectObject(hdc_source, win32.HGDIOBJ(app.hbitmap))
258 client_size := get_rect_size(&ps.rcPaint)
259 win32.StretchBlt(hdc, 0, 0, client_size.x, client_size.y, hdc_source, 0, 0, app.size.x, app.size.y, win32.SRCCOPY)
260 }
261
262 if show_help {
263 rect := HELP_RECT
264 win32.RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20)
265 win32.InflateRect(&rect, -10, -10)
266 win32.DrawTextW(hdc, HELP_TEXT, -1, &rect, .DT_TOP)
267 }
268
269 return 0
270}
271
272WM_TIMER :: proc(hwnd: win32.HWND, wparam: win32.WPARAM, lparam: win32.LPARAM) -> win32.LRESULT {
273 switch wparam {
274 case IDT_TIMER1:
275 app := get_app(hwnd)
276 if app != nil {
277
278 if app.tick == 40 && show_help {show_help = false}
279
280 if !app.pause {
281 app.tick += 1
282 update_world(app.world, app.next_world)
283 app.world, app.next_world = app.next_world, app.world
284 }
285
286 draw_world(app)
287
288 win32.SetWindowTextW(hwnd, win32.utf8_to_wstring(fmt.tprintf("%s - %d\n", app.window.name, app.tick)))
289 win32.RedrawWindow(hwnd, nil, nil, .RDW_INVALIDATE | .RDW_UPDATENOW)
290 }
291 case:
292 fmt.printf("WM_TIMER %v %v %v\n", hwnd, wparam, lparam)
293 }
294 return 0
295}
296
297WM_CHAR :: proc(hwnd: win32.HWND, wparam: win32.WPARAM, lparam: win32.LPARAM) -> win32.LRESULT {
298 switch wparam {
299 case '\x1b':
300 win32.PostMessageW(hwnd, win32.WM_CLOSE, 0, 0)
301 case 'h':
302 show_help ~= true
303 case 'p':
304 app := get_app(hwnd)
305 app.pause ~= true
306 if !app.pause {show_help = false}
307 case 'r':
308 app := get_app(hwnd)
309 randomize_world(app.world)
310 case ' ':
311 app := get_app(hwnd)
312 siz := app.world.width * app.world.height
313 idx := rand.int31_max(siz)
314 app.world.alive[idx] = 1
315 }
316 return 0
317}
318
319wndproc :: proc "system" (hwnd: win32.HWND, msg: win32.UINT, wparam: win32.WPARAM, lparam: win32.LPARAM) -> win32.LRESULT {
320 context = runtime.default_context()
321 switch msg {
322 case win32.WM_CREATE: return WM_CREATE(hwnd, lparam)
323 case win32.WM_DESTROY: return WM_DESTROY(hwnd)
324 case win32.WM_ERASEBKGND: return 1
325 case win32.WM_PAINT: return WM_PAINT(hwnd)
326 case win32.WM_CHAR: return WM_CHAR(hwnd, wparam, lparam)
327 case win32.WM_TIMER: return WM_TIMER(hwnd, wparam, lparam)
328 case: return win32.DefWindowProcW(hwnd, msg, wparam, lparam)
329 }
330}
331
332register_class :: proc(instance: win32.HINSTANCE) -> win32.ATOM {
333 icon: win32.HICON = win32.LoadIconW(instance, cstring16(win32.MAKEINTRESOURCEW(101)))
334 if icon == nil {icon = win32.LoadIconW(nil, win32.wstring(win32._IDI_APPLICATION))}
335 if icon == nil {show_error_and_panic("Missing icon")}
336 cursor := win32.LoadCursorW(nil, win32.wstring(win32._IDC_ARROW))
337 if cursor == nil {show_error_and_panic("Missing cursor")}
338 wcx := win32.WNDCLASSW {
339 style = win32.CS_HREDRAW | win32.CS_VREDRAW | win32.CS_OWNDC,
340 lpfnWndProc = wndproc,
341 hInstance = instance,
342 hIcon = icon,
343 hCursor = cursor,
344 lpszClassName = CLASS_NAME,
345 }
346 return win32.RegisterClassW(&wcx)
347}
348
349unregister_class :: proc(atom: win32.ATOM, instance: win32.HINSTANCE) {
350 if atom == 0 {show_error_and_panic("atom is zero")}
351 if !win32.UnregisterClassW(CLASS_NAME, instance) {show_error_and_panic("UnregisterClassW")}
352}
353
354adjust_size_for_style :: proc(size: ^Int2, dwStyle: win32.DWORD) {
355 rect := win32.RECT{0, 0, size.x, size.y}
356 if win32.AdjustWindowRect(&rect, dwStyle, false) {
357 size^ = {i32(rect.right - rect.left), i32(rect.bottom - rect.top)}
358 }
359}
360
361center_window :: proc(position: ^Int2, size: Int2) {
362 if deviceMode: win32.DEVMODEW; win32.EnumDisplaySettingsW(nil, win32.ENUM_CURRENT_SETTINGS, &deviceMode) {
363 device_size := Int2{i32(deviceMode.dmPelsWidth), i32(deviceMode.dmPelsHeight)}
364 position^ = (device_size - size) / 2
365 }
366}
367
368create_window :: #force_inline proc(instance: win32.HINSTANCE, atom: win32.ATOM, game: ^Game) -> win32.HWND {
369 if atom == 0 {show_error_and_panic("atom is zero")}
370 style :: win32.WS_OVERLAPPED | win32.WS_CAPTION | win32.WS_SYSMENU
371 size := game.window.size
372 pos := Int2{i32(win32.CW_USEDEFAULT), i32(win32.CW_USEDEFAULT)}
373 adjust_size_for_style(&size, style)
374 if .CENTER in game.window.control_flags {
375 center_window(&pos, size)
376 }
377 return win32.CreateWindowW(CLASS_NAME, game.window.name, style, pos.x, pos.y, size.x, size.y, nil, nil, instance, game)
378}
379
380message_loop :: proc() -> int {
381 msg: win32.MSG
382 for win32.GetMessageW(&msg, nil, 0, 0) > 0 {
383 win32.TranslateMessage(&msg)
384 win32.DispatchMessageW(&msg)
385 }
386 return int(msg.wParam)
387}
388
389run :: proc() -> int {
390 game := Game {
391 tick_rate = 300 * time.Millisecond,
392 last_tick = time.now(),
393 pause = true,
394 colors = {BLACK, WHITE},
395 size = WORLD_SIZE,
396 zoom = ZOOM,
397 window = Window{name = TITLE, size = WORLD_SIZE * ZOOM, fps = FPS, control_flags = {.CENTER}},
398 }
399 for i in 0 ..< PALETTE_COUNT {
400 c := u8((255 * int(i)) / (PALETTE_COUNT - 1))
401 game.colors[i] = {c, c, c, 255}
402 }
403
404 game.colors[0] = {128, 64, 32, 255}
405 game.colors[1] = {255, 128, 64, 255}
406
407 world := World{game.size.x, game.size.y, make([]u8, game.size.x * game.size.y)}
408 next_world := World{game.size.x, game.size.y, make([]u8, game.size.x * game.size.y)}
409 defer delete(world.alive)
410 defer delete(next_world.alive)
411 game.world = &world
412 game.next_world = &next_world
413
414 instance := win32.HINSTANCE(win32.GetModuleHandleW(nil))
415 if (instance == nil) {show_error_and_panic("No instance")}
416 atom := register_class(instance)
417 if atom == 0 {show_error_and_panic("Failed to register window class")}
418 defer unregister_class(atom, instance)
419
420 hwnd := create_window(instance, atom, &game)
421 if hwnd == nil {show_error_and_panic("Failed to create window")}
422 win32.ShowWindow(hwnd, win32.SW_SHOWDEFAULT)
423 win32.UpdateWindow(hwnd)
424
425 return message_loop()
426}
427
428main :: proc() {
429 exit_code := run()
430 os.exit(exit_code)
431}