mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-27 12:41:30 +11:00
479 lines
22 KiB
HTML
479 lines
22 KiB
HTML
<!DOCTYPE HTML>
|
|
<html lang="en" class="sidebar-visible no-js">
|
|
<head>
|
|
<!-- Book generated using mdBook -->
|
|
<meta charset="UTF-8">
|
|
<title>memory_game - Rust GBA Guide</title>
|
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
|
<meta name="description" content="">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="theme-color" content="#ffffff" />
|
|
|
|
<link rel="shortcut icon" href="../favicon.png">
|
|
<link rel="stylesheet" href="../css/variables.css">
|
|
<link rel="stylesheet" href="../css/general.css">
|
|
<link rel="stylesheet" href="../css/chrome.css">
|
|
<link rel="stylesheet" href="../css/print.css" media="print">
|
|
|
|
<!-- Fonts -->
|
|
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
|
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
|
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
|
|
|
<!-- Highlight.js Stylesheets -->
|
|
<link rel="stylesheet" href="../highlight.css">
|
|
<link rel="stylesheet" href="../tomorrow-night.css">
|
|
<link rel="stylesheet" href="../ayu-highlight.css">
|
|
|
|
<!-- Custom theme stylesheets -->
|
|
|
|
|
|
|
|
</head>
|
|
<body class="light">
|
|
<!-- Provide site root to javascript -->
|
|
<script type="text/javascript">var path_to_root = "../";</script>
|
|
|
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
|
<script type="text/javascript">
|
|
try {
|
|
var theme = localStorage.getItem('mdbook-theme');
|
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
|
|
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
|
}
|
|
|
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
|
}
|
|
} catch (e) { }
|
|
</script>
|
|
|
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
|
<script type="text/javascript">
|
|
var theme;
|
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
|
if (theme === null || theme === undefined) { theme = 'light'; }
|
|
document.body.className = theme;
|
|
document.querySelector('html').className = theme + ' js';
|
|
</script>
|
|
|
|
<!-- Hide / unhide sidebar before it is displayed -->
|
|
<script type="text/javascript">
|
|
var html = document.querySelector('html');
|
|
var sidebar = 'hidden';
|
|
if (document.body.clientWidth >= 1080) {
|
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
|
sidebar = sidebar || 'visible';
|
|
}
|
|
html.classList.remove('sidebar-visible');
|
|
html.classList.add("sidebar-" + sidebar);
|
|
</script>
|
|
|
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
|
<ol class="chapter"><li><a href="../introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li><a href="../ch00/index.html"><strong aria-hidden="true">2.</strong> Ch 0: Development Setup</a></li><li><a href="../ch01/index.html"><strong aria-hidden="true">3.</strong> Ch 1: Hello GBA</a></li><li><ol class="section"><li><a href="../ch01/hello1.html"><strong aria-hidden="true">3.1.</strong> hello1</a></li><li><a href="../ch01/volatile.html"><strong aria-hidden="true">3.2.</strong> Volatile</a></li><li><a href="../ch01/io_registers.html"><strong aria-hidden="true">3.3.</strong> IO Registers</a></li><li><a href="../ch01/the_display_control_register.html"><strong aria-hidden="true">3.4.</strong> The Display Control Register</a></li><li><a href="../ch01/video_memory_intro.html"><strong aria-hidden="true">3.5.</strong> Video Memory Intro</a></li><li><a href="../ch01/hello2.html"><strong aria-hidden="true">3.6.</strong> hello2</a></li></ol></li><li><a href="../ch02/index.html"><strong aria-hidden="true">4.</strong> Ch 2: User Input</a></li><li><ol class="section"><li><a href="../ch02/the_key_input_register.html"><strong aria-hidden="true">4.1.</strong> The Key Input Register</a></li><li><a href="../ch02/the_vcount_register.html"><strong aria-hidden="true">4.2.</strong> The VCount Register</a></li><li><a href="../ch02/light_cycle.html"><strong aria-hidden="true">4.3.</strong> light_cycle</a></li></ol></li><li><a href="../ch03/index.html"><strong aria-hidden="true">5.</strong> Ch 3: Memory and Objects</a></li><li><ol class="section"><li><a href="../ch03/gba_memory_mapping.html"><strong aria-hidden="true">5.1.</strong> GBA Memory Mapping</a></li><li><a href="../ch03/tile_data.html"><strong aria-hidden="true">5.2.</strong> Tile Data</a></li><li><a href="../ch03/regular_backgrounds.html"><strong aria-hidden="true">5.3.</strong> Regular Backgrounds</a></li><li><a href="../ch03/regular_objects.html"><strong aria-hidden="true">5.4.</strong> Regular Objects</a></li><li><a href="../ch03/gba_prng.html"><strong aria-hidden="true">5.5.</strong> GBA PRNG</a></li><li><a href="../ch03/memory_game.html" class="active"><strong aria-hidden="true">5.6.</strong> memory_game</a></li></ol></li></ol>
|
|
</nav>
|
|
|
|
<div id="page-wrapper" class="page-wrapper">
|
|
|
|
<div class="page">
|
|
|
|
<div id="menu-bar" class="menu-bar">
|
|
<div id="menu-bar-sticky-container">
|
|
<div class="left-buttons">
|
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
|
<i class="fa fa-bars"></i>
|
|
</button>
|
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
|
<i class="fa fa-paint-brush"></i>
|
|
</button>
|
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
|
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
|
</ul>
|
|
|
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
|
<i class="fa fa-search"></i>
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<h1 class="menu-title">Rust GBA Guide</h1>
|
|
|
|
<div class="right-buttons">
|
|
<a href="../print.html" title="Print this book" aria-label="Print this book">
|
|
<i id="print-button" class="fa fa-print"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="search-wrapper" class="hidden">
|
|
<form id="searchbar-outer" class="searchbar-outer">
|
|
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
|
</form>
|
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
|
<div id="searchresults-header" class="searchresults-header"></div>
|
|
<ul id="searchresults">
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
|
<script type="text/javascript">
|
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
|
});
|
|
</script>
|
|
|
|
<div id="content" class="content">
|
|
<main>
|
|
<a class="header" href="#making-a-memory-game" id="making-a-memory-game"><h1>Making A Memory Game</h1></a>
|
|
<p>For this example to show off our new skills we'll make a "memory" game. The idea
|
|
is that there's some face down cards and you pick one, it flips, you pick a
|
|
second, if they match they both go away, if they don't match they both turn back
|
|
face down. The player keeps going until all the cards are gone, then we'll deal
|
|
the cards again.</p>
|
|
<p>There are many steps to do to get such a simple seeming game going. In fact I
|
|
stumbled a bit myself when trying to get things set up and going despite having
|
|
written and explained all the parts so far. Accordingly, we'll take each part
|
|
very slowly, and review things as we build up our game.</p>
|
|
<p>We'll start back with a nearly blank file, calling it <code>memory_game.rs</code>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#![feature(start)]
|
|
#![no_std]
|
|
|
|
#[panic_handler]
|
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
|
loop {}
|
|
}
|
|
|
|
#[start]
|
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
|
loop {
|
|
// TODO the whole thing
|
|
}
|
|
}
|
|
</code></pre></pre>
|
|
<a class="header" href="#displaying-a-background" id="displaying-a-background"><h2>Displaying A Background</h2></a>
|
|
<p>First let's try to get a background going. We'll display a simple checker
|
|
pattern just so that we know that we did something.</p>
|
|
<p>Remember, backgrounds have the following essential components:</p>
|
|
<ul>
|
|
<li>Background Palette</li>
|
|
<li>Background Tiles</li>
|
|
<li>Screenblock</li>
|
|
<li>IO Registers</li>
|
|
</ul>
|
|
<a class="header" href="#background-palette" id="background-palette"><h3>Background Palette</h3></a>
|
|
<p>To write to the background palette memory we'll want to name a <code>VolatilePtr</code> for
|
|
it. We'll probably also want to be able to cast between different types either
|
|
right away or later in this program, so we'll add a method for that.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(transparent)]
|
|
pub struct VolatilePtr<T>(pub *mut T);
|
|
impl<T> VolatilePtr<T> {
|
|
pub unsafe fn read(&self) -> T {
|
|
core::ptr::read_volatile(self.0)
|
|
}
|
|
pub unsafe fn write(&self, data: T) {
|
|
core::ptr::write_volatile(self.0, data);
|
|
}
|
|
pub fn offset(self, count: isize) -> Self {
|
|
VolatilePtr(self.0.wrapping_offset(count))
|
|
}
|
|
pub fn cast<Z>(self) -> VolatilePtr<Z> {
|
|
VolatilePtr(self.0 as *mut Z)
|
|
}
|
|
}
|
|
#}</code></pre></pre>
|
|
<p>Now we give ourselves an easy way to write a color into a palbank slot.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
pub const BACKGROUND_PALETTE: VolatilePtr<u16> = VolatilePtr(0x500_0000 as *mut u16);
|
|
|
|
pub fn set_bg_palette_4bpp(palbank: usize, slot: usize, color: u16) {
|
|
assert!(palbank < 16);
|
|
assert!(slot > 0 && slot < 16);
|
|
unsafe {
|
|
BACKGROUND_PALETTE
|
|
.cast::<[u16; 16]>()
|
|
.offset(palbank as isize)
|
|
.cast::<u16>()
|
|
.offset(slot as isize)
|
|
.write(color);
|
|
}
|
|
}
|
|
#}</code></pre></pre>
|
|
<p>And of course we need to bring back in our ability to build color values, as
|
|
well as a few named colors to start us off:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
|
blue << 10 | green << 5 | red
|
|
}
|
|
|
|
pub const WHITE: u16 = rgb16(31, 31, 31);
|
|
pub const LIGHT_GRAY: u16 = rgb16(25, 25, 25);
|
|
pub const DARK_GRAY: u16 = rgb16(15, 15, 15);
|
|
#}</code></pre></pre>
|
|
<p>Which <em>finally</em> allows us to set our palette colors in <code>main</code>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
|
set_bg_palette_4bpp(0, 1, WHITE);
|
|
set_bg_palette_4bpp(0, 2, LIGHT_GRAY);
|
|
set_bg_palette_4bpp(0, 3, DARK_GRAY);
|
|
</code></pre></pre>
|
|
<a class="header" href="#background-tiles" id="background-tiles"><h3>Background Tiles</h3></a>
|
|
<p>So we'll want some light gray tiles and some dark gray tiles. We could use a
|
|
single tile and then swap it between palbanks to do the color selection, but for
|
|
now we'll just use two different tiles, since we've got tons of tile space to
|
|
spare.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
#[repr(transparent)]
|
|
pub struct Tile4bpp {
|
|
pub data: [u32; 8],
|
|
}
|
|
|
|
pub const ALL_TWOS: Tile4bpp = Tile4bpp {
|
|
data: [
|
|
0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222,
|
|
],
|
|
};
|
|
|
|
pub const ALL_THREES: Tile4bpp = Tile4bpp {
|
|
data: [
|
|
0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333,
|
|
],
|
|
};
|
|
#}</code></pre></pre>
|
|
<p>And then we have to have a way to put the tiles into video memory:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
#[derive(Clone, Copy)]
|
|
#[repr(transparent)]
|
|
pub struct Charblock4bpp {
|
|
pub data: [Tile4bpp; 512],
|
|
}
|
|
|
|
pub const VRAM: VolatilePtr<Charblock4bpp> = VolatilePtr(0x0600_0000 as *mut Charblock4bpp);
|
|
|
|
pub fn set_bg_tile_4bpp(charblock: usize, index: usize, tile: Tile4bpp) {
|
|
assert!(charblock < 4);
|
|
assert!(index < 512);
|
|
unsafe { VRAM.offset(charblock as isize).cast::<Tile4bpp>().offset(index as isize).write(tile) }
|
|
}
|
|
#}</code></pre></pre>
|
|
<p>And finally, we can call that within <code>main</code>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
|
// bg palette
|
|
set_bg_palette_4bpp(0, 1, WHITE);
|
|
set_bg_palette_4bpp(0, 2, LIGHT_GRAY);
|
|
set_bg_palette_4bpp(0, 3, DARK_GRAY);
|
|
// bg tiles
|
|
set_bg_tile_4bpp(0, 0, ALL_TWOS);
|
|
set_bg_tile_4bpp(0, 1, ALL_THREES);
|
|
</code></pre></pre>
|
|
<a class="header" href="#setup-a-screenblock" id="setup-a-screenblock"><h3>Setup A Screenblock</h3></a>
|
|
<p>Screenblocks are a little weird because they take the same space as the
|
|
charblocks (8 screenblocks per charblock). The GBA will let you mix and match
|
|
and it's up to you to keep it all straight. We're using tiles at the base of
|
|
charblock 0, so we'll place our screenblock at the base of charblock 1.</p>
|
|
<p>First, we have to be able to make one single screenblock entry at a time:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
#[repr(transparent)]
|
|
pub struct RegularScreenblockEntry(u16);
|
|
|
|
impl RegularScreenblockEntry {
|
|
pub const SCREENBLOCK_ENTRY_TILE_ID_MASK: u16 = 0b11_1111_1111;
|
|
pub fn from_tile_id(id: u16) -> Self {
|
|
RegularScreenblockEntry(id & Self::SCREENBLOCK_ENTRY_TILE_ID_MASK)
|
|
}
|
|
}
|
|
#}</code></pre></pre>
|
|
<p>And then with 32x32 of these things we'll have a whole screenblock. Now, we
|
|
probably won't actually make values of the screenblock type itself, but we at
|
|
least need it to have the type declared with the correct size so that we can
|
|
move our pointers around by the right amount.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
#[derive(Clone, Copy)]
|
|
#[repr(transparent)]
|
|
pub struct RegularScreenblock {
|
|
pub data: [RegularScreenblockEntry; 32 * 32],
|
|
}
|
|
#}</code></pre></pre>
|
|
<p>Alright, so, as I said those things are kinda big, we don't really want to be
|
|
building them up on the stack if we can avoid it, so we'll write one straight
|
|
into memory at the correct location.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
pub fn checker_screenblock(slot: usize, a_entry: RegularScreenblockEntry, b_entry: RegularScreenblockEntry) {
|
|
let mut p = VRAM.cast::<RegularScreenblock>().offset(slot as isize).cast::<RegularScreenblockEntry>();
|
|
let mut checker = true;
|
|
for _row in 0..32 {
|
|
for _col in 0..32 {
|
|
unsafe { p.write(if checker { a_entry } else { b_entry }) };
|
|
p = p.offset(1);
|
|
checker = !checker;
|
|
}
|
|
checker = !checker;
|
|
}
|
|
}
|
|
#}</code></pre></pre>
|
|
<p>And then we add this into <code>main</code></p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
// screenblock
|
|
let light_entry = RegularScreenblockEntry::from_tile_id(0);
|
|
let dark_entry = RegularScreenblockEntry::from_tile_id(1);
|
|
checker_screenblock(8, light_entry, dark_entry);
|
|
#}</code></pre></pre>
|
|
<a class="header" href="#background-io-registers" id="background-io-registers"><h3>Background IO Registers</h3></a>
|
|
<p>Our most important step is of course the IO register step. There's four
|
|
different background layers, but each of them has the same format for their
|
|
control register. For the moment, all that we care about is being able to set
|
|
the "screen base block" value.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
|
#[repr(transparent)]
|
|
pub struct BackgroundControlSetting(u16);
|
|
|
|
impl BackgroundControlSetting {
|
|
pub fn from_base_block(sbb: u16) -> Self {
|
|
BackgroundControlSetting(sbb << 8)
|
|
}
|
|
}
|
|
|
|
pub const BG0CNT: VolatilePtr<BackgroundControlSetting> = VolatilePtr(0x400_0008 as *mut BackgroundControlSetting);
|
|
#}</code></pre></pre>
|
|
<p>And... that's all it takes for us to be able to add a line into <code>main</code></p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
// bg0 control
|
|
unsafe { BG0CNT.write(BackgroundControlSetting::from_base_block(8)) };
|
|
#}</code></pre></pre>
|
|
<a class="header" href="#set-the-display-control-register" id="set-the-display-control-register"><h3>Set The Display Control Register</h3></a>
|
|
<p>We're finally ready to set the display control register and get things going.</p>
|
|
<p>We've slightly glossed over it so far, but when the GBA is first booted most
|
|
everything within the address space will be all zeroed. However, the display
|
|
control register has the "Force VBlank" bit enabled by the BIOS, giving you a
|
|
moment to put the memory in place that you'll need for the first frame.</p>
|
|
<p>So, now that have got all of our memory set, we'll overwrite the initial
|
|
display control register value with what we'll call "just enable bg0".</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
# #![allow(unused_variables)]
|
|
#fn main() {
|
|
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
|
#[repr(transparent)]
|
|
pub struct DisplayControlSetting(u16);
|
|
|
|
impl DisplayControlSetting {
|
|
pub const JUST_ENABLE_BG0: DisplayControlSetting = DisplayControlSetting(1 << 8);
|
|
}
|
|
|
|
pub const DISPCNT: VolatilePtr<DisplayControlSetting> = VolatilePtr(0x0400_0000 as *mut DisplayControlSetting);
|
|
#}</code></pre></pre>
|
|
<p>And so finally we have a complete <code>main</code></p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[start]
|
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
|
// bg palette
|
|
set_bg_palette_4bpp(0, 1, WHITE);
|
|
set_bg_palette_4bpp(0, 2, LIGHT_GRAY);
|
|
set_bg_palette_4bpp(0, 3, DARK_GRAY);
|
|
// bg tiles
|
|
set_bg_tile_4bpp(0, 0, ALL_TWOS);
|
|
set_bg_tile_4bpp(0, 1, ALL_THREES);
|
|
// screenblock
|
|
let light_entry = RegularScreenblockEntry::from_tile_id(0);
|
|
let dark_entry = RegularScreenblockEntry::from_tile_id(1);
|
|
checker_screenblock(8, light_entry, dark_entry);
|
|
// bg0 control
|
|
unsafe { BG0CNT.write(BackgroundControlSetting::from_base_block(8)) };
|
|
// Display Control
|
|
unsafe { DISPCNT.write(DisplayControlSetting::JUST_ENABLE_BG0) };
|
|
loop {
|
|
// TODO the whole thing
|
|
}
|
|
}
|
|
</code></pre></pre>
|
|
<p>And <em>It works, Marty! It works!</em></p>
|
|
<p><img src="screenshot_checkers.png" alt="screenshot_checkers" /></p>
|
|
|
|
</main>
|
|
|
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
|
<!-- Mobile navigation buttons -->
|
|
|
|
<a rel="prev" href="../ch03/gba_prng.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
|
<i class="fa fa-angle-left"></i>
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<div style="clear: both"></div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
|
|
|
<a href="../ch03/gba_prng.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
|
<i class="fa fa-angle-left"></i>
|
|
</a>
|
|
|
|
|
|
|
|
</nav>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
|
|
|
|
|
|
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
|
|
|
|
<!-- Custom JS scripts -->
|
|
|
|
|
|
|
|
|
|
</body>
|
|
</html>
|