<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://torlenor.org/feed.xml" rel="self" type="application/atom+xml" /><link href="https://torlenor.org/" rel="alternate" type="text/html" /><updated>2026-03-07T15:38:25+00:00</updated><id>https://torlenor.org/feed.xml</id><title type="html">Torlenor.org</title><subtitle>My private page where I am blogging about my coding adventures.</subtitle><entry><title type="html">Building Chirm: A calmer way to share information across a company</title><link href="https://torlenor.org/chirm/2026/03/05/chirm_1.html" rel="alternate" type="text/html" title="Building Chirm: A calmer way to share information across a company" /><published>2026-03-05T22:00:00+00:00</published><updated>2026-03-05T22:00:00+00:00</updated><id>https://torlenor.org/chirm/2026/03/05/chirm_1</id><content type="html" xml:base="https://torlenor.org/chirm/2026/03/05/chirm_1.html"><![CDATA[<p style="text-align: center;"><img src="/assets/img/chirm/Chirm-1.png" alt="Chirm overview" style="max-width: 40%; height: auto;" /></p>

<h1 id="introduction">Introduction</h1>

<p>In many companies, important operational updates are spread across too many places. Some things land in chat, some in email, some are mentioned in meetings and some are simply learned by accident. This gets especially messy once the company grows beyond a few tightly connected teams, or when remote work becomes normal, as it did especially since Covid.</p>

<p>Chat tools are great for fast back-and-forth communication, but they are not necessarily a good place for information that should remain visible, easy to catch up on, and available to the right people later on. A release update, a process change, a location-specific announcement or maybe a very helpful tool that was built in a team should not have to drown in a river of chat messages or Teams teams.</p>

<p>This is one of the reasons why I started building Chirm, an internal communication platform for calmer, asynchronous company-wide updates. The project is available at <a href="https://chirm.app/en">https://chirm.app/en</a>.</p>

<p>A mildly amusing detail is that this did not start as that grand product plan. At first, it was mainly a playground for microservice architecture and I thought that a microblogging-style application would be a good idea for experimenting with service boundaries, timelines, events and the general joys and pains of distributed systems. Only while building it did I notice that the thing I was building maps surprisingly well to solving a real communication problem inside companies. That is the point where the project started turning from a technical playground into what Chirm is now.</p>

<p>In this article I want to describe the problem Chirm is trying to solve, what kind of system I ended up building and a bit of the technical setup behind it. I may come up with some more technical articles later.</p>

<h1 id="setup">Setup</h1>

<p>The basic observation behind Chirm is simple: Many companies do not have a good calm, asynchronous place for the exchange of information.</p>

<p>There are of course many tools around already. Slack and Microsoft Teams are widely used, but they are primarily optimized for collaboration, direct interaction and quick exchange. That is useful, but it also creates a certain mode of communication where everything competes for immediate attention, the last chat message wins. Important updates can easily be buried under ongoing conversations, side discussions, quick questions and the usual background noise of daily work.</p>

<p>At the same time, alternatives on the more social-intranet side often try to do many things at once. That can be useful too, but it also increases complexity.</p>

<p>What I wanted instead was something much narrower: One central place for company communication, built around a personal timeline, where relevant updates can be read asynchronously, reacted to and discussed without creating the feeling that one has to constantly watch a chat window. Basically a social microblogging service inside a company.</p>

<p>And I came up with the core principle that guides the development: <em>The user’s time is precious, do not steal it.</em></p>

<p>This also means that Chirm is not supposed to replace every communication or collaboration tool inside a company. It is not a project management system, not a wiki and not a general-purpose chat tool. The goal is much more focused: Provide a clean channel for internal company communication that remains readable and useful as an organization grows.</p>

<p>Typical examples that should go over Chirm would be company-wide operational updates, release notes for internal teams, process changes, announcements from leadership, updates for specific departments or locations, and compliance-relevant information that should clearly reach the correct audience. But it should also work for smaller, useful things that normally stay trapped inside one team. A team might build a small internal tool, automate a repetitive task, improve a workflow or discover a useful practice. In many companies, that kind of progress remains local because nobody outside the team ever hears about it, especially since remote work increased the the coffee machine encounters do not happen that often anymore. In Chirm, sharing that should be as easy as writing a short post, so that other teams can react, comment and potentially adapt the idea for their own work.</p>

<p>This is where a timeline-oriented model becomes interesting. Instead of forcing all communication into chat threads or emails, updates can be posted into a structured shared space and consumed when people actually have time for it. And for the really important things, there are mechanisms that allow you to request confirmation, which also makes it a very natural fit for areas like QM.</p>

<h1 id="what-did-we-do">What did we do?</h1>

<p>From the beginning, I wanted Chirm to feel closer to a microblogging platform than to a classic enterprise chat tool, because the goal was not only to distribute important information, but also to make useful work inside a company more visible across team boundaries.</p>

<p>That means a few things. A post is the central object. It can belong to a group, have a topic, receive replies and reactions and remain visible in a timeline. The timeline itself is the main interface. Ideally, most of the daily interaction with the system happens there. Users should not need to jump through ten different menus to understand what is going on or to look into various chat channels or Teams teams to find something.</p>

<p>This also means the system should support both broad and restricted visibility. Some updates are relevant for everyone in a company, while others only make sense within a department, team or location. Therefore group handling and access control are part of the core model.</p>

<p>Another requirement was that the platform should work well in a multi-tenant setup. Chirm is meant to be used by different organizations, each with their own users, groups, permissions and data boundaries. So tenant separation was very important.</p>

<p>On the product side, the first goal was to make posting and reading updates feel straightforward and low effort. A user can create a post, assign it to a group or add a topic, and others can read, react and reply. The timeline then acts as the central view where relevant items come together. This is intentionally simple.</p>

<p>The result is a system where the same basic model can cover different scenarios. A leadership update, a team announcement, a release note, a department-specific operational notice or a short post about a useful internal tool all use the same underlying mechanism. That matters because products become confusing quite quickly when each use case feels like a separate application.</p>

<p>To support all of this technically, the backend was split into services with clearly separated responsibilities, without over-complicating things. There is an API gateway in front of the whole system, an authentication layer that supports SSO and identity providers, and behind it several domain-specific services. For example, posts are handled by the post service, while timeline generation is handled by a timeline service.</p>

<p>I only want to sketch the architecture here at a high level. I will go into more detail about the backend design and the reasoning behind specific architectural choices in a later post. The main point I want to make here is that the service structure follows the communication model quite directly. Writing and retrieving posts, generating timelines, searching content, sending notifications and handling tenant context all have different requirements.</p>

<p>It also supports AI summaries, to help users get a quick overview of activity in their timeline. This becomes more useful once a timeline grows denser or when people return after being away. I wanted summarization to be a distinct capability in the architecture rather than something mixed awkwardly into the rest of the system.</p>

<p>Because this is built for companies and large enterprises, tenants and groups are treated as first-class concepts from the start. Tenant handling determines which organization a user belongs to and what data context is active. Groups define smaller visibility domains inside that tenant. This is what makes it possible to support both broad communication and more focused internal channels without turning everything into one giant company-wide feed, and it is very easy to build company structure into the message flow structure.</p>

<p>The product philosophy and the architecture are therefore connected quite directly. If the goal is calmer communication, the system needs predictable timelines, good search, reliable notifications, clear visibility rules and a user interface that does not constantly fragment attention.</p>

<h1 id="summary-and-discussion">Summary and discussion</h1>

<p>Chirm started from a fairly ordinary observation: Companies often lack a good place for operational communication that is visible, structured and not overly noisy.</p>

<p>The default answer is often to use the tools that are already there, especially chat. That works up to a point, but it also creates a mode of communication where important updates are mixed with everything else and easily get lost.</p>

<p>But the issue is not only that important company information gets buried. Another problem is that useful work often stays invisible outside the team that did it. A small internal tool, an improved workflow, a practical script or a process improvement may be very valuable to others, but in many organizations nobody hears about it unless it happens to come up by chance. That means useful ideas stay local when they could have spread.</p>

<p>This is one of the things Chirm is meant to improve. It should not only support structured top-down communication, but also make it easier for teams to share useful things with the rest of the organization in a lightweight and visible way. A short post should be enough to make others aware, let them react, ask questions and reuse what is relevant for them.</p>

<p>Instead of trying to replace every existing tool, Chirm focuses on asynchronous company communication through a simple timeline-based model. The technical design follows from that goal. It should make important updates easier to find, but it should also help ideas, tools and practices travel further across the company.</p>

<p>There is of course still a lot of room for improvement. Internal communication is one of those domains that looks simple at first, but it easily turns into a mess. Building a system that allows communication to remain clear, relevant and pleasant across different teams and organizations is the harder part.</p>

<p>There is also a broader product question underneath all of this. Many software tools optimize for engagement, speed and constant activity, especially classical social media. But for companies this is often the wrong optimization target. Constant noise costs time, attention and therefore money. What matters more is that the right people get the right information at the right time, and that useful knowledge does not remain trapped inside isolated teams. This is what I want to achieve with Chirm.</p>

<h1 id="next-steps">Next Steps</h1>

<p>The immediate next steps for Chirm are not about adding more and more features, but about showing what this communication model can do in practice, both for important operational updates and for lightweight sharing across teams.</p>

<p>For that I continue improving the experience around targeted updates, making it easier to curate relevant information, expanding the document-sharing side and refining the timeline experience further. It also means validating the product in realistic organizational settings.</p>

<p>The overall direction is clear: Building a system that helps companies communicate important things clearly, without adding even more noise to the day.</p>

<p>If you want to see the current product direction or try the demo instance, you can find Chirm at <a href="https://chirm.app/en">https://chirm.app/en</a>.</p>

<p style="text-align: center;"><img src="/assets/img/chirm/use_case_synergien_1_screenshot_chirm_post.png" alt="" /></p>
<p style="text-align: center;"><em>Screenshot of Chirm</em></p>]]></content><author><name></name></author><category term="Chirm" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Using SDL2 in Rust</title><link href="https://torlenor.org/rust/graphics/gamedev/2023/09/16/sdl2_with_rust.html" rel="alternate" type="text/html" title="Using SDL2 in Rust" /><published>2023-09-16T22:00:00+00:00</published><updated>2023-09-16T22:00:00+00:00</updated><id>https://torlenor.org/rust/graphics/gamedev/2023/09/16/sdl2_with_rust</id><content type="html" xml:base="https://torlenor.org/rust/graphics/gamedev/2023/09/16/sdl2_with_rust.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Simple DirectMedia Layer or just simple SDL is a cross-platform library used for accessing video, audio, input devices like keyboard, mouse or joysticks, in addition to also providing some networking abstractions. Despite its age of already 25 years, it is still used extensively for games and other multimedia software as an abstraction layer, either using it to directly draw graphics and play sounds or as a lower-level library on which games engines are built.</p>

<p>While written mainly in C, a lot of language bindings where created and one of them is the Rust binding <a href="https://github.com/Rust-SDL2/rust-sdl2">rust-sdl2</a>, which we will introduce here. We will show how to open a window, draw a small thing and use the events system from SDL2 to handle keyboard inputs.</p>

<h1 id="linux-setup">Linux setup</h1>

<p>To get started with SDL2 in Rust, you first need the sdl2 library and headers installed on your system (the Rust crate has a bundled feature, where it compiles it from source, but we are not gonna talk about this here). Install the library using the appropriate package manager for your distribution.</p>

<p>Ubuntu:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>libsdl2-dev
</code></pre></div></div>

<p>Fedora:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>SDL2-devel
</code></pre></div></div>

<p>Arch:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>pacman <span class="nt">-S</span> sdl2
</code></pre></div></div>

<h1 id="windows-setup">Windows setup</h1>

<p>We assume you are using MSVC as your C++ compiler environment. if you are using MINGW, please see the crate documentation on how to continue.</p>

<ol>
  <li>
    <p>Download the MSVC version of SDL2 from http://www.libsdl.org/ (usually named something like <em>SDL2-devel-2.x.x-VC.zip</em>).</p>
  </li>
  <li>
    <p>Unzip <em>SDL2-devel-2.x.x-VC.zip</em>.</p>
  </li>
  <li>
    <p>Copy all <em>.lib</em> files from <code class="language-plaintext highlighter-rouge">SDL2-2.x.x\lib\x64\</code> to <code class="language-plaintext highlighter-rouge">%userprofile%\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\x86_64-pc-windows-msvc\lib\</code></p>
  </li>
  <li>
    <p>Copy <code class="language-plaintext highlighter-rouge">SDL2-2.x.x\lib\x64\SDL2.dll</code> into your project directory or into any directory which is in your PATH. When you want to distribute the compiled application, make sure the ship <code class="language-plaintext highlighter-rouge">SDL2.dll</code> right next to it, or it may not run, if the user doesn’t have the <code class="language-plaintext highlighter-rouge">SDL2.dll</code> lying around.</p>
  </li>
</ol>

<h1 id="creating-a-rust-project">Creating a Rust project</h1>

<p>If you haven’t installed it, yet, install Rust following the <a href="https://www.rust-lang.org/tools/install">“Install Rust”</a> guide.</p>

<p>Create a new Rust project by typing</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo new sdl2-example
</code></pre></div></div>

<p>Then change into the newly created directory and type</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo add sdl2 <span class="nt">-F</span> unsafe_textures
</code></pre></div></div>

<p>to add th SDL2 rust bindings.</p>

<p>We are going to use the <code class="language-plaintext highlighter-rouge">unsafe_textures</code> feature, even though we are not going to use any textures. Mainly because, if you do use textures, you will notice that without that option you are getting a lost of Rust <a href="https://doc.rust-lang.org/rust-by-example/scope/lifetime.html">lifetime</a> issues. However, this comes with he downside, that you have to manage the texture objects yourself and make sure to call destroy, if you do not need them any longer. For more information about this feature see <a href="https://doc.rust-lang.org/rust-by-example/scope/lifetime.html">here</a>.</p>

<p>You can also add other features which correspond to the different optional SDL2 libraries like gfx, mixer or tff.</p>

<p>When this was successful, you are ready to start developing with SDL2!</p>

<h1 id="opening-a-window">Opening a window</h1>

<p>Let’s open our <code class="language-plaintext highlighter-rouge">main.rs</code> file and start by creating a window inside the main function (note, we adapt the return value of the main function slightly):</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">sdl2</span><span class="p">::{</span><span class="nn">event</span><span class="p">::</span><span class="n">Event</span><span class="p">,</span> <span class="nn">keyboard</span><span class="p">::</span><span class="n">Keycode</span><span class="p">};</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="nb">String</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">sdl_context</span> <span class="o">=</span> <span class="nn">sdl2</span><span class="p">::</span><span class="nf">init</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">video_subsystem</span> <span class="o">=</span> <span class="n">sdl_context</span><span class="nf">.video</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>

    <span class="k">let</span> <span class="n">window</span> <span class="o">=</span> <span class="n">video_subsystem</span>
        <span class="nf">.window</span><span class="p">(</span><span class="s">"rust-sdl2 example"</span><span class="p">,</span> <span class="mi">800</span><span class="p">,</span> <span class="mi">600</span><span class="p">)</span>
        <span class="nf">.opengl</span><span class="p">()</span>
        <span class="nf">.build</span><span class="p">()</span>
        <span class="nf">.map_err</span><span class="p">(|</span><span class="n">e</span><span class="p">|</span> <span class="n">e</span><span class="nf">.to_string</span><span class="p">())</span><span class="o">?</span><span class="p">;</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">event_pump</span> <span class="o">=</span> <span class="n">sdl_context</span><span class="nf">.event_pump</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>

    <span class="nv">'main</span><span class="p">:</span> <span class="k">loop</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">event</span> <span class="k">in</span> <span class="n">event_pump</span><span class="nf">.poll_iter</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">match</span> <span class="n">event</span> <span class="p">{</span>
                <span class="nn">Event</span><span class="p">::</span><span class="n">Quit</span> <span class="p">{</span> <span class="o">..</span> <span class="p">}</span>
                <span class="p">|</span> <span class="nn">Event</span><span class="p">::</span><span class="n">KeyDown</span> <span class="p">{</span>
                    <span class="n">keycode</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Keycode</span><span class="p">::</span><span class="n">Escape</span><span class="p">),</span>
                    <span class="o">..</span>
                <span class="p">}</span> <span class="k">=&gt;</span> <span class="p">{</span>
                    <span class="k">break</span> <span class="nv">'main</span>
                <span class="p">},</span>
                <span class="n">_</span> <span class="k">=&gt;</span> <span class="p">{}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now let’s try to run the program</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo run
</code></pre></div></div>

<p>If everything compiles and runs successfully, you will see an empty window. This is fine, we are not drawing anything, yet. But you should be able to close the program with the ESC key.</p>

<p>Here you can already see, that SDL2 is not a full game engine, you really have to do a lot of things yourself, like maintaining an event loop and mapping the events that SDL2 captures to something meaningful, like closing the program.</p>

<h1 id="drawing-a-rectangle">Drawing a rectangle</h1>

<p>We are going to use build-in drawing functionalities from SDL2. They are suitable for drawing simple primitives and we will use one of them to draw a rectangle.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">sdl2</span><span class="p">::{</span><span class="nn">event</span><span class="p">::</span><span class="n">Event</span><span class="p">,</span> <span class="nn">keyboard</span><span class="p">::</span><span class="n">Keycode</span><span class="p">,</span> <span class="nn">pixels</span><span class="p">::</span><span class="n">Color</span><span class="p">,</span> <span class="nn">rect</span><span class="p">::</span><span class="n">Rect</span><span class="p">};</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="nb">String</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">sdl_context</span> <span class="o">=</span> <span class="nn">sdl2</span><span class="p">::</span><span class="nf">init</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">video_subsystem</span> <span class="o">=</span> <span class="n">sdl_context</span><span class="nf">.video</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>

    <span class="k">let</span> <span class="n">window</span> <span class="o">=</span> <span class="n">video_subsystem</span>
        <span class="nf">.window</span><span class="p">(</span><span class="s">"rust-sdl2 example"</span><span class="p">,</span> <span class="mi">800</span><span class="p">,</span> <span class="mi">600</span><span class="p">)</span>
        <span class="nf">.opengl</span><span class="p">()</span>
        <span class="nf">.build</span><span class="p">()</span>
        <span class="nf">.map_err</span><span class="p">(|</span><span class="n">e</span><span class="p">|</span> <span class="n">e</span><span class="nf">.to_string</span><span class="p">())</span><span class="o">?</span><span class="p">;</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">event_pump</span> <span class="o">=</span> <span class="n">sdl_context</span><span class="nf">.event_pump</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">canvas</span> <span class="o">=</span> <span class="n">window</span><span class="nf">.into_canvas</span><span class="p">()</span><span class="nf">.build</span><span class="p">()</span><span class="nf">.map_err</span><span class="p">(|</span><span class="n">e</span><span class="p">|</span> <span class="n">e</span><span class="nf">.to_string</span><span class="p">())</span><span class="o">?</span><span class="p">;</span>

    <span class="nv">'main</span><span class="p">:</span> <span class="k">loop</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">event</span> <span class="k">in</span> <span class="n">event_pump</span><span class="nf">.poll_iter</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">match</span> <span class="n">event</span> <span class="p">{</span>
                <span class="nn">Event</span><span class="p">::</span><span class="n">Quit</span> <span class="p">{</span> <span class="o">..</span> <span class="p">}</span>
                <span class="p">|</span> <span class="nn">Event</span><span class="p">::</span><span class="n">KeyDown</span> <span class="p">{</span>
                    <span class="n">keycode</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Keycode</span><span class="p">::</span><span class="n">Escape</span><span class="p">),</span>
                    <span class="o">..</span>
                <span class="p">}</span> <span class="k">=&gt;</span> <span class="k">break</span> <span class="nv">'main</span><span class="p">,</span>
                <span class="n">_</span> <span class="k">=&gt;</span> <span class="p">{}</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="c1">// Set the background</span>
        <span class="n">canvas</span><span class="nf">.set_draw_color</span><span class="p">(</span><span class="nn">Color</span><span class="p">::</span><span class="nf">RGB</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
        <span class="n">canvas</span><span class="nf">.clear</span><span class="p">();</span>

        <span class="c1">// Draw a red rectangle</span>
        <span class="n">canvas</span><span class="nf">.set_draw_color</span><span class="p">(</span><span class="nn">Color</span><span class="p">::</span><span class="nf">RGB</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
        <span class="n">canvas</span><span class="nf">.fill_rect</span><span class="p">(</span><span class="nn">Rect</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">600</span><span class="p">,</span> <span class="mi">400</span><span class="p">))</span><span class="o">?</span><span class="p">;</span>

        <span class="c1">// Show it on the screen</span>
        <span class="n">canvas</span><span class="nf">.present</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And that’s it!</p>

<h1 id="summary">Summary</h1>

<p>As you can see, using SDL2 in Rust is as straightforward as in C and I hope this small introduction could serve as a starting point for your SDL2 and Rust adventures.</p>

<p>And here is a screenshot of our running program:</p>

<p><img src="/assets/img/sdl2_rust/image.png" alt="rust2-sdl example window" /></p>]]></content><author><name></name></author><category term="Rust" /><category term="Graphics" /><category term="GameDev" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Ordinary least squares linear regression in Rust</title><link href="https://torlenor.org/machine%20learning/rust/2022/03/29/linear_regression_in_rust_from_scratch.html" rel="alternate" type="text/html" title="Ordinary least squares linear regression in Rust" /><published>2022-03-29T16:25:00+00:00</published><updated>2022-03-29T16:25:00+00:00</updated><id>https://torlenor.org/machine%20learning/rust/2022/03/29/linear_regression_in_rust_from_scratch</id><content type="html" xml:base="https://torlenor.org/machine%20learning/rust/2022/03/29/linear_regression_in_rust_from_scratch.html"><![CDATA[<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#linear-regression" id="markdown-toc-linear-regression">Linear Regression</a>    <ul>
      <li><a href="#what-is-linear-regression" id="markdown-toc-what-is-linear-regression">What is linear regression</a></li>
      <li><a href="#solution-of-the-equation" id="markdown-toc-solution-of-the-equation">Solution of the equation</a></li>
    </ul>
  </li>
  <li><a href="#implementation-in-rust" id="markdown-toc-implementation-in-rust">Implementation in Rust</a></li>
  <li><a href="#example-diabetes-dataset" id="markdown-toc-example-diabetes-dataset">Example: Diabetes dataset</a></li>
  <li><a href="#summary" id="markdown-toc-summary">Summary</a></li>
  <li><a href="#next-steps" id="markdown-toc-next-steps">Next steps</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>In contrast to the widespread use of Python and common machine learning packages like scikit-learn <a href="#r1">[1]</a>, there is an advantage in doing things from scratch. For example, learning how things work gives you an advantage in choosing the right algorithms for the job later down the line. We will start doing that with the simplest machine learning algorithm and maybe the most commonly used one: linear regression. In this article we are going to implement the so called ordinary least squares (OLS) <a href="#r2">[2]</a> linear regression <a href="#r3">[3]</a> in Rust <a href="#r4">[4]</a>. We will show that with just a few lines of code it is possible to implement this algorithm from scratch. We will then work through an example and compare it with known results. During this work we will gain a better understanding of the concept behind the algorithm and we learn about the Rust package called nalgebra <a href="#r5">[5]</a>, which will help us with our linear algebra needs.</p>

<h1 id="linear-regression">Linear Regression</h1>

<h2 id="what-is-linear-regression">What is linear regression</h2>

<p>Linear regression <a href="#r3">[3]</a> is used to model the relationship between a response/target variable and (multiple) explanatory variables or parameters. It is called linear, because the coefficients in the model are linear. A linear model for the target variable $y_i$ can be written in the form</p>

\[y_i = \beta_{0} + \beta_{1} x_{i1} + \cdots + \beta_{p} x_{ip} + \varepsilon_i \, ,
 \qquad i = 1, \ldots, n \; ,\]

<p>where $x_{ip}$ are the explanatory variables and $\beta_{p}$ are unknown coefficients. The $\varepsilon_i$ are called error terms or noise and they capture all the other information that we cannot explain with the linear model.</p>

<p>It is much easier to work with these equations if one writes them in matrix form as</p>

\[\mathbf{y} = X\boldsymbol\beta + \boldsymbol\varepsilon \, ,\]

<p>where all the $n$ equations are squashed together. As we will see below, this notation is useful for deriving our method of determining the parameters $\boldsymbol\beta$. Note: Here we integrated the $\beta_0$ in the $\boldsymbol\beta$ and therefore $\boldsymbol\beta$ is now a $(p+1)$-dimensional vector and $\mathbf{X}$ is now a (n, p+1)-dimensional matrix, where we include a constant first column, i.e., $x_{i0}=1$ for $i = 1, \ldots, n$.</p>

<p>The goal is to get values for all the $\boldsymbol\beta$ fulfilling the equation above.</p>

<h2 id="solution-of-the-equation">Solution of the equation</h2>

<p>Ordinary least squares (OLS) <a href="#r2">[2]</a> is, as the name suggests, a least squares method for find the unknown parameters $\boldsymbol\beta$ for a linear regression model. The idea is to minimize the sum of squares of the differences between the observed target variable and the predicted target variable coming from the linear regression model.</p>

<p>For the linear case the minimization problem possesses a unique global minimum and its solution can be expressed by an explicit formula for the coefficients $\boldsymbol\beta$:</p>

\[\boldsymbol\beta = (\mathbf{X}^\mathbf{T}\mathbf{X})^{-1}\mathbf{X}^\mathbf{T}\mathbf{y}\]

<p>As we can see here, we have to calculate a matrix inverse and we have to make some assumptions on the input values to guarantee that the solution exists and the matrix is invertible. One of these assumptions is, for example, that the column vectors in $\mathbf{X}$ are linearly independent.</p>

<p>If you are interested in the derivation of this solution, please take a look at the linked Wikipedia page or any good statistics book.</p>

<h1 id="implementation-in-rust">Implementation in Rust</h1>

<p>Finally, we are at the point where we can start implementing the algorithm. As it is basically given by some matrix/vector multiplications and inversions, we have two choices: either we implement the matrix/vector options ourselves or we use a library. As I want to focus on the algorithm implementation, I chose to use a library (nalgebra <a href="#r5">[5]</a>).</p>

<p>Before we start, we need to bring in the nalgebra functions that we are going to use</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="k">crate</span> <span class="n">nalgebra</span> <span class="k">as</span> <span class="n">na</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">ops</span><span class="p">::</span><span class="nb">Mul</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">na</span><span class="p">::{</span><span class="n">DMatrix</span><span class="p">,</span> <span class="n">DVector</span><span class="p">};</span>
</code></pre></div></div>

<p>Then we define the x values and the y values that we want to fit</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">x_training_values</span> <span class="o">=</span> <span class="nn">na</span><span class="p">::</span><span class="nd">dmatrix!</span><span class="p">[</span>
    <span class="mf">1.0f64</span><span class="p">,</span> <span class="mf">3.0f64</span><span class="p">;</span>
    <span class="mf">2.0f64</span><span class="p">,</span> <span class="mf">1.0f64</span><span class="p">;</span>
    <span class="mf">3.0f64</span><span class="p">,</span> <span class="mf">8.0f64</span><span class="p">;</span>
<span class="p">];</span>
<span class="k">let</span> <span class="n">y_values</span> <span class="o">=</span> <span class="nn">na</span><span class="p">::</span><span class="nd">dvector!</span><span class="p">[</span><span class="mf">2.0f64</span><span class="p">,</span> <span class="mf">3.0f64</span><span class="p">,</span> <span class="mf">4.0f64</span><span class="p">];</span>
</code></pre></div></div>
<p>As you can see, we use two x variables, which means we are going to have a model of the form</p>

\[y = \beta_{0} + \beta_{1} x_{1} + \beta_{2} x_{2} \; ,\]

<p>Fitting the model can be performed as follows:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">beta</span> <span class="o">=</span> <span class="n">x_values</span>
    <span class="nf">.tr_mul</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x_values</span><span class="p">)</span>
    <span class="nf">.try_inverse</span><span class="p">()</span>
    <span class="nf">.unwrap</span><span class="p">()</span>
    <span class="nf">.mul</span><span class="p">(</span><span class="n">x_values</span><span class="nf">.transpose</span><span class="p">())</span>
    <span class="nf">.mul</span><span class="p">(</span><span class="n">y_values</span><span class="p">);</span>
</code></pre></div></div>
<p>This also tries to calculate the inverse of the $X^TX$ matrix. As this can fail, if the matrix is not invertible (for example because the columns in $x$ are not linearly independent), in a production environment one should handle this gracefully and don’t use unwrap. For our coding example, however, it is good enough.</p>

<p>Getting predictions is then as simple as</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">prediction</span> <span class="o">=</span> <span class="n">x_values</span><span class="nf">.mul</span><span class="p">(</span><span class="n">beta</span><span class="p">);</span>
</code></pre></div></div>

<p>The full source code for LinearRegression can be seen here (excuse the unwraps). It contains a bit more boilerplate, but also supports fitting without the intercept term, which can be useful if the data is already centered around its mean (in which case the intercept would be zero).</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">ops</span><span class="p">::</span><span class="nb">Mul</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">na</span><span class="p">::{</span><span class="n">DMatrix</span><span class="p">,</span> <span class="n">DVector</span><span class="p">};</span>

<span class="cd">/// Ordinary least squares linear regression</span>
<span class="cd">///</span>
<span class="cd">/// It fits a linear model of the form y = b_0 + b_1*x + w_2*x_2 + ...</span>
<span class="cd">/// which minimizes the residual sum of squared between the observed targets</span>
<span class="cd">/// and the predicted targets.</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">LinearRegression</span> <span class="p">{</span>
    <span class="n">w</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">DVector</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="n">fit_intercept</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">LinearRegression</span> <span class="p">{</span>
    <span class="cd">/// Returns a linear regressor using ordinary least squares linear regression</span>
    <span class="cd">///</span>
    <span class="cd">/// # Arguments</span>
    <span class="cd">///</span>
    <span class="cd">/// * `fit_intercept` - Whether to fit a intercept for this model.</span>
    <span class="cd">///     If false assume that the data is centered, i.e., intercept is 0.</span>
    <span class="cd">///</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">fit_intercept</span><span class="p">:</span> <span class="nb">bool</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">LinearRegression</span> <span class="p">{</span>
        <span class="n">LinearRegression</span> <span class="p">{</span>
            <span class="n">w</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
            <span class="n">fit_intercept</span><span class="p">,</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="cd">/// Fit the model</span>
    <span class="cd">///</span>
    <span class="cd">/// # Arguments</span>
    <span class="cd">///</span>
    <span class="cd">/// * `x_values` - parameters of shape (n_samples, n_features)</span>
    <span class="cd">/// * `y_values` - target values of shape (n_samples)</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">fit</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">x_values</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">DMatrix</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">y_values</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">DVector</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">self</span><span class="py">.fit_intercept</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">x_values</span> <span class="o">=</span> <span class="n">x_values</span><span class="nf">.clone</span><span class="p">()</span><span class="nf">.insert_column</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">);</span>
            <span class="k">self</span><span class="nf">._fit</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x_values</span><span class="p">,</span> <span class="n">y_values</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">self</span><span class="nf">._fit</span><span class="p">(</span><span class="n">x_values</span><span class="p">,</span> <span class="n">y_values</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">fn</span> <span class="nf">_fit</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">x_values</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">DMatrix</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">y_values</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">DVector</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.w</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span>
            <span class="n">x_values</span>
                <span class="nf">.tr_mul</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x_values</span><span class="p">)</span>
                <span class="nf">.try_inverse</span><span class="p">()</span>
                <span class="nf">.unwrap</span><span class="p">()</span>
                <span class="nf">.mul</span><span class="p">(</span><span class="n">x_values</span><span class="nf">.transpose</span><span class="p">())</span>
                <span class="nf">.mul</span><span class="p">(</span><span class="n">y_values</span><span class="p">),</span>
        <span class="p">);</span>
    <span class="p">}</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">coef</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="nb">Option</span><span class="o">&lt;</span><span class="n">DVector</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
        <span class="c1">// TODO: Do not return 0th entry if fit_intercept is active</span>
        <span class="k">return</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.w</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">intercept</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="nb">f64</span><span class="p">,</span> <span class="nb">String</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="o">!</span><span class="k">self</span><span class="py">.w</span><span class="nf">.is_some</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="s">"Model was not fitted"</span><span class="nf">.to_string</span><span class="p">());</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="k">self</span><span class="py">.fit_intercept</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Ok</span><span class="p">(</span><span class="k">self</span><span class="py">.w</span><span class="nf">.as_ref</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()[</span><span class="mi">0</span><span class="p">]);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="s">"Model was not fitted with intercept"</span><span class="nf">.to_string</span><span class="p">());</span>
    <span class="p">}</span>

    <span class="cd">/// Returns the predictions using the provided parameters `x_values`</span>
    <span class="cd">///</span>
    <span class="cd">/// # Arguments</span>
    <span class="cd">///</span>
    <span class="cd">/// * `x_values` - parameters of shape (n_samples, n_features)</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">predict</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">x_values</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">DMatrix</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">DVector</span><span class="o">&lt;</span><span class="nb">f64</span><span class="o">&gt;</span><span class="p">,</span> <span class="nb">String</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="o">=</span> <span class="k">self</span><span class="py">.w</span><span class="nf">.as_ref</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">if</span> <span class="k">self</span><span class="py">.fit_intercept</span> <span class="p">{</span>
                <span class="k">let</span> <span class="n">x_values</span> <span class="o">=</span> <span class="n">x_values</span><span class="nf">.clone</span><span class="p">()</span><span class="nf">.insert_column</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">);</span>
                <span class="k">let</span> <span class="n">res</span> <span class="o">=</span> <span class="n">x_values</span><span class="nf">.mul</span><span class="p">(</span><span class="n">w</span><span class="p">);</span>
                <span class="k">return</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">res</span><span class="p">);</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="k">let</span> <span class="n">res</span> <span class="o">=</span> <span class="n">x_values</span><span class="nf">.mul</span><span class="p">(</span><span class="n">w</span><span class="p">);</span>
                <span class="k">return</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">res</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="nf">Err</span><span class="p">(</span><span class="s">"Model was not fitted."</span><span class="nf">.to_string</span><span class="p">())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is a crude version of this implementation and assumes fixed data types. For using it in a more convenient way, a bit more work has to be invested. If I decide to upload it to my GitHub page, I will update this blog post.</p>

<h1 id="example-diabetes-dataset">Example: Diabetes dataset</h1>

<p>As an example we will follow the scikit-learn example about <a href="https://scikit-learn.org/stable/auto_examples/linear_model/plot_ols.html">OLS</a>. There they use a feature of the diabetes dataset <a href="#r6">[6]</a> and perform linear regression on it. In addition, they calculate the mean squared error and the $R^2$ score. We will do the same and also try to fit the model using more features and see if it increases/decreases the score.</p>

<p>The first thing that we try it, is to take the full dataset (442 records, 10 feature variables $x$, standardized to have mean 0 and $\sum(x_i^2)=1$ and the last column is the variable that we want to predict) and try to fit it with our code.</p>

<p>We decide, as in the scikit-learn example, that we want to take <em>bmi</em> (3rd column in the dataset) as our feature variable and we want to predict y (the last column in the dataset). After reading them into an nalgebra matrix and vector, we can fit the model with</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="k">mut</span> <span class="n">model</span> <span class="o">=</span> <span class="nn">LinearRegression</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
<span class="n">model</span><span class="nf">.fit</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x_values</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">y_values</span><span class="p">);</span>
</code></pre></div></div>
<p>where the parameter <code class="language-plaintext highlighter-rouge">true</code> indicates that we want to fit the intercept.</p>

<p>The result of the fit provides us with the model parameters</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Coeffs: 949.435260383949
Intercept: 152.1334841628967
</code></pre></div></div>
<p>We can also calculate the scores based on the true values, which gives us</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MSE of fit: 3890.456585461273
R^2 of fit: 0.3439237602253802
</code></pre></div></div>
<p>and we can plot the regression line as seen in Fig. 1.</p>

<p style="text-align: center;"><img src="/assets/img/ols/results_diabetes_full_data.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 1: Fit to the complete diabetes dataset. The purple circles indicate the true data points and the red line indicates the linear model.</em></p>

<p>We can also try it with additional features, let’s say taking not only the <em>bmi</em>, but also the cholesterol values <em>ldl</em> and <em>hdl</em>. For that model we get the scores</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MSE of fit: 3669.2644919453955
R^2 of fit: 0.3812250059259711
</code></pre></div></div>
<p>which seems to be a slight improvement and the model seems to explain a bit more of the variance in the data.</p>

<p>As usually, you split the data into a training and a test set, we will do the same and follow exactly the scikit-learn example. This will serve as the final test! Can we reproduce the scikit-learn results with our code? In their example, they take the first 422 records as the training set and the last 20 records as the test set. We will do the same!</p>

<p>After reading the data and performing the split, we train our model again using the <em>bmi</em>. We find as the model parameters</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Coeffs: 938.2378612512634
Intercept: 152.91886182616173
</code></pre></div></div>
<p>and as scores</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MSE of fit: 2548.072398725972
R^2 of fit: 0.472575447982271
</code></pre></div></div>
<p>which is exactly as in the scikit-learn example. We can also reproduce the plot that they show (see Fig. 2).</p>

<p style="text-align: center;"><img src="/assets/img/ols/results_diabetes_scikit_learn_example.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 2: Fit to the complete diabetes dataset. The purple circles indicate the true data points and the red line indicates the linear model. Here we show the result of the validation data set.</em></p>

<p>So, we did it. We just implemented (I have to admit, a quite simple) machine learning algorithm ourselves and made sure that the results are exactly the same as in one of the most used Python libraries out there.</p>

<h1 id="summary">Summary</h1>

<p>This concludes this brief excursion into linear regression and demonstrates, that some things are not that complicated as they seem and it may make sense to implement some of those algorithms from scratch to get a better understanding what they do and what’s behind all the magic.</p>

<h1 id="next-steps">Next steps</h1>

<p>Possible next steps from here could be:</p>
<ul>
  <li>Implementation of (stochastic) gradient descent linear regression</li>
  <li>Look into classification models (perceptron, k-nearest neighbors, logistic regression)</li>
  <li>Explore unsupervised methods, like support vector machines (SVMs)</li>
</ul>

<h1 id="references">References</h1>

<p>[1]<a name="r1"></a> <a href="https://scikit-learn.org/stable/">https://scikit-learn.org/stable/</a><br />
[2]<a name="r2"></a> <a href="https://en.wikipedia.org/wiki/Ordinary_least_squares">https://en.wikipedia.org/wiki/Ordinary_least_squares</a><br />
[3]<a name="r3"></a> <a href="https://en.wikipedia.org/wiki/Linear_regression">https://en.wikipedia.org/wiki/Linear_regression</a><br />
[4]<a name="r4"></a> <a href="https://www.rust-lang.org/">https://www.rust-lang.org/</a><br />
[5]<a name="r5"></a> <a href="https://nalgebra.org/">https://nalgebra.org/</a><br />
[6]<a name="r6"></a> Bradley Efron, Trevor Hastie, Iain Johnstone and Robert Tibshirani (2004) “Least Angle Regression,” Annals of Statistics (with discussion), 407-499, <a href="https://www4.stat.ncsu.edu/~boos/var.select/diabetes.html">https://www4.stat.ncsu.edu/~boos/var.select/diabetes.html</a></p>]]></content><author><name></name></author><category term="Machine Learning" /><category term="Rust" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">A CI/CD pipeline with GitLab and Kubernetes - the simple way</title><link href="https://torlenor.org/linux/k8s/devops/2020/12/23/deployment_on_k8s_from_gitlab_cicd.html" rel="alternate" type="text/html" title="A CI/CD pipeline with GitLab and Kubernetes - the simple way" /><published>2020-12-23T11:00:00+00:00</published><updated>2020-12-23T11:00:00+00:00</updated><id>https://torlenor.org/linux/k8s/devops/2020/12/23/deployment_on_k8s_from_gitlab_cicd</id><content type="html" xml:base="https://torlenor.org/linux/k8s/devops/2020/12/23/deployment_on_k8s_from_gitlab_cicd.html"><![CDATA[<p>To speed up the development process for a new project, we were investigating the possibility to integrating our Kubernetes (k8s) cluster into our GitLab instance. It turned out, that all of the examples and tutorials we found were either way to complicated (examples repos, Medium articles), or not helpful at all because they omitted crucial parts (the GitLab documentation on deployment). So we decided to write an up-to-date tutorial.</p>

<p>This tutorial will cover how to integrate a running k8s cluster into GitLab (as a cluster not managed by GitLab), how to install the runner and, most importantly, how to write a <code class="language-plaintext highlighter-rouge">.gitlab-ci.yml</code> file which builds a Docker image, pushes it into the GitLab Container registry and does the deployment. What we will not cover, is the installation of the cluster or of the GitLab instance.</p>

<p>Do not fear, it is much easier than you think!</p>

<p>Disclaimer: we do not take any responsibility for bricked GitLab instances or k8s clusters!</p>

<h1 id="requirements">Requirements</h1>

<ul>
  <li>An up and running Kubernetes cluster and admin rights on it.</li>
  <li>A current installation of GitLab (tested on 13.6 and  13.7) and a user with Admin permissions.</li>
  <li>GitLab must be able to reach the Kubernetes API port.</li>
  <li>An example project to build and deploy on the cluster with an initial k8s deployment ready (we will also provide an example deployment yaml for k8s if you shouldn’t have one).</li>
</ul>

<h1 id="connecting-gitlab-with-the-k8s-cluster">Connecting GitLab with the k8s cluster</h1>

<p>The first step is to enable GitLab to speak with our k8s cluster.</p>

<p>The following steps assume you are suing GitLab 13.6 or 13.7.</p>

<ol>
  <li>First go to the <strong>Admin area</strong> in your GitLab instance and the navigate to the <strong>Kubernetes</strong> section.</li>
  <li>Click on <strong>Connect cluster with certificate</strong>.</li>
  <li>Switch to the <strong>Connect existing cluster</strong> tab.</li>
  <li>Enter your desired name for the cluster. This name will be used through GitLab to identify the cluster.</li>
  <li>(Optional) specify which <strong>Environment scope</strong> the cluster is used for. This lets you split testing/staging/production environments into separate k8s cluster. Keep the default “*” if you are unsure.</li>
  <li>Enter the <strong>API URL</strong>. It usually has the form <code class="language-plaintext highlighter-rouge">https://some_host_name_or_address:6443</code>.</li>
  <li>On your k8s cluster type <code class="language-plaintext highlighter-rouge">kubectl get secrets</code> and find the line with the name of your default token. It has the form of <code class="language-plaintext highlighter-rouge">default-token-&lt;something&gt;</code>.</li>
  <li>Enter
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> kubectl get secret default-token-&lt;something&gt; <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s2">"{['data']['ca</span><span class="se">\.</span><span class="s2">crt']}"</span> | <span class="nb">base64</span> <span class="nt">--decode</span>
</code></pre></div>    </div>
    <p>where you replace <em>default-token-&lt;something&gt;</em> with what you found with the command above.</p>
  </li>
  <li>You should get an output like that:
    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code> -----BEGIN CERTIFICATE-----
 A LOT OF CHARACTERS
 -----END CERTIFICATE-----
</code></pre></div>    </div>
    <p>Copy the whole output (including the “—” lines) and past it into the <strong>CA Certificate</strong> field.</p>
  </li>
  <li>Now we have to create a service account for GitLab on the cluster. Create a file <em>gitlab-admin-service-account.yaml</em> with the following contents:
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
  <span class="na">kind</span><span class="pi">:</span> <span class="s">ServiceAccount</span>
  <span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">gitlab</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">kube-system</span>
  <span class="s">---</span>
  <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io/v1beta1</span>
  <span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterRoleBinding</span>
  <span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">gitlab-admin</span>
  <span class="na">roleRef</span><span class="pi">:</span>
  <span class="na">apiGroup</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io</span>
  <span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterRole</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">cluster-admin</span>
  <span class="na">subjects</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">kind</span><span class="pi">:</span> <span class="s">ServiceAccount</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">gitlab</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">kube-system</span>
</code></pre></div>    </div>
    <p>and type</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> gitlab-admin-service-account.yaml
</code></pre></div>    </div>
    <p>to apply it. The expected output is</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>serviceaccount <span class="s2">"gitlab"</span> created
clusterrolebinding <span class="s2">"gitlab-admin"</span> created
</code></pre></div>    </div>
  </li>
  <li>Type
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nt">-n</span> kube-system describe secret <span class="si">$(</span>kubectl <span class="nt">-n</span> kube-system get secret | <span class="nb">grep </span>gitlab | <span class="nb">awk</span> <span class="s1">'{print $1}'</span><span class="si">)</span>
</code></pre></div>    </div>
    <p>to get the token for that newly created account. Paste everything from the <em>token</em> key into the <strong>Service Token</strong> field.</p>
  </li>
  <li>Unselect <strong>GitLab-managed cluster</strong> because we do not want GitLab to manage the cluster.</li>
  <li>Finally click on <strong>Add Kubernetes cluster</strong> and GitLab should now be able to talk to k8s.</li>
</ol>

<h1 id="install-the-gitlab-runner-onto-the-k8s-cluster">Install the GitLab Runner onto the k8s cluster</h1>

<p>This step is easy: Go to <strong>Admin Area</strong> - <strong>Kubernetes</strong> and click on your clusters name. On the <strong>Applications</strong> tab search for GitLab Runner and click <strong>Install</strong>. After a few seconds you should have an installed and fully integrated shared runner in your GitLab instance.</p>

<h1 id="writing-a-gitlab-cicd-configuration-for-deployment-on-the-k8s-cluster">Writing a GitLab CI/CD configuration for deployment on the k8s cluster</h1>

<p>For the next step you need an example project which you can pack into a Docker image and deploy on your cluster. Because we want to manage our deployments and yaml files for k8s in a separate repository, We usually create the deployment for the application once by hand and use GitLab to modify the deployment to roll out the newest version of the application.</p>

<h2 id="creating-the-initial-deployment-from-a-yaml-file">Creating the initial deployment from a yaml file</h2>

<p>The following snippet is a deployment declaration for a simple, generic application. We omitted all the additional things you may need, like service or ingress, because this would be beyond the scope of this article.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">my-app-namespace</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">my-app</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">my-app</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">image</span><span class="pi">:</span> <span class="s">myapp:latest</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
</code></pre></div></div>

<p>This assumes there is a namespace <em>my-app-namespace</em> were you can deploy to and that it does not need image pull secrets (see https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ if you should need that).</p>

<p>After applying this deployment, we are good to go to create the CI/CD pipeline (the central part of this article).</p>

<h2 id="creating-a-cicd-gitlab-pipeline-including-k8s-deployment">Creating a CI/CD GitLab pipeline including k8s deployment</h2>

<p>In contrast to all the examples we found, it is very easy to deploy a new version via a CI/CD pipeline if you have a GitLab integrated k8s cluster, because GitLab will provide the pipeline will the necessary credentials to deploy to the cluster.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">stages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">build_image</span>
  <span class="pi">-</span> <span class="s">deploy</span>

<span class="na">create_docker_image</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">build_image</span>
  <span class="na">image</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">gcr.io/kaniko-project/executor:debug</span>
    <span class="na">entrypoint</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">mkdir -p /kaniko/.docker</span>
    <span class="pi">-</span> <span class="s">echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" &gt; /kaniko/.docker/config.json</span>
    <span class="pi">-</span> <span class="s">/kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA</span>

<span class="na">deploy_production</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">deploy</span>
  <span class="na">when</span><span class="pi">:</span> <span class="s">manual</span>
  <span class="na">dependencies</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">create_docker_image</span>
  <span class="na">image</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">bitnami/kubectl:latest</span>
    <span class="na">entrypoint</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
  <span class="na">environment</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">production</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">https://my-app.com</span>
    <span class="na">kubernetes</span><span class="pi">:</span>
      <span class="na">namespace</span><span class="pi">:</span> <span class="s">my-app</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">kubectl set image deployment/my-app my-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA</span>
    <span class="pi">-</span> <span class="s">kubectl rollout status deployment/my-app --timeout=10s</span>
</code></pre></div></div>

<p>This pipeline definition contains two very useful examples: The first is how to build a Docker image without Docker-in-Docker, Docker-from-Docker or any bare metal Docker installation and without any superuser rights. The awesome kaniko project provides a Docker compatible way to build Docker image from a Dockerfile inside a k8s cluster without compromising security. Here it is used to build the image for our application and to automatically push it into the GitLab Container registry.</p>

<p>The second part is the deployment part. We are using the bitnami/kubectl image which provides us with the kubectl command. The actual deployment is just two lines! How is that possible? Well, in contrast to many of the examples we found, you do not need to worry about the k8s connection and credentials anymore, because GitLab provides a fully working KUBECONFIG as environment variable and kubectl will automatically use this to connect to the cluster. GitLab will also make sure, that you are only modifying the namespace which is defined in the environment section of your yaml file. If you should need to modify deployments in other namespaces, you will have to go through the ordeal of providing your own credentials for the cluster.</p>

<p>Feel free to omit the second script line or increase the timeout. It is a useful command to make sure the pipeline fails when the deployment fails. If you are using a lot of replicas, large images or other settings which would make the rollout much slower, you will have to increase the timeout or the pipeline step will fail.</p>

<h1 id="running-the-pipeline">Running the pipeline</h1>

<p>When you push something to the project repository, the first part, creating the Docker image, will always run. The second part, the deployment is marked as manual, i.e., it has to be triggered by hand via GitLab (Fig. 1). This is useful for production deployments. For testing you could automatically deploy, if you want.</p>

<p style="text-align: center;"><img src="/assets/img/gitlab_k8s/pipeline_1.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 1: Finished first step of the pipeline, building the Docker image.</em></p>

<p>To start the deployment click on the Play symbol on the right hand side and then select the stage you want to run. In our case this is “deploy_production” (see Fig. 2). This will start the deployment on the cluster.</p>

<p style="text-align: center;"><img src="/assets/img/gitlab_k8s/pipeline_2.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 2: Starting the deployment.</em></p>

<p>The output of the job should look similar to the output in the following image (Fig. 3).</p>

<p style="text-align: center;"><img src="/assets/img/gitlab_k8s/pipeline_3.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 3: Deployment job output.</em></p>

<h1 id="summary">Summary</h1>

<p>We have shown how to integrate an existing k8s cluster into GitLab and how to use it for building and deploying an application. In contrast to many believes, this is much easier than doing it, for example, on a bare metal Docker installation. K8s already has a lot of advantages and together with GitLab it becomes very simple to automate deployments and build complete CI/CD pipelines.</p>]]></content><author><name></name></author><category term="Linux" /><category term="k8s" /><category term="DevOps" /><summary type="html"><![CDATA[To speed up the development process for a new project, we were investigating the possibility to integrating our Kubernetes (k8s) cluster into our GitLab instance. It turned out, that all of the examples and tutorials we found were either way to complicated (examples repos, Medium articles), or not helpful at all because they omitted crucial parts (the GitLab documentation on deployment). So we decided to write an up-to-date tutorial.]]></summary></entry><entry><title type="html">Teaching an AI how to play the classic game Snake</title><link href="https://torlenor.org/machine%20learning/reinforcement%20learning/2020/11/22/machine_learning_reinforcement_learning_snake.html" rel="alternate" type="text/html" title="Teaching an AI how to play the classic game Snake" /><published>2020-11-22T19:30:00+00:00</published><updated>2020-11-22T19:30:00+00:00</updated><id>https://torlenor.org/machine%20learning/reinforcement%20learning/2020/11/22/machine_learning_reinforcement_learning_snake</id><content type="html" xml:base="https://torlenor.org/machine%20learning/reinforcement%20learning/2020/11/22/machine_learning_reinforcement_learning_snake.html"><![CDATA[<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#snake" id="markdown-toc-snake">Snake</a></li>
  <li><a href="#the-snake-environment" id="markdown-toc-the-snake-environment">The Snake environment</a></li>
  <li><a href="#training-of-the-agent" id="markdown-toc-training-of-the-agent">Training of the agent</a></li>
  <li><a href="#using-it-to-play-the-game" id="markdown-toc-using-it-to-play-the-game">Using it to play the game</a></li>
  <li><a href="#summary" id="markdown-toc-summary">Summary</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>In this article we are going to use reinforcement learning (RL) <a href="#r1">[1]</a> to teach a computer to play the classic game Snake <a href="#r2">[2]</a> (remember the good old Nokia phones?). The game is implemented from scratch using Python including a visualization with PySDL2 <a href="#r3">[3]</a>. We are going to use TensorFlow <a href="#r4">[4]</a> to implement the actor-critic algorithm <a href="#r5">[5]</a> which is then used to learn playing the game.</p>

<p>We will show that, with moderate effort, an agent can be trained which plays the game reasonably well.</p>

<p style="text-align: center;"><img src="/assets/img/snake/snake_demo.gif" alt="" /></p>
<p style="text-align: center;"><em>Figure 1: The agent playing the game on a 10x10 board for 500 steps.</em></p>

<h1 id="snake">Snake</h1>

<p>Snake is a name for a series of video games where the player controls a growing “snake”-like line. According to Wikipedia the game concept is as old as 1976 and because it is so easy to implement (but still fun!) a ton of different implementations exist for nearly every computer platform. Many of you will probably know Snake from Nokia phones. Nokia started putting a variant of Snake onto their mobile phones in 1998, which brought a lot of new attention to this game. I, for myself, have to admit to have spent too much time trying to feed that little snake on my Nokia 6310.</p>

<p>The gameplay is simple: The player controls a dot, square, or something similar on a 2d world. As it moves, it leaves a tail behind, resembling a snake. Usually the length of the tail depends on the amount of food the snake ate. As the goal is to eat as much food as possible to increase your score, the length of the snake keeps increasing. The player loses when the snake runs into itself or into the screen border.</p>

<p>The game can easily be implemented in a few lines of Python code and when you throw in a couple more, you can even make a simple visualization in PySDL2. Therefore, we implemented the game ourselves, as relying on other implementations or looking into using them, may have taken more time than just doing it ourselves. And: It’s a fun little project to code.</p>

<p>We modified the traditional behavior of an automatically moving snake to a snake, which only moves one step when it gets the next action. This may make it boring if you are playing as a human, but it saves boilerplate code to discretize the game output into separate observations again. We believe, that the agent is fast enough to handle also that time constraint, if you should really want to use it in the self-moving game.</p>

<p>In our implementation always one piece of food is placed randomly onto an empty field of the game. The initial length of the snake is one tile, therefore, the maximum score for a given game field size of $N_x$ x $N_y$ is $N_xN_y - 1$.</p>

<h1 id="the-snake-environment">The Snake environment</h1>

<p>In addition to the game itself, it is necessary to encapsulate the game in an environment suitable for machine learning. We need to be able to tell the game what to do (which step to perform next) and we need a way to get an observation describing the current game state.</p>

<p>It is necessary to have four discrete actions to control the snake:</p>

<ul>
  <li>Move up</li>
  <li>Move left</li>
  <li>Move down</li>
  <li>Move right</li>
</ul>

<p>Determining the best state representation for our agent took more experimenting: The first approach was to encode all the tiles of the field with its type in a one-dimensional tensor. While this did work well, we soon found out, that this is not easily generalizable, because the observation space grows or shrinks with the size of the game board and one cannot just train one model and reuse it on other board sizes. Then we came up with a different idea: Restrict the snakes visibility range to a certain amount of tiles around its head. This helps reducing the state space dramatically, it speeds up the learning and it lifts the restriction to a fixed field size. We decided, arbitrarily, that we are going to restrict its view to four tiles in each of the possible movement directions (for an example see the blue tiles in Figure 2).</p>

<p style="text-align: center;"><img src="/assets/img/snake/snake_visibility_range.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 2: The agent playing the game on a 16x16 board. The tiles in the agents view are colored in blue.</em></p>

<p>For the rewards per step we first tried it with</p>

<ul>
  <li><strong>+10</strong> when food was eaten</li>
  <li><strong>-0.5</strong> when no food was eaten</li>
  <li><strong>-100</strong> when game over (hitting a wall or itself)</li>
</ul>

<p>which turned out to be a bad decision. Most of the time the agent just tried to stay on the same spot, being “afraid” of hitting a wall or itself, because that penalty was much larger than getting the slight penalty of not eating.</p>

<p>We then changed the rewards per step to</p>

<ul>
  <li><strong>+1</strong> when food was eaten</li>
  <li><strong>-0.01</strong> when no food was eaten</li>
  <li><strong>-1</strong> when game over (hitting a wall or itself)</li>
</ul>

<p>which worked reasonably well.</p>

<h1 id="training-of-the-agent">Training of the agent</h1>

<p>After a bit of experimentation, we decided on a model with one hidden layer and 512 neurons. We were able to use a learning rate of $1e-3$ through the whole training without running into instabilities. The discount factor was set to $\gamma=0.995$. Usually we stopped playing an episode when the agent reached 200 steps. Then the episode would end without negative reward.</p>

<p>We trained the model in four phases: In the first run, we used a 4x4 field and ran for 100k episodes to see if the agent is improving. Next we continued training on the same field size for additional 500k episodes. Then we switched to a larger game field of 8x8 and trained for another 500k episodes. In the final training phase we increased the number of maximal steps for each episode to 400 and increased the field size to 10x10.</p>

<p>In Figure 3 the evolution of the total moves per episode is shown. The number of moves tend to go up to the maximum of 200/400, but with strong fluctuations for the first two training runs, which took place on small game field. The fluctuations reduce in the third training run, were we switched to the larger board. In the last training run a drop in the total moves can be observed after 155k episodes.</p>

<p style="text-align: center;"><img src="/assets/img/snake/snake_total_moves_training.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 3: Total moves the agent reached during the episodes of training (note: the episode stopped automatically after 200 moves in the first 3 runs and after 400 moves in the last run). Plots taken from TensorBoard. The different colors of the lines correspond to the different phases of training: orange - first run, dark blue - second run, red - third run and light blue - final run. The data is smoothed using the TensorBoard smoothing value of 0.9.</em></p>

<p>The total reward per episode (Figure 4) shows a clear trend of improving during training. but also that training beyond a certain number of episodes does not increase the total rewards any further, due to the saturation on the small game field. Increasing the game field allowed a rise in the total rewards again. The final run, on an even larger field, seemed to show the same behavior up to about 155k episodes, but then a massive drop in total rewards could be observed.</p>

<p style="text-align: center;"><img src="/assets/img/snake/snake_total_reward_training.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 4: Total reward the agent reached during the episodes of training. Plots taken from TensorBoard. The different colors of the lines correspond to the different phases of training: orange - first run, dark blue - second run, red - third run and light blue - final run.</em></p>

<p>The final plot shows the running reward over the course of the training (Figure 5). The running reward $R$ at episode $i$ is calculated as</p>

\[R_{i} = 0.01R^{(e)}_i + 0.99 R_{i}\]

<p>where $R^{(e)}_i$ is the reward of the current episode $i$. Due to its definition its much smoother and a better indicator for the training progress of the model. The saturation due to limited field size is much clearer here and it seems to saturate between 11 and 12 for the 4x4 field, around 16 for the 8x8 field and around 21 for the 10x10 field. Judging from the fact, that the maximum score for the agent on a 4x4 field is 15 (taking into account the initial size of 1 tile for the snake), this is a good value, especially as our food placement is completely random and does not take into account if the food can be reached by the snake or not.</p>

<p style="text-align: center;"><img src="/assets/img/snake/snake_running_reward_training.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 5: Running reward during training. Plots taken from TensorBoard. The different colors of the lines correspond to the different phases of training: orange - first run, dark blue - second run, red - third run and light blue - final run.</em></p>

<p>The most noticeable event happened during the fourth run. It seems we encountered the effect of catastrophic forgetting after approximately 150k episodes. Because of that we decided to take the model at the 150kth episode as the final model.</p>

<h1 id="using-it-to-play-the-game">Using it to play the game</h1>

<p>Already from the training it was clear that the agent learned to play the game quite well. It learned to cycle the game field and when it encounters food in its vision range it tries to catch it and it is taking into account that it is not allowed to move over its own tail.</p>

<center>
<iframe width="560" height="315" src="https://www.youtube.com/embed/GUY7ishJip8" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen=""></iframe>
</center>
<p style="text-align: center;">Figure 6: A video of the agent playing snake on a 16x16 game field.</p>

<p>However, for larger game fields the agent can become stuck in a loop while searching for food, because if it cycles across the field without encountering food in his view, it will just continue cycling. But up to 16x16 boards, the agent works quite well.</p>

<h1 id="summary">Summary</h1>

<p>We showed that for the simple game Snake a well working agent can be trained using the simple actor-critic algorithm with one hidden layer and 512 neurons. Once trained, the agent can play on different game field sizes from 4x4 up to 16x16 fields. Increasing the field sizes further, may lead to the agent getting stuck in a loop, depending on the (random) placement of the foods on the field.</p>

<p>Ways to improve the agent would be changing the view to not be just a cross, but a square, so that also food diagonally away from the agent can be seen. For larger fields an easy way to increase its efficiency is to increase the view range to values larger than the currently used four tiles in each direction. However, that will require longer training. Maybe a better solution can be found which lets the agent explore the field more efficiently.</p>

<p>Other ideas for continuing on that project would be to introduce more than one food on the field or walls inside the field. Both are easy to implement and, with enough training, the agent should be able to overcome these additional difficulties.</p>

<p>If you want to give it a try, the code is available on GitHub: <a href="https://github.com/torlenor/rlsnake">https://github.com/torlenor/rlsnake</a></p>

<h1 id="references">References</h1>

<p>[1]<a name="r1"></a> <a href="https://en.wikipedia.org/wiki/Reinforcement_learning">https://en.wikipedia.org/wiki/Reinforcement_learning</a><br />
[2]<a name="r2"></a> <a href="https://en.wikipedia.org/wiki/Snake_(video_game_genre)">https://en.wikipedia.org/wiki/Snake_(video_game_genre)</a><br />
[3]<a name="r3"></a> <a href="https://github.com/marcusva/py-sdl2">https://github.com/marcusva/py-sdl2</a><br />
[4]<a name="r4"></a> <a href="https://www.tensorflow.org/">https://www.tensorflow.org/</a><br />
[5]<a name="r5"></a> A. Barto, R. Sutton, and C. Anderson, Neuron-like elements that can solve difficult learning control problems, IEEE Transactions on Systems, Man and Cybernetics, 13 (1983), pp. 835–846.</p>]]></content><author><name></name></author><category term="Machine Learning" /><category term="Reinforcement Learning" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">How to install Linux Mint 20 on a Dell XPS 13 (9310)</title><link href="https://torlenor.org/linux/2020/10/31/mint_on_dell_xps_13.html" rel="alternate" type="text/html" title="How to install Linux Mint 20 on a Dell XPS 13 (9310)" /><published>2020-10-31T11:00:00+00:00</published><updated>2020-10-31T11:00:00+00:00</updated><id>https://torlenor.org/linux/2020/10/31/mint_on_dell_xps_13</id><content type="html" xml:base="https://torlenor.org/linux/2020/10/31/mint_on_dell_xps_13.html"><![CDATA[<p>The new late-2020 Dell XPS 13 (9310) is one of the first notebooks based on Intel’s new Evo platform with Intel Core ix-11xx series which features the new integrated graphics architecture Iris Xe. Like the older Dell XPS 13 models, also this new one can be ordered preinstalled with Ubuntu Linux. This time, even in Austria! It comes preinstalled with Ubuntu 20.04, which may be already a good fit for many people. In my case, however, I got fond of the Cinnamon desktop and, therefore, I wanted to install Linux Mint on my new workhorse. It turned out to work quite smoothly, also due to the fact, that Ubuntu did a good job providing packages to support the new Intel platform.</p>

<p>Specs of my model:</p>

<ul>
  <li>Intel(R) Core(TM) i7-1165G7 (12 MB Cache, up to 4,7 GHz)</li>
  <li>16 GB RAM</li>
  <li>1 TB M.2-PCIe-NVMe-SSD</li>
  <li>Killer(TM) Wi-Fi 6 AX1650 and Bluetooth 5.1 1</li>
  <li>Non-glare InfinityEdge-Display without touch, 13,4” FHD+ (1.920 x 1.200) and 500 cd/m²</li>
</ul>

<h1 id="preparations">Preparations</h1>

<p style="text-align: center;"><img src="/assets/img/xps13/default_boot.jpg" alt="" /></p>
<p style="text-align: center;"><em>First boot of the Dell XPS 13 (9310) Developer Edition with Ubuntu preinstalled.</em></p>

<p>Before I went to install Mint 20, I booted up the installed Ubuntu 20.04 and made sure everything was working. The system was pre-configured quite well and after answering questions about my name and the desired user name, I got logged in and it was ready to be used. I made sure wireless lan was working, updated everything to their latest versions and generated the recovery image for the pre-installed Ubuntu via the Dell Recovery application.</p>

<p>In addition, I backed up the directories</p>
<ul>
  <li>/etc</li>
  <li>/usr/local</li>
  <li>/opt</li>
</ul>

<p>Especially backing up /etc is useful, because it contains the enabled package repositories, which we are going to use later.</p>

<p>Also downloading the current <a href="https://linuxmint.com/">Linux Mint</a> image and creating a USB flash drive were part of my preparations.</p>

<p>Important is also to go to
<a href="http://archive.ubuntu.com/ubuntu/pool/main/l/linux-meta-oem-5.6/">http://archive.ubuntu.com/ubuntu/pool/main/l/linux-meta-oem-5.6/</a>
and
<a href="http://archive.ubuntu.com/ubuntu/pool/main/l/linux-firmware/">http://archive.ubuntu.com/ubuntu/pool/main/l/linux-firmware/</a>
to download the packages</p>

<ul>
  <li>linux-headers-oem-20.04_5.6.0.1032.28_amd64.deb</li>
  <li>linux-image-oem-20.04_5.6.0.1032.28_amd64.deb</li>
  <li>linux-oem-20.04_5.6.0.1032.28_amd64.deb</li>
  <li>linux-oem-5.6-headers-5.6.0-1032_5.6.0-1032.33_all.deb</li>
  <li>linux-firmware_1.187.3_all.deb</li>
</ul>

<p>or their respective latest versions. We are going to need them to get WiFi working.</p>

<h1 id="installing-linux-mint">Installing Linux Mint</h1>

<p>Plug in the USB flash drive, if necessary using the adapter to USB-C delivered with your XPS 13, and boot up the laptop. Shortly after pressing the power-on button, keep hammering on the F12 key to get into the boot menu. Select your USB drive and boot up the Linux Mint live system. Then install Linux Mint either by replacing Ubuntu or, as I did, alongside Ubuntu 20.04. During the installation Linux Mint will ask you if you want to install 3rd party multimedia libraries. If you do, you will have to enter a secure boot password which will be asked from you the next time you reboot. Do that and remember the password.</p>

<p>When the installation is finished reboot and you should be, after acknowledging the secure boot changes by entering the password you set during installation, prompted with the Linux Mint login.</p>

<h1 id="getting-wifi-to-work-and-install-oem-components">Getting WiFi to work and install OEM components</h1>

<p>The first step will be to get WIFi to work, because what use is the best laptop if you cannot watch cat videos on YouTube with it?</p>

<p>Now the previously downloaded files come into play: Get the downloaded files onto your laptop, for example via USB flash drive, and open a terminal. Change into the directory were you copied the files to and type</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> ./linux-<span class="k">*</span>
</code></pre></div></div>
<p>to install all the packages. When the installation is finished reboot the laptop and when it comes back on you should have wireless lan.</p>

<p>After connecting to your WiFi you should add a few OEM repositories coming from Canonical to enable full support of the new hardware. If you copied the etc directory of your Ubuntu installation, or if you still have the Ubuntu installation on a separate partition, copy the files</p>

<ul>
  <li>somerville-dla-team-ubuntu-ppa-bionic.list</li>
  <li>focal-oem.list</li>
  <li>oem-somerville-bulbasaur-meta.list</li>
</ul>

<p>from <em>_your_etc_backup_/apt/sources.list.d/</em> to <em>/etc/apt/sources.list.d/</em>.</p>

<p>If you do not have the files backed up, no problem, here is their content:</p>

<p><strong>/etc/apt/sources.list.d/somerville-dla-team-ubuntu-ppa-bionic.list:</strong></p>
<pre><code class="language-t"> deb http://ppa.launchpad.net/somerville-dla-team/ppa/ubuntu bionic main
 # deb-src http://ppa.launchpad.net/somerville-dla-team/ppa/ubuntu bionic main
 # deb-src http://ppa.launchpad.net/somerville-dla-team/ppa/ubuntu bionic main
</code></pre>

<p><strong>/etc/apt/sources.list.d/focal-oem.list:</strong></p>
<pre><code class="language-t"> deb http://oem.archive.canonical.com/ focal oem
 #deb-src http://oem.archive.canonical.com/ focal oem
</code></pre>

<p><strong>/etc/apt/sources.list.d/oem-somerville-bulbasaur-meta.list:</strong></p>
<pre><code class="language-t"> deb http://dell.archive.canonical.com/ focal somerville
 # deb-src http://dell.archive.canonical.com/ focal somerville
 deb http://dell.archive.canonical.com/ focal somerville-bulbasaur
 # deb-src http://dell.archive.canonical.com/ focal somerville-bulbasaur
</code></pre>

<p>When you have copied or created the files, type</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-key adv <span class="nt">--keyserver</span> keyserver.ubuntu.com <span class="nt">--recv-keys</span> F992900E3BBF9275 F9FDA6BED73CDC22 F9FDA6BED73CDC22 78BD65473CB3BD13
<span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get dist-upgrade
<span class="nb">sudo </span>apt-get <span class="nb">install </span>ubuntu-oem-keyring oem-somerville-meta oem-somerville-bulbasaur-meta
</code></pre></div></div>

<p>to install the rest of the OEM packages for your laptop and update everything to the latest version.</p>

<p>For good measures reboot once more and your laptop should be ready to go with Linux Mint 20 and everything, including WiFi and support for the Iris Xe graphics, should now work as it did with the pre-installed Ubuntu 20.04 installation.</p>

<p>Enjoy!</p>

<p style="text-align: center;"><img src="/assets/img/xps13/linux_mint.png" alt="" /></p>
<p style="text-align: center;"><em>My finished Linux Mint 20 desktop on the Dell XPS 13 (9310).</em></p>]]></content><author><name></name></author><category term="Linux" /><summary type="html"><![CDATA[The new late-2020 Dell XPS 13 (9310) is one of the first notebooks based on Intel’s new Evo platform with Intel Core ix-11xx series which features the new integrated graphics architecture Iris Xe. Like the older Dell XPS 13 models, also this new one can be ordered preinstalled with Ubuntu Linux. This time, even in Austria! It comes preinstalled with Ubuntu 20.04, which may be already a good fit for many people. In my case, however, I got fond of the Cinnamon desktop and, therefore, I wanted to install Linux Mint on my new workhorse. It turned out to work quite smoothly, also due to the fact, that Ubuntu did a good job providing packages to support the new Intel platform.]]></summary></entry><entry><title type="html">Tackling the game Kalah using reinforcement learning - Part 1</title><link href="https://torlenor.org/machine%20learning/reinforcement%20learning/2020/10/23/machine_learning_reinforment_learning_kalah_part1.html" rel="alternate" type="text/html" title="Tackling the game Kalah using reinforcement learning - Part 1" /><published>2020-10-23T08:00:00+00:00</published><updated>2020-10-23T08:00:00+00:00</updated><id>https://torlenor.org/machine%20learning/reinforcement%20learning/2020/10/23/machine_learning_reinforment_learning_kalah_part1</id><content type="html" xml:base="https://torlenor.org/machine%20learning/reinforcement%20learning/2020/10/23/machine_learning_reinforment_learning_kalah_part1.html"><![CDATA[<p><strong>Update (2020-11-03):</strong> The code is now available on GitHub: <a href="https://github.com/torlenor/kalah">https://github.com/torlenor/kalah</a></p>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#kalah" id="markdown-toc-kalah">Kalah</a></li>
  <li><a href="#classic-agents" id="markdown-toc-classic-agents">Classic agents</a>    <ul>
      <li><a href="#random-agent" id="markdown-toc-random-agent">Random agent</a></li>
      <li><a href="#maxscore-agent" id="markdown-toc-maxscore-agent">MaxScore agent</a></li>
      <li><a href="#maxscorerepeat-agent" id="markdown-toc-maxscorerepeat-agent">MaxScoreRepeat agent</a></li>
      <li><a href="#minimax-agent" id="markdown-toc-minimax-agent">Minimax agent</a></li>
    </ul>
  </li>
  <li><a href="#reinforcement-learning-agents" id="markdown-toc-reinforcement-learning-agents">Reinforcement learning agents</a>    <ul>
      <li><a href="#reinforce-algorithm" id="markdown-toc-reinforce-algorithm">REINFORCE algorithm</a></li>
      <li><a href="#actor-critic-algorithm" id="markdown-toc-actor-critic-algorithm">Actor-critic algorithm</a></li>
    </ul>
  </li>
  <li><a href="#training-of-the-rl-agents" id="markdown-toc-training-of-the-rl-agents">Training of the RL agents</a></li>
  <li><a href="#comparison" id="markdown-toc-comparison">Comparison</a></li>
  <li><a href="#summary" id="markdown-toc-summary">Summary</a></li>
  <li><a href="#outlook" id="markdown-toc-outlook">Outlook</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<p>In this article series we are going to talk about reinforcement learning (RL) <a href="#r1">[1]</a>, an exciting and one of the three major parts of machine learning, besides supervised (see <a href="/machine/learning/2020/07/11/machine_learning_lol_10min_match_predictions.html">Predicting the outcome of a League of Legends match</a> for an example) and unsupervised learning. The idea behind RL is to train a model, usually called an agent, to take actions in an environment so that the cumulative reward over time, which must not necessarily mean real time, is maximized. In contrast to supervised learning, in RL the agent is not fed with labels and is not told what is the “correct” move, but the idea is, that the agent learns by itself in the given environment solely by providing an observation/state of the environment and the gained/lost reward after a taken action.</p>

<p>Here we will use this approach to tackle the game Kalah <a href="#r2">[2]</a>. To mix things up a little, this time we are going to use PyTorch <a href="#r3">[3]</a> as our library of choice.</p>

<p>We will show that it is possible to train an RL agent to play better than hard-coded approaches. In the last section we will give an outlook on improvements to the algorithms and what other approaches we could use.</p>

<h1 id="introduction">Introduction</h1>

<p>Reinforcement learning (RL) is one of the exiting fields of machine learning (ML) which gained popularity over the last years do to advances in computer performance, algorithms and because of the involvement of big technology companies. The research on learning ATARI games by Google’s DeepMind Technologies <a href="#r4">[4,5]</a> and their subsequent proof that RL can beat humans in go <a href="#r6">[6]</a>, chess and shogi showed that RL can be a powerful tool which will find its way out of toy models into real world applications sooner or later. Even learning complex computer games, like Dota 2 <a href="#r7">[7]</a> or Starcraft II <a href="#r8">[8]</a>, are no longer just visions, but under certain controlled conditions this is already possible.</p>

<p>Here we will first introduce the game Kalah, followed by implementations of some classic agents for the game, which will serve as our baseline and as a sparing partner for our machine learning models. Afterwards, we will present two simple RL agents for Kalah, show how to train them and compare them to the classic agents. We will show, that it is possible to have simple reinforcement learning models to learn the game Kalah and outperform classic agents, though currently restricted to smaller game boards.</p>

<h1 id="kalah">Kalah</h1>

<p>Kalah <a href="#r2">[2]</a> is a two-player game in the Mancala family invented by William Julius Champion, Jr. in 1940.</p>

<p>The game is played on a board and with a number of “seeds”. The board has a certain number of small pits, called houses, on each side (usually 6, but we will also use 4) and a big pit, called the end zone, at each end. The objective of the game is to capture more seeds than your opponent.</p>

<p style="text-align: center;"><img src="/assets/img/kalah_board.jpg" alt="" /></p>
<p style="text-align: center;"><em>Figure 1: A Kalah board $(6,6)$ in the start configuration.</em></p>

<p>There are various rule sets available and we will take the rule set which is considered standard. It is summarized in the following:</p>

<p>1) You start with 4 or 6 (or whatever you agree on) number of seeds in every of the players pits.</p>

<p>2) The players take turns “sowing” their seeds. The current player takes all the seeds from one of its pits and places them, one by one counter-clockwise into each of the following pits, including its own end zone pit, but not in the opponents end zone pit.</p>

<p>3) If the last sown seed lands in an empty house owned by the current player, and if the opposite house contains seeds, all the seeds in the pit where he placed the last seed and the seeds in the opposite pit belongs to the player and shall be placed into its end zone.</p>

<p>4) If the last sown seed lands in the player’s end zone, the player can take an additional move.</p>

<p>5) When a player does not have any more seeds in its pits, the game ends and the opposing player can take all its remaining seeds and place it in its end zone.</p>

<p>6) The player with the most seeds in its end zone wins.</p>

<p>For many variants of the game it was shown that the first player has a strong advantage when both are playing a perfect game. However, for the $(N_\text{pits}, N_\text{seeds})  = (6,6)$ variant, this is not yet that clear how big the advantage is. There are also additional rules which can mitigate that advantage, but we will not go into detail and if you are interested in that, feel free to consult Wikipedia.</p>

<p>In this article we are going to play with the $(4,4)$, $(6,4)$ and $(6,6)$ variants.</p>

<h1 id="classic-agents">Classic agents</h1>

<p>Before we talk about reinforcement learning approaches to playing Kalah, we first present classic agents which will serve as our baseline in the comparisons and which will be used for training. For most of its variations Kalah is a solved game were the first player would win if we would use pre-computed move databases with perfect moves, but we are not going to use them here.</p>

<h2 id="random-agent">Random agent</h2>

<p>This agent, as the name suggests, will randomly choose a move out of all valid moves. This is the simplest approach we can take and it can be implemented essentially with just one line of Python code.</p>

<h2 id="maxscore-agent">MaxScore agent</h2>

<p>The idea behind this agent is, that it will always take the move which gives him the highest score. This can either be a move which will let him sow a seed into its own end zone, or, ideally, it will be a move were it can steel the opponents seeds by hitting an empty pit on its own side of the board.</p>

<h2 id="maxscorerepeat-agent">MaxScoreRepeat agent</h2>

<p>The base strategy for this agent is the same as the MaxScore agent. The difference is, that it will prefer a move were it will hit its own end zone with its last seed, meaning that it can take another move. This is implemented in such a way to exploit the possibility of having more than one additional move if the board permits that. This can easily be implemented by always taking a look at the possible moves starting from the left of the board going right and picking the first where a repeating play is possible.</p>

<h2 id="minimax-agent">Minimax agent</h2>

<p>The minimax algorithm <a href="#r9">[9]</a> is a very common decision rule in game theory, statistics and many other fields. One tries to minimize the possible loss for a worst case (maximum loss) scenario.</p>

<p>The pseudo code for the algorithm (take from Wikipedia) is given by:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function minimax(node, depth, maximizingPlayer) is
    if depth = 0 or node is a terminal node then
        return the heuristic value of node
    if maximizingPlayer then
        value := −∞
        for each child of node do
            value := max(value, minimax(child, depth − 1, FALSE))
        return value
    else (* minimizing player *)
        value := +∞
        for each child of node do
            value := min(value, minimax(child, depth − 1, TRUE))
        return value
</code></pre></div></div>

<p>If not otherwise specified, we will use a minimax depth of $D_{max}=4$. In addition we implement alpha-beta pruning <a href="#r10">[10]</a> to speed up the calculations.</p>

<h1 id="reinforcement-learning-agents">Reinforcement learning agents</h1>

<p style="text-align: center;"><img src="/assets/img/Reinforcement_learning_diagram.svg.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 2: Reinforcement learning. Courtesy of Wikipedia.</em></p>

<p>Reinforcement learning (RL) is a branch of machine learning dealing with the maximization of cumulative rewards in a given environment. When talking about RL models running in such an environment one is usually talking about agents, a notion we already introduced in the sections above. Reinforcement learning does not need labelled inputs/outputs and the environment is typically sketched as a Markov decision process (MDP).</p>

<p>Usually the way RL works is shown in Figure 2: An agent takes action in a given environment, the action leads to a reward (positive or negative) and a representation of the state of the environment (in our case the Kalah board). The reward and the state are fed back into the agent model.</p>

<h2 id="reinforce-algorithm">REINFORCE algorithm</h2>

<p>There are many different approaches to reinforcement learning. In our case, we will take, in my opinion, the most straightforward and easy to gasp approach: Policy gradients.</p>

<p>In the policy gradient method, we are directly trying to find the best policy (something which tells us what action to choose in each step of the problem). The algorithm we are going to apply is named REINFORCE and was described in <a href="#r11">[11]</a> and a good explanation and implementation can be found in <a href="#r12">[12]</a>. Additionally, a good overview of different algorithms, including REINFORCE, is presented at: <a href="https://lilianweng.github.io/lil-log/2018/04/08/policy-gradient-algorithms.html#reinforce">https://lilianweng.github.io/lil-log/2018/04/08/policy-gradient-algorithms.html#reinforce</a></p>

<p>Here we are going to briefly outline the idea behind the algorithm:</p>

<p>1) Initialize the network with random weights.</p>

<p>2) Play an episode and save its $(s, a, r, s’)$ transition.</p>

<p>3) For every step $t=1,2,…,T$: Calculate the discounted reward/return</p>

\[Q_t=\sum^\infty_{k=0}\gamma^kR_{t+k+1}\]

<p>where $\gamma$ is the discount factor. $\gamma = 1$ means no discount, all time steps count the same, and $\gamma &lt; 1$ means higher discounts.</p>

<p>4) Calculate the loss function</p>

\[L=-\sum_tQ_t\ln(\pi(s_t,a_t))\]

<p>5) Calculate the gradients, use stochastic gradient decent and update the weights of the model, minimizing the loss (therefore, we need the minus sign in step 4 in front of the sum).</p>

<p>6) Repeat from step 2 until problem is considered solved.</p>

<p>$s$ is a state, $s’$ is the new state after taking action $a$ and $r$ is the reward obtained at a specific time step.</p>

<p>An example implementation in PyTorch can be found <a href="https://github.com/pytorch/examples/blob/master/reinforcement_learning/reinforce.py">here</a>, solving the CartPole problem.</p>

<h2 id="actor-critic-algorithm">Actor-critic algorithm</h2>

<p style="text-align: center;"><img src="/assets/img/actor_critic.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 3: Sketch of the actor-critic model structure.</em></p>

<p>In case of the actor-critic algorithm <a href="#r13">[13]</a> a value functions in learned in addition of the policy. This helps reducing the gradient variance. Actor-critic methods consist of two models, which may optionally share parameters:</p>

<ul>
  <li>The Critic updates the value function $V_\omega$ parameters $\omega$.</li>
  <li>The Actor updates the policy parameters $\theta$ for $\pi_\theta(s,a)$ in the direction suggested by the critic.</li>
</ul>

<p>An example implementation in PyTorch can be found <a href="https://github.com/pytorch/examples/blob/master/reinforcement_learning/actor_critic.py">here</a>.</p>

<h1 id="training-of-the-rl-agents">Training of the RL agents</h1>

<p>Training the RL agents turned out to be a challenge. After tuning $\gamma$, learning rate and rewards we were finally able to get an improving REINFORCE agent with win rates over 80%. Usually the agent had no problem to learn what moves are invalid and it usually had invalid moves below $5%$, but it had troubles learning a good policy for actually winning games against the classic agents. With the actor-critic agent it was easier to find parameters for which the algorithm converged, at least on $(4,4)$ boards.</p>

<p style="text-align: center;"><img src="/assets/img/ac_4_4_g0.99_s1_solved_98_lr0.001_n512_evalgames200.png" alt="" /></p>
<p style="text-align: center;"><em>Figure 4: Example for the evolution of win rate as a function of the training episode during training of the actor-critic agent on a $(4,4)$ board.</em></p>

<p>For the rewards we settled in the end with</p>
<ul>
  <li>Get number seeds placed into own house as rewards minus 0.1 (to make it less favorable to gain no points)</li>
  <li>For a win get +10</li>
  <li>For a loss get -10</li>
  <li>For an invalid move get -5 and the game is over</li>
</ul>

<p>It also turned out that it was hard to train against the random agent. Training worked best against the MaxScore and MaxScoreRepeat agents and in the end we settled with the MaxScoreRepeat agent for training of the AC and REINFORCE agents.</p>

<p>Training on larger boards/boards with more seeds, i.e., $(6,4)$ and $(6,6)$, did not lead to a high enough win rate with, neither the AC, nor the REINFORCE agent, even after tuning the parameters or after trying with various random seeds. We may need improvements to the models, which we are going to discuss in the Outlook section and hopefully we will be able to produce well trained agents for the larger boards, too.</p>

<h1 id="comparison">Comparison</h1>

<p>For the comparison we let every agent play $N=1000$ games against every other agent, including itself, with the exception of the RL agents, as currently it can only play as player 1. Updating the environment, so that it is possible to play as player 2 is part of the planed improvements. Draws are not taken into account when calculating the win rate.</p>

<p>In Table 1 we compare the classic agents against the RL agents on a $(4,4)$ board. From the classic agents the random agent performed worst, but a slight advantage for player 1 can be seen there, which may be related to the advantage the player 1 has in Kalah. The MaxScore agent performed already reasonably well with just a few lines of code. It can easily beat random chance and if played against itself also a slight advantage for player 1 is visible. The MaxScoreRepeat agent improved the scores even further and is only beaten more often by the Minimax agent. The Minimax agent clearly is the best classic agent, winning most of the games against the other agents. The reinforcement agents did perform reasonably well themselves. Especially the AC agent was able to outperform the classic agents including the Minimax agent.</p>

<table>
  <caption>Table 1: Comparison of classic and RL agents on a $(4,4)$ board. Shown is the average win percentage of player 1 (rows) vs. player 2 (columns) after playing $N=1000$ games.</caption>
  
    
    <tr>
      
        <th>vs</th>
      
        <th>Random</th>
      
        <th>MaxScore</th>
      
        <th>MaxScoreRepeat</th>
      
        <th>Minimax</th>
      
    </tr>
    

    <tr class="row1">
<td class="col1">
      Random
    </td><td class="col2">
      50.54
    </td><td class="col3">
      21.37
    </td><td class="col4">
      15.13
    </td><td class="col5">
      17.63
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      MaxScore
    </td><td class="col2">
      82.84
    </td><td class="col3">
      53.02
    </td><td class="col4">
      23.68
    </td><td class="col5">
      19.58
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      MaxScoreRepeat
    </td><td class="col2">
      87.94
    </td><td class="col3">
      84.71
    </td><td class="col4">
      67.56
    </td><td class="col5">
      48.23
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      Minimax
    </td><td class="col2">
      86.57
    </td><td class="col3">
      82.87
    </td><td class="col4">
      74.68
    </td><td class="col5">
      59.15
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      Reinforce
    </td><td class="col2">
      84.01
    </td><td class="col3">
      87.50
    </td><td class="col4">
      77.64
    </td><td class="col5">
      39.77
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      ActorCritic
    </td><td class="col2">
      89.60
    </td><td class="col3">
      90.60
    </td><td class="col4">
      88.88
    </td><td class="col5">
      64.36
    </td></tr>

  
</table>

<p>The comparison on the larger board with six bins each side and four seeds in each bin, i.e., $(6,4)$ in our notation, must be done without the RL agents, because, as we discussed in the previous section, we were not able to train a well-performing RL agent for larger boards. However, we are still comparing the classic agents for the larger boards. The biggest difference to the smaller board is that player 1 has a much higher win rate in case of the first three agent types. For minimax it is not so clear and the performance seems to be en-par with the performance on the smaller board, with the exception of the matchup against the MaxScoreRepeat agent, where the Minimax agent performed worse, but still winning more than half of the games.</p>

<table>
    <caption>Table 2: Comparison of classic agents on a $(6,4)$ board. Shown is the average win percentage of player 1 (rows) vs. player 2 (columns) after playing $N=1000$ games.</caption>
  
    
    <tr>
      
        <th>vs</th>
      
        <th>Random</th>
      
        <th>MaxScore</th>
      
        <th>MaxScoreRepeat</th>
      
        <th>Minimax</th>
      
    </tr>
    

    <tr class="row1">
<td class="col1">
      Random
    </td><td class="col2">
      50.05
    </td><td class="col3">
      3.68
    </td><td class="col4">
      0.51
    </td><td class="col5">
      3.13
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      MaxScore
    </td><td class="col2">
      96.74
    </td><td class="col3">
      56.94
    </td><td class="col4">
      5.25
    </td><td class="col5">
      12.83
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      MaxScoreRepeat
    </td><td class="col2">
      99.30
    </td><td class="col3">
      97.78
    </td><td class="col4">
      74.05
    </td><td class="col5">
      73.57
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      Minimax
    </td><td class="col2">
      98.19
    </td><td class="col3">
      91.91
    </td><td class="col4">
      59.69
    </td><td class="col5">
      64.05
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      Reinforce
    </td><td class="col2">
      N/A
    </td><td class="col3">
      N/A
    </td><td class="col4">
      N/A
    </td><td class="col5">
      N/A
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      ActorCritic
    </td><td class="col2">
      N/A
    </td><td class="col3">
      N/A
    </td><td class="col4">
      N/A
    </td><td class="col5">
      N/A
    </td></tr>

  
</table>

<p>On the $(6,6)$ the results, Table 3, look more similar to the $(4,4)$ board again, except for the random agent. The Minimax agent was still performing well against the other agents despite the depth of only $D_{max}=4$ which we used.</p>

<p>To see how a larger depth for the minimax algorithm changes things, we did another calculation on a $(6,6)$ board, but this time with $D_{max}=6$. This version of the Minimax agent is depicted as “Minimax 6” in Table 3. It drastically increased the calculation time, but did further improve the win percentage of the Minimax agent.</p>

<table>
  <caption>Table 3: Comparison of classic agents on a $(6,6)$ board. Shown is the average win percentage of player 1 (rows) vs. player 2 (columns) after playing $N=1000$ games. Minimax 6 uses a maximum depth of 6 for the minimax algorithm.</caption>
  
    
    <tr>
      
        <th>vs</th>
      
        <th>Random</th>
      
        <th>MaxScore</th>
      
        <th>MaxScoreRepeat</th>
      
        <th>Minimax</th>
      
        <th>Minimax 6</th>
      
    </tr>
    

    <tr class="row1">
<td class="col1">
      Random
    </td><td class="col2">
      49.95
    </td><td class="col3">
      2.83
    </td><td class="col4">
      1.30
    </td><td class="col5">
      0.70
    </td><td class="col6">
      1.80
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      MaxScore
    </td><td class="col2">
      97.39
    </td><td class="col3">
      53.06
    </td><td class="col4">
      14.81
    </td><td class="col5">
      14.51
    </td><td class="col6">
      14.42
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      MaxScoreRepeat
    </td><td class="col2">
      99.20
    </td><td class="col3">
      93.47
    </td><td class="col4">
      64.42
    </td><td class="col5">
      50.05
    </td><td class="col6">
      42.20
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      Minimax
    </td><td class="col2">
      97.90
    </td><td class="col3">
      91.47
    </td><td class="col4">
      76.51
    </td><td class="col5">
      65.13
    </td><td class="col6">
      N/A
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      Minimax 6
    </td><td class="col2">
      99.40
    </td><td class="col3">
      94.24
    </td><td class="col4">
      82.47
    </td><td class="col5">
      N/A
    </td><td class="col6">
      67.43
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      Reinforce
    </td><td class="col2">
      N/A
    </td><td class="col3">
      N/A
    </td><td class="col4">
      N/A
    </td><td class="col5">
      N/A
    </td><td class="col6">
      N/A
    </td></tr>

  
    

    <tr class="row1">
<td class="col1">
      ActorCritic
    </td><td class="col2">
      N/A
    </td><td class="col3">
      N/A
    </td><td class="col4">
      N/A
    </td><td class="col5">
      N/A
    </td><td class="col6">
      N/A
    </td></tr>

  
</table>

<h1 id="summary">Summary</h1>

<p>In this first part of what’s, hopefully, going to be a series of posts, we discussed how to play the board game Kalah with classic agents and how reinforcement learning can be used to successfully learn the game and win against classic agents, at least on small enough game boards. We also found that the training of a reinforcement model tends to be hard and a lot of hyperparameters tuning, i.e., fiddling with parameters, including the discount factor, rewards and learning rates, can be necessary. However, even though we implemented only two simple reinforcement algorithms, REINFORCE and actor-critic, it worked quite well. There is also lots of room for improvements, which may lead to even better performing RL agents.</p>

<h1 id="outlook">Outlook</h1>

<p>The next step will be implementing improved versions of REINFORCE. Especially we want to batch together episodes in the update step which should reduce the variance, i.e., should allow for a much more stable model over training time, and hopefully will lead to an improved performance and an easier trainable model. In addition, we will look into improvements to the actor-critic method. Especially we will see how advantage actor-critic (A2C) and asynchronous advantage actor-critic (A3C) models are implemented and how they perform in comparison to our current agents.</p>

<p>For the training process itself we are considering moving away from training always against one type of classic agent more to a heterogeneous approach were we train against various types of agents, which should hopefully improve the overall performance of the RL agents.</p>

<p>From the implementation point of view improvements to the environment and to the Kalah board will be made to allow the RL agent to play as the second player. We should also refactor the current code base so that it is easier to plot various metrics of the machine learning process and also to make it easier exporting these plots.</p>

<p>A distant goal will also be to make this implementation more user friendly so that a human player can easily play against the agents, or even better, a graphical/web interface….</p>

<h1 id="references">References</h1>

<p>[1]<a name="r1"></a> <a href="https://en.wikipedia.org/wiki/Reinforcement_learning">https://en.wikipedia.org/wiki/Reinforcement_learning</a><br />
[2]<a name="r2"></a> <a href="https://en.wikipedia.org/wiki/Kalah">https://en.wikipedia.org/wiki/Kalah</a><br />
[3]<a name="r3"></a> <a href="https://pytorch.org/">https://pytorch.org/</a><br />
[4]<a name="r4"></a> Mnih, Volodymyr; Kavukcuoglu, Koray; Silver, David; Graves, Alex; Antonoglou, Ioannis; Wierstra, Daan; Riedmiller, Martin (19 December 2013). “Playing Atari with Deep Reinforcement Learning”. arXiv:1312.5602.<br />
[5]<a name="r5"></a> Adrià Puigdomènech Badia; Piot, Bilal; Kapturowski, Steven; Sprechmann, Pablo; Vitvitskyi, Alex; Guo, Daniel; Blundell, Charles (30 March 2020). “Agent57: Outperforming the Atari Human Benchmark”. arXiv:2003.13350.<br />
[6]<a name="r6"></a> Silver, David; Huang, Aja; Maddison, Chris J.; Guez, Arthur; Sifre, Laurent; Driessche, George van den; Schrittwieser, Julian; Antonoglou, Ioannis; Panneershelvam, Veda; Lanctot, Marc; Dieleman, Sander; Grewe, Dominik; Nham, John; Kalchbrenner, Nal; Sutskever, Ilya; Lillicrap, Timothy; Leach, Madeleine; Kavukcuoglu, Koray; Graepel, Thore; Hassabis, Demis (28 January 2016). “Mastering the game of Go with deep neural networks and tree search”. Nature. 529 (7587): 484–489. https://doi.org/10.1038/nature16961.<br />
[7]<a name="r7"></a> Berner, C., Brockman, G., Chan, B., Cheung, V., Debiak, P., Dennison, C., Farhi, D., Fischer, Q., Hashme, S., Hesse, C., Józefowicz, R., Gray, S., Olsson, C., Pachocki, J.W., Petrov, M., Pinto, H.P., Raiman, J., Salimans, T., Schlatter, J., Schneider, J., Sidor, S., Sutskever, I., Tang, J., Wolski, F., &amp; Zhang, S. (2019). Dota 2 with Large Scale Deep Reinforcement Learning. ArXiv:1912.06680. <br />
[8]<a name="r8"></a> Vinyals, O., Babuschkin, I., Czarnecki, W.M. et al. Grandmaster level in StarCraft II using multi-agent reinforcement learning. Nature 575, 350–354 (2019). https://doi.org/10.1038/s41586-019-1724-z <br />
[9]<a name="r9"></a> <a href="https://en.wikipedia.org/wiki/Minimax">https://en.wikipedia.org/wiki/Minimax</a><br />
[10]<a name="r10"></a> <a href="https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning">https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning</a><br />
[11]<a name="r11"></a> Williams, Ronald J. “Simple statistical gradient-following algorithms for connectionist reinforcement learning.” Reinforcement Learning. Springer, Boston, MA, 1992. 5-32.<br />
[12]<a name="r12"></a> Lapan, Maxim. “Deep Reinforcement Learning Hands-On”, Second Edition, Packt, Birmingham, UK, 2020, 286-308.<br />
[13]<a name="r13"></a> A. Barto, R. Sutton, and C. Anderson, Neuron-like elements that can solve difficult learning control problems, IEEE Transactions on Systems, Man and Cybernetics, 13 (1983), pp. 835–846.</p>]]></content><author><name></name></author><category term="Machine Learning" /><category term="Reinforcement Learning" /><summary type="html"><![CDATA[Update (2020-11-03): The code is now available on GitHub: https://github.com/torlenor/kalah]]></summary></entry><entry><title type="html">Predicting the outcome of a League of Legends match 10 minutes into the game with 70% accuracy</title><link href="https://torlenor.org/machine/learning/2020/07/11/machine_learning_lol_10min_match_predictions.html" rel="alternate" type="text/html" title="Predicting the outcome of a League of Legends match 10 minutes into the game with 70% accuracy" /><published>2020-07-11T14:00:00+00:00</published><updated>2020-07-11T14:00:00+00:00</updated><id>https://torlenor.org/machine/learning/2020/07/11/machine_learning_lol_10min_match_predictions</id><content type="html" xml:base="https://torlenor.org/machine/learning/2020/07/11/machine_learning_lol_10min_match_predictions.html"><![CDATA[<ul id="markdown-toc">
  <li><a href="#introductory-remarks" id="markdown-toc-introductory-remarks">Introductory remarks</a></li>
  <li><a href="#retrieving-data-from-mongodb-and-data-preprocessing" id="markdown-toc-retrieving-data-from-mongodb-and-data-preprocessing">Retrieving data from MongoDB and data preprocessing</a></li>
  <li><a href="#model-definition-and-fitting" id="markdown-toc-model-definition-and-fitting">Model definition and fitting</a></li>
  <li><a href="#predicting-winloss" id="markdown-toc-predicting-winloss">Predicting Win/Loss</a></li>
  <li><a href="#summary-and-discussion" id="markdown-toc-summary-and-discussion">Summary and discussion</a></li>
  <li><a href="#appendix" id="markdown-toc-appendix">Appendix</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<p>Predicting the outcome of a sports match just after a few minutes into the game is an intriguing topic. Wouldn’t it be great to know for certain that your favorite football team will win before the game is finished playing? Or betting on your Formula 1 driver to win and being right about it most of the time could earn you a lot of money. While this is not so easily done for regular sports, it can be done for games which heavily depend on the history of the current match, i.e., on things which happened before in the match. Our most promising candidate to develop a model for such a study is Riot Games’ League of Legends (LoL).</p>

<p>In this article we will show how to access data from a MongoDB which was fetched from Riot’s LoL API, how to process it so that it is usable for modeling, define useful features and how to train an eXtreme Gradient Boosting (XGBoost) model [1]. Using that model we will show that it is possible to predict the outcome of 5v5 Solo Queue match played on Summoner’s Rift after just 10 minutes into the match with 70 % accuracy. We will use data from game version 10.13.</p>

<p>The article is written in a hands-on way and we are going to show code examples. As a language of choice we took Python 3 with the libraries matplotlib [2], numpy [3], pandas [4], pymongo [5], seaborn [6], sklearn [7], xgboost [8].</p>

<h1 id="introductory-remarks">Introductory remarks</h1>

<p><a href="https://leagueoflegends.com/">League of Legends</a> (LoL) is a multiplayer online battle arena (MOBA) game. Players compete in matches (in the mode we are looking at 5 vs 5) which can last from 20 to 50 minutes on average (see <a href="#appendix">Appendix</a> for more details on that). Teams have to work together to achieve victory by destroying the core building (called the Nexus) of the enemy team. Until they get there, they have to destroy towers and get past the defense lines of the enemy team without falling victim of losing their own Nexus in the process.</p>

<p>The players control characters called champions which are picked at the beginning of a match from a rich pool of different champions with their own set of unique abilities. During the match the champions will level up and gain additional abilities. They also have to accumulate gold to buy equipment. If a champion is killed it will not permanently die, but just removed from the battle field for a certain amount of time (which grows longer the longer the match is running).</p>

<p>To fetch the data we are using <a href="https://github.com/torlenor/alolstats">alolstats</a> which provides functionality to fetch match data from <a href="https://developer.riotgames.com/apis">Riot’s API</a> and to store it in a MongoDB collection. It would also feature basic statistical calculations and provide a convenient REST API, but for this project only the ability to fetch and store match data is from importance.</p>

<p>The match data, besides other information, contains timeline information in 0-10 min, 10-20 min, 20-30 min and 30-end min slots for each participant (10 in total, 5 for each team) and we are going to use this data in the modeling approach as features. The prediction target is going to be if team 1 wins the game or now.</p>

<p>As a regression model we are going to use the XGBoost model [1] on approx. 50,000 matches.</p>

<h1 id="retrieving-data-from-mongodb-and-data-preprocessing">Retrieving data from MongoDB and data preprocessing</h1>

<p>Fetching data from a MongoDB is really simple with Python. With just a few lines of code you are receiving a cursor pointing to the data which can be used to iterate through the results. We are taking the results and put them directly into Pandas DataFrames, which may not be the best if we would have a very large collection, but it will do for our data set.</p>

<p>Fetching meta information about the matches from the MongoDB collection (we are filtering for the correct <em>mapid</em>, <em>queueid</em> and <em>gameversion</em> here) can be done via:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pymongo</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="n">game_version</span> <span class="o">=</span> <span class="s">"10.13.326.4870"</span>

<span class="n">connection</span> <span class="o">=</span> <span class="n">pymongo</span><span class="p">.</span><span class="n">MongoClient</span><span class="p">(</span><span class="s">"mongodb://[redacted]:[redacted]@localhost/alolstats"</span><span class="p">)</span>
<span class="n">db</span> <span class="o">=</span> <span class="n">connection</span><span class="p">.</span><span class="n">alolstats</span>

<span class="n">matches_meta</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">matches</span><span class="p">.</span><span class="n">aggregate</span><span class="p">([</span>
    <span class="p">{</span> <span class="s">"$match"</span><span class="p">:</span> <span class="p">{</span><span class="s">"gameversion"</span><span class="p">:</span> <span class="n">game_version</span><span class="p">,</span> <span class="s">"mapid"</span><span class="p">:</span> <span class="mi">11</span><span class="p">,</span> <span class="s">"queueid"</span><span class="p">:</span> <span class="mi">420</span><span class="p">}},</span>
    <span class="p">{</span> <span class="s">"$unset"</span><span class="p">:</span> <span class="p">[</span><span class="s">"teams"</span><span class="p">,</span><span class="s">"participants"</span><span class="p">,</span> <span class="s">"participantidentities"</span><span class="p">]</span> <span class="p">},</span>
<span class="p">])</span>

<span class="n">df_matches_meta</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">matches_meta</span><span class="p">))</span>
<span class="n">df_matches_meta</span> <span class="o">=</span> <span class="n">df_matches_meta</span><span class="p">.</span><span class="n">set_index</span><span class="p">(</span><span class="s">"gameid"</span><span class="p">)</span>
</code></pre></div></div>

<p>We will perform the same for the timeline data, but this needs a bit more effort as we have to flatten the embedded documents that we are receiving from our MongoDB collection:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">flatten_nested_json_df</span><span class="p">(</span><span class="n">df</span><span class="p">):</span>
    <span class="c1"># Thanks to random StackOverflow user for that piece of code
</span>    <span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">reset_index</span><span class="p">()</span>

    <span class="c1"># search for columns to explode/flatten
</span>    <span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">applymap</span><span class="p">(</span><span class="nb">type</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">).</span><span class="nb">all</span><span class="p">()</span>
    <span class="n">list_columns</span> <span class="o">=</span> <span class="n">s</span><span class="p">[</span><span class="n">s</span><span class="p">].</span><span class="n">index</span><span class="p">.</span><span class="n">tolist</span><span class="p">()</span>

    <span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">applymap</span><span class="p">(</span><span class="nb">type</span><span class="p">)</span> <span class="o">==</span> <span class="nb">dict</span><span class="p">).</span><span class="nb">all</span><span class="p">()</span>
    <span class="n">dict_columns</span> <span class="o">=</span> <span class="n">s</span><span class="p">[</span><span class="n">s</span><span class="p">].</span><span class="n">index</span><span class="p">.</span><span class="n">tolist</span><span class="p">()</span>

    <span class="k">while</span> <span class="nb">len</span><span class="p">(</span><span class="n">list_columns</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">or</span> <span class="nb">len</span><span class="p">(</span><span class="n">dict_columns</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">new_columns</span> <span class="o">=</span> <span class="p">[]</span>

        <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="n">dict_columns</span><span class="p">:</span>
            <span class="c1"># explode dictionaries horizontally, adding new columns
</span>            <span class="n">horiz_exploded</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">json_normalize</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="n">col</span><span class="p">]).</span><span class="n">add_prefix</span><span class="p">(</span><span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">col</span><span class="si">}</span><span class="s">.'</span><span class="p">)</span>
            <span class="n">horiz_exploded</span><span class="p">.</span><span class="n">index</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">index</span>
            <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">concat</span><span class="p">([</span><span class="n">df</span><span class="p">,</span> <span class="n">horiz_exploded</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">).</span><span class="n">drop</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="n">col</span><span class="p">])</span>
            <span class="n">new_columns</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">horiz_exploded</span><span class="p">.</span><span class="n">columns</span><span class="p">)</span> <span class="c1"># inplace
</span>
        <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="n">list_columns</span><span class="p">:</span>
            <span class="c1"># explode lists vertically, adding new columns
</span>            <span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">drop</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="n">col</span><span class="p">]).</span><span class="n">join</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="n">col</span><span class="p">].</span><span class="n">explode</span><span class="p">().</span><span class="n">to_frame</span><span class="p">())</span>
            <span class="n">new_columns</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">col</span><span class="p">)</span>

        <span class="c1"># check if there are still dict o list fields to flatten
</span>        <span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="n">new_columns</span><span class="p">].</span><span class="n">applymap</span><span class="p">(</span><span class="nb">type</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">).</span><span class="nb">all</span><span class="p">()</span>
        <span class="n">list_columns</span> <span class="o">=</span> <span class="n">s</span><span class="p">[</span><span class="n">s</span><span class="p">].</span><span class="n">index</span><span class="p">.</span><span class="n">tolist</span><span class="p">()</span>

        <span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="n">new_columns</span><span class="p">].</span><span class="n">applymap</span><span class="p">(</span><span class="nb">type</span><span class="p">)</span> <span class="o">==</span> <span class="nb">dict</span><span class="p">).</span><span class="nb">all</span><span class="p">()</span>
        <span class="n">dict_columns</span> <span class="o">=</span> <span class="n">s</span><span class="p">[</span><span class="n">s</span><span class="p">].</span><span class="n">index</span><span class="p">.</span><span class="n">tolist</span><span class="p">()</span>
        
    <span class="k">return</span> <span class="n">df</span>

<span class="n">df_matches_participant</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">10</span><span class="p">,</span><span class="mi">1</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Fetching general infos for participant "</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="s">" of 10"</span><span class="p">)</span>
    <span class="n">m</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">matches</span><span class="p">.</span><span class="n">aggregate</span><span class="p">([</span>
        <span class="p">{</span> <span class="s">"$match"</span><span class="p">:</span> <span class="p">{</span><span class="s">"gameversion"</span><span class="p">:</span> <span class="n">game_version</span><span class="p">,</span> <span class="s">"mapid"</span><span class="p">:</span> <span class="mi">11</span><span class="p">,</span> <span class="s">"queueid"</span><span class="p">:</span> <span class="mi">420</span><span class="p">}},</span>
        <span class="p">{</span> <span class="s">"$addFields"</span><span class="p">:</span> <span class="p">{</span> <span class="s">"participants.gameid"</span><span class="p">:</span> <span class="s">"$gameid"</span> <span class="p">}</span> <span class="p">},</span>
        <span class="p">{</span> <span class="s">"$replaceRoot"</span><span class="p">:</span> <span class="p">{</span> <span class="s">"newRoot"</span><span class="p">:</span> <span class="p">{</span><span class="s">"$arrayElemAt"</span><span class="p">:</span> <span class="p">[</span> <span class="s">"$participants"</span><span class="p">,</span> <span class="n">i</span><span class="p">]</span> <span class="p">}</span>  <span class="p">}</span>  <span class="p">},</span>
        <span class="p">{</span> <span class="s">"$sort"</span> <span class="p">:</span> <span class="p">{</span> <span class="s">"gameid"</span> <span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"participantid"</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">},</span>
    <span class="p">],</span> <span class="n">allowDiskUse</span> <span class="o">=</span> <span class="bp">True</span> <span class="p">)</span>
    <span class="n">df_matches_participant</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">flatten_nested_json_df</span><span class="p">(</span><span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">m</span><span class="p">))).</span><span class="n">set_index</span><span class="p">(</span><span class="s">"gameid"</span><span class="p">))</span>
</code></pre></div></div>

<p>We are ending up with data for each participant of the match, which we can further process to filter out only required columns and limit our features to only timeline fields for 0-10 min:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Join all participants data into columns so that we have one line per game
</span><span class="n">X_participants</span> <span class="o">=</span> <span class="n">df_matches_participant</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">join</span><span class="p">(</span><span class="n">df_matches_participant</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">lsuffix</span><span class="o">=</span><span class="s">"_p0"</span><span class="p">,</span> <span class="n">rsuffix</span><span class="o">=</span><span class="s">"_p1"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="mi">10</span><span class="p">,</span><span class="mi">1</span><span class="p">):</span>
    <span class="n">X_participants</span> <span class="o">=</span> <span class="n">X_participants</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">df_matches_participant</span><span class="p">[</span><span class="n">p</span><span class="p">],</span> <span class="n">rsuffix</span><span class="o">=</span><span class="s">"_p"</span><span class="o">+</span><span class="nb">str</span><span class="p">(</span><span class="n">p</span><span class="p">))</span>

<span class="n">X_participants_timeline_0_10</span> <span class="o">=</span> <span class="n">X_participants</span><span class="p">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">regex</span><span class="o">=</span><span class="p">(</span><span class="s">"teamid|timeline.*0-10.*"</span><span class="p">))</span>

<span class="c1"># Drop all Diffs between the players on the same lane, we do not want them
</span><span class="n">X_participants_timeline_0_10</span> <span class="o">=</span> <span class="n">X_participants_timeline_0_10</span><span class="p">[</span><span class="n">X_participants_timeline_0_10</span><span class="p">.</span><span class="n">columns</span><span class="p">.</span><span class="n">drop</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">X_participants_timeline_0_10</span><span class="p">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">regex</span><span class="o">=</span><span class="s">'diff'</span><span class="p">)))]</span>

<span class="n">y</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">df_matches_team1</span><span class="p">[</span><span class="n">df_matches_team1</span><span class="p">[</span><span class="s">"teamid"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">100</span><span class="p">][</span><span class="s">"win"</span><span class="p">])</span>
<span class="n">y</span><span class="p">.</span><span class="n">rename</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">{</span><span class="s">"win"</span><span class="p">:</span> <span class="s">"team1_did_win"</span><span class="p">},</span> <span class="n">inplace</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">Xy</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">concat</span><span class="p">([</span><span class="n">X_participants_timeline_0_10</span><span class="p">,</span> <span class="n">y</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">Xy</span> <span class="o">=</span> <span class="n">X_participants_timeline_0_10</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>
<span class="n">Xy</span> <span class="o">=</span> <span class="n">Xy</span><span class="p">[</span><span class="n">Xy</span><span class="p">[</span><span class="s">"team1_did_win"</span><span class="p">].</span><span class="n">isnull</span><span class="p">()</span> <span class="o">==</span> <span class="bp">False</span><span class="p">]</span>

<span class="c1"># Final data set for prediction variable...
</span><span class="n">y_final</span><span class="o">=</span> <span class="n">Xy</span><span class="p">[</span><span class="s">"team1_did_win"</span><span class="p">]</span>
<span class="c1"># ... and for features, we drop all data sets were we do now know who one
</span><span class="n">X_final</span> <span class="o">=</span> <span class="n">Xy</span><span class="p">.</span><span class="n">drop</span><span class="p">(</span><span class="s">'team1_did_win'</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<p>The final data sets look like this now:</p>

<ul>
  <li>
    <p><strong>X_final (first 5 lines):</strong></p>

    <style scoped="">
      .dataframe tbody tr th:only-of-type {
          vertical-align: middle;
      }

      .dataframe tbody tr th {
          vertical-align: top;
      }

      .dataframe thead th {
          text-align: right;
      }
      .overflow {
                      overflow-x: scroll;
      }
  </style>

    <div class="overflow">
  <table border="1" class="dataframe">
        <thead>
      <tr style="text-align: right;">
      <th></th>
      <th>teamid_p0</th>
      <th>timeline.creepspermindeltas.0-10_p0</th>
      <th>timeline.xppermindeltas.0-10_p0</th>
      <th>timeline.goldpermindeltas.0-10_p0</th>
      <th>timeline.damagetakenpermindeltas.0-10_p0</th>
      <th>teamid_p1</th>
      <th>timeline.creepspermindeltas.0-10_p1</th>
      <th>timeline.xppermindeltas.0-10_p1</th>
      <th>timeline.goldpermindeltas.0-10_p1</th>
      <th>timeline.damagetakenpermindeltas.0-10_p1</th>
      <th>teamid</th>
      <th>timeline.creepspermindeltas.0-10</th>
      <th>timeline.xppermindeltas.0-10</th>
      <th>timeline.goldpermindeltas.0-10</th>
      <th>timeline.damagetakenpermindeltas.0-10</th>
      <th>teamid_p3</th>
      <th>timeline.creepspermindeltas.0-10_p3</th>
      <th>timeline.xppermindeltas.0-10_p3</th>
      <th>timeline.goldpermindeltas.0-10_p3</th>
      <th>timeline.damagetakenpermindeltas.0-10_p3</th>
      <th>teamid_p4</th>
      <th>timeline.creepspermindeltas.0-10_p4</th>
      <th>timeline.xppermindeltas.0-10_p4</th>
      <th>timeline.goldpermindeltas.0-10_p4</th>
      <th>timeline.damagetakenpermindeltas.0-10_p4</th>
      <th>teamid_p5</th>
      <th>timeline.creepspermindeltas.0-10_p5</th>
      <th>timeline.xppermindeltas.0-10_p5</th>
      <th>timeline.goldpermindeltas.0-10_p5</th>
      <th>timeline.damagetakenpermindeltas.0-10_p5</th>
      <th>teamid_p6</th>
      <th>timeline.creepspermindeltas.0-10_p6</th>
      <th>timeline.xppermindeltas.0-10_p6</th>
      <th>timeline.goldpermindeltas.0-10_p6</th>
      <th>timeline.damagetakenpermindeltas.0-10_p6</th>
      <th>teamid_p7</th>
      <th>timeline.creepspermindeltas.0-10_p7</th>
      <th>timeline.xppermindeltas.0-10_p7</th>
      <th>timeline.goldpermindeltas.0-10_p7</th>
      <th>timeline.damagetakenpermindeltas.0-10_p7</th>
      <th>teamid_p8</th>
      <th>timeline.creepspermindeltas.0-10_p8</th>
      <th>timeline.xppermindeltas.0-10_p8</th>
      <th>timeline.goldpermindeltas.0-10_p8</th>
      <th>timeline.damagetakenpermindeltas.0-10_p8</th>
      <th>teamid_p9</th>
      <th>timeline.creepspermindeltas.0-10_p9</th>
      <th>timeline.xppermindeltas.0-10_p9</th>
      <th>timeline.goldpermindeltas.0-10_p9</th>
      <th>timeline.damagetakenpermindeltas.0-10_p9</th>
      </tr>
      <tr>
      <th>gameid</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      </tr>
  </thead>
        <tbody>
      <tr>
      <th>317415113</th>
      <td>100</td>
      <td>1.5</td>
      <td>230.2</td>
      <td>157.1</td>
      <td>318.2</td>
      <td>100</td>
      <td>0.0</td>
      <td>377.4</td>
      <td>324.2</td>
      <td>849.2</td>
      <td>100</td>
      <td>6.1</td>
      <td>266.1</td>
      <td>230.2</td>
      <td>341.0</td>
      <td>100</td>
      <td>6.3</td>
      <td>368.7</td>
      <td>281.9</td>
      <td>663.4</td>
      <td>100</td>
      <td>7.2</td>
      <td>431.5</td>
      <td>258.7</td>
      <td>482.1</td>
      <td>200</td>
      <td>5.4</td>
      <td>398.7</td>
      <td>233.3</td>
      <td>492.3</td>
      <td>200</td>
      <td>7.3</td>
      <td>511.6</td>
      <td>388.1</td>
      <td>450.8</td>
      <td>200</td>
      <td>7.8</td>
      <td>330.8</td>
      <td>443.3</td>
      <td>351.9</td>
      <td>200</td>
      <td>0.1</td>
      <td>335.3</td>
      <td>342.0</td>
      <td>776.1</td>
      <td>200</td>
      <td>1.3</td>
      <td>328.2</td>
      <td>235.2</td>
      <td>205.3</td>
      </tr>
      <tr>
      <th>317416566</th>
      <td>100</td>
      <td>7.6</td>
      <td>338.3</td>
      <td>263.9</td>
      <td>205.6</td>
      <td>100</td>
      <td>0.3</td>
      <td>274.1</td>
      <td>155.2</td>
      <td>171.7</td>
      <td>100</td>
      <td>4.6</td>
      <td>354.8</td>
      <td>200.6</td>
      <td>419.5</td>
      <td>100</td>
      <td>0.2</td>
      <td>260.6</td>
      <td>230.4</td>
      <td>441.6</td>
      <td>100</td>
      <td>8.3</td>
      <td>499.8</td>
      <td>394.3</td>
      <td>385.4</td>
      <td>200</td>
      <td>7.0</td>
      <td>299.2</td>
      <td>240.6</td>
      <td>157.8</td>
      <td>200</td>
      <td>0.1</td>
      <td>278.3</td>
      <td>132.2</td>
      <td>153.1</td>
      <td>200</td>
      <td>3.7</td>
      <td>371.2</td>
      <td>201.5</td>
      <td>502.1</td>
      <td>200</td>
      <td>8.3</td>
      <td>517.9</td>
      <td>335.8</td>
      <td>211.8</td>
      <td>200</td>
      <td>0.5</td>
      <td>298.1</td>
      <td>258.8</td>
      <td>694.8</td>
      </tr>
      <tr>
      <th>317418523</th>
      <td>100</td>
      <td>5.9</td>
      <td>314.5</td>
      <td>215.4</td>
      <td>397.8</td>
      <td>100</td>
      <td>7.4</td>
      <td>481.4</td>
      <td>259.2</td>
      <td>146.6</td>
      <td>100</td>
      <td>0.0</td>
      <td>310.0</td>
      <td>307.0</td>
      <td>758.9</td>
      <td>100</td>
      <td>0.7</td>
      <td>218.8</td>
      <td>153.1</td>
      <td>241.2</td>
      <td>100</td>
      <td>5.0</td>
      <td>398.7</td>
      <td>196.5</td>
      <td>434.8</td>
      <td>200</td>
      <td>7.3</td>
      <td>489.0</td>
      <td>304.2</td>
      <td>399.2</td>
      <td>200</td>
      <td>1.3</td>
      <td>263.4</td>
      <td>201.9</td>
      <td>269.0</td>
      <td>200</td>
      <td>6.8</td>
      <td>425.8</td>
      <td>301.1</td>
      <td>287.3</td>
      <td>200</td>
      <td>0.6</td>
      <td>364.5</td>
      <td>365.7</td>
      <td>576.5</td>
      <td>200</td>
      <td>9.0</td>
      <td>352.6</td>
      <td>353.6</td>
      <td>259.6</td>
      </tr>
      <tr>
      <th>317419849</th>
      <td>100</td>
      <td>7.3</td>
      <td>423.2</td>
      <td>249.0</td>
      <td>395.0</td>
      <td>100</td>
      <td>0.0</td>
      <td>276.4</td>
      <td>144.3</td>
      <td>24.8</td>
      <td>100</td>
      <td>6.9</td>
      <td>298.6</td>
      <td>270.8</td>
      <td>268.5</td>
      <td>100</td>
      <td>0.4</td>
      <td>311.2</td>
      <td>358.8</td>
      <td>864.9</td>
      <td>100</td>
      <td>5.9</td>
      <td>461.4</td>
      <td>400.6</td>
      <td>519.4</td>
      <td>200</td>
      <td>1.2</td>
      <td>241.1</td>
      <td>167.3</td>
      <td>219.3</td>
      <td>200</td>
      <td>7.5</td>
      <td>344.2</td>
      <td>292.1</td>
      <td>252.3</td>
      <td>200</td>
      <td>3.9</td>
      <td>348.6</td>
      <td>237.9</td>
      <td>635.3</td>
      <td>200</td>
      <td>0.2</td>
      <td>373.8</td>
      <td>366.0</td>
      <td>616.8</td>
      <td>200</td>
      <td>6.1</td>
      <td>435.1</td>
      <td>315.6</td>
      <td>148.0</td>
      </tr>
      <tr>
      <th>317425382</th>
      <td>100</td>
      <td>0.2</td>
      <td>183.0</td>
      <td>177.8</td>
      <td>250.7</td>
      <td>100</td>
      <td>7.5</td>
      <td>499.0</td>
      <td>304.2</td>
      <td>436.4</td>
      <td>100</td>
      <td>0.2</td>
      <td>344.9</td>
      <td>348.8</td>
      <td>671.2</td>
      <td>100</td>
      <td>7.0</td>
      <td>444.3</td>
      <td>235.5</td>
      <td>310.3</td>
      <td>100</td>
      <td>4.0</td>
      <td>341.9</td>
      <td>385.6</td>
      <td>438.5</td>
      <td>200</td>
      <td>0.4</td>
      <td>362.0</td>
      <td>269.1</td>
      <td>676.1</td>
      <td>200</td>
      <td>0.8</td>
      <td>271.6</td>
      <td>286.5</td>
      <td>334.9</td>
      <td>200</td>
      <td>5.7</td>
      <td>273.7</td>
      <td>290.1</td>
      <td>550.2</td>
      <td>200</td>
      <td>3.8</td>
      <td>365.2</td>
      <td>177.4</td>
      <td>485.9</td>
      <td>200</td>
      <td>8.4</td>
      <td>539.3</td>
      <td>294.1</td>
      <td>284.2</td>
      </tr>
  </tbody>
      </table>
  </div>
  </li>
  <li>
    <p><strong>y_final (first 5 lines):</strong></p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  gameid
  317415113    Fail
  317416566    Fail
  317418523    Fail
  317419849    Fail
  317425382    Fail
  Name: team1_did_win, dtype: object
</code></pre></div>    </div>
  </li>
</ul>

<p>We want to split our data set into training, validation and test sets, to validate the model and to later on test the model on test data. This can be easily accomplished with sklearn train_test_split function:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">train_test_split</span>
<span class="n">X_tmp</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_tmp</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="n">train_test_split</span><span class="p">(</span><span class="n">X_final</span><span class="p">,</span> <span class="n">y_final</span><span class="p">,</span> <span class="n">train_size</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="mf">0.2</span><span class="p">,</span> <span class="n">random_state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span>
<span class="n">X_train</span><span class="p">,</span> <span class="n">X_valid</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_valid</span> <span class="o">=</span> <span class="n">train_test_split</span><span class="p">(</span><span class="n">X_tmp</span><span class="p">,</span> <span class="n">y_tmp</span><span class="p">,</span> <span class="n">train_size</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="mf">0.2</span><span class="p">,</span> <span class="n">random_state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span>
<span class="k">del</span> <span class="n">X_tmp</span><span class="p">,</span> <span class="n">y_tmp</span>
</code></pre></div></div>

<p>No data set is perfect and there  are NaN values in the data sets and we have to fill them (the other possibility would be to drop the columns entirely, but we would lose a lot of data). It turns out that Riot seems to set certain fields to NaN if they could not determine certain metrics for a player in that time frame. It is clear that it will not be normally distributed data and we should not use the mean to fill the missing data points. It would be a possibility to take the median to fill the data, but even better works to just set the value to zero. We will use the sklearn SimpleImputer to perform this step:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.impute</span> <span class="kn">import</span> <span class="n">SimpleImputer</span>

<span class="n">my_imputer</span> <span class="o">=</span> <span class="n">SimpleImputer</span><span class="p">(</span><span class="n">strategy</span><span class="o">=</span><span class="s">'constant'</span><span class="p">,</span> <span class="n">fill_value</span><span class="o">=</span><span class="mf">0.0</span><span class="p">)</span>
<span class="n">imputed_X_train</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">my_imputer</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">X_train</span><span class="p">))</span>
<span class="n">imputed_X_valid</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">my_imputer</span><span class="p">.</span><span class="n">transform</span><span class="p">(</span><span class="n">X_valid</span><span class="p">))</span>
<span class="n">imputed_X_test</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">my_imputer</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">X_test</span><span class="p">))</span>

<span class="c1"># Imputation removed column names; put them back
</span><span class="n">imputed_X_train</span><span class="p">.</span><span class="n">columns</span> <span class="o">=</span> <span class="n">X_train</span><span class="p">.</span><span class="n">columns</span>
<span class="n">imputed_X_valid</span><span class="p">.</span><span class="n">columns</span> <span class="o">=</span> <span class="n">X_valid</span><span class="p">.</span><span class="n">columns</span>
<span class="n">imputed_X_test</span><span class="p">.</span><span class="n">columns</span> <span class="o">=</span> <span class="n">X_test</span><span class="p">.</span><span class="n">columns</span>

<span class="c1"># Imputation removed indices; put them back
</span><span class="n">imputed_X_train</span><span class="p">.</span><span class="n">index</span> <span class="o">=</span> <span class="n">X_train</span><span class="p">.</span><span class="n">index</span>
<span class="n">imputed_X_valid</span><span class="p">.</span><span class="n">index</span> <span class="o">=</span> <span class="n">X_valid</span><span class="p">.</span><span class="n">index</span>
<span class="n">imputed_X_test</span><span class="p">.</span><span class="n">index</span> <span class="o">=</span> <span class="n">X_test</span><span class="p">.</span><span class="n">index</span>
</code></pre></div></div>

<p>The last step left to do is to encode our prediction target, which is “Win” or “Fail”, to something numeric which can be used in our model. We perform this encoding with the LabelEncoder:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.preprocessing</span> <span class="kn">import</span> <span class="n">LabelEncoder</span>
<span class="n">label_encoder</span> <span class="o">=</span> <span class="n">LabelEncoder</span><span class="p">()</span>
<span class="n">label_y_train</span> <span class="o">=</span> <span class="n">label_encoder</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">y_train</span><span class="p">)</span>
<span class="n">label_y_valid</span> <span class="o">=</span> <span class="n">label_encoder</span><span class="p">.</span><span class="n">transform</span><span class="p">(</span><span class="n">y_valid</span><span class="p">)</span>
</code></pre></div></div>

<h1 id="model-definition-and-fitting">Model definition and fitting</h1>

<p>The features we are going to use for the model are now</p>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Team id</td>
      <td>The Team ID of that participant (either 100 or 200).</td>
    </tr>
    <tr>
      <td>Creeps per minute 0-10min</td>
      <td>The NPC creatures killed per minute during the time of 0 to 10 minutes into the game.</td>
    </tr>
    <tr>
      <td>Gold per minute 0-10min</td>
      <td>The gold earned per minute during the time of 0 to 10 minutes into the game.</td>
    </tr>
    <tr>
      <td>Damage taken per minute 0-10min</td>
      <td>Damage taken per minute during the time of 0 to 10 minutes into the game.</td>
    </tr>
  </tbody>
</table>

<p>These features will occur 10 times in our data set, once for each player in the match.</p>

<p>We define two functions, one for the model and one for judging the quality of the model:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.ensemble</span> <span class="kn">import</span> <span class="n">RandomForestRegressor</span>
<span class="kn">from</span> <span class="nn">sklearn.metrics</span> <span class="kn">import</span> <span class="n">mean_absolute_error</span>
<span class="kn">from</span> <span class="nn">xgboost</span> <span class="kn">import</span> <span class="n">XGBRegressor</span>

<span class="k">def</span> <span class="nf">fit_xgboost_model</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_valid</span><span class="p">,</span> <span class="n">y_valid</span><span class="p">,</span> <span class="n">learning_rate</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">n_estimators</span><span class="o">=</span><span class="mi">500</span><span class="p">,</span> <span class="n">early_stopping_rounds</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">XGBRegressor</span><span class="p">(</span><span class="n">n_estimators</span><span class="o">=</span><span class="n">n_estimators</span><span class="p">,</span> <span class="n">learning_rate</span><span class="o">=</span><span class="n">learning_rate</span><span class="p">,</span> <span class="n">n_jobs</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
    <span class="n">model</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span>
              <span class="n">early_stopping_rounds</span><span class="o">=</span><span class="n">early_stopping_rounds</span><span class="p">,</span> 
              <span class="n">eval_set</span><span class="o">=</span><span class="p">[(</span><span class="n">X_valid</span><span class="p">,</span> <span class="n">y_valid</span><span class="p">)],</span> 
              <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span>
             <span class="p">)</span>
    <span class="k">return</span> <span class="n">model</span>

<span class="k">def</span> <span class="nf">score_dataset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X_valid</span><span class="p">,</span> <span class="n">y_valid</span><span class="p">):</span>
    <span class="n">preds</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_valid</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">mean_absolute_error</span><span class="p">(</span><span class="n">y_valid</span><span class="p">,</span> <span class="n">preds</span><span class="p">)</span>
</code></pre></div></div>

<p>To find the best XGB parameters we are looping over number of estimators (n_estimators) and learning rate (learning_rate) and find the parameters which minimize the mean absolute error.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="n">best_learning_rate</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">best_n_estimators</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">best_mae</span> <span class="o">=</span> <span class="mi">100000</span>
<span class="k">for</span> <span class="n">learning_rate</span> <span class="ow">in</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mf">0.004</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">,</span> <span class="mf">0.002</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">n_estimators</span> <span class="ow">in</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="mi">1600</span><span class="p">,</span> <span class="mi">200</span><span class="p">):</span>
        <span class="n">model</span> <span class="o">=</span> <span class="n">fit_xgboost_model</span><span class="p">(</span><span class="n">imputed_X_train</span><span class="p">,</span> <span class="n">label_y_train</span><span class="p">,</span> <span class="n">imputed_X_valid</span><span class="p">,</span> <span class="n">label_y_valid</span><span class="p">,</span> <span class="n">learning_rate</span><span class="p">,</span> <span class="n">n_estimators</span><span class="p">)</span>
        <span class="n">mae</span> <span class="o">=</span> <span class="n">score_dataset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">imputed_X_valid</span><span class="p">,</span> <span class="n">label_y_valid</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">mae</span> <span class="o">&lt;</span> <span class="n">best_mae</span><span class="p">:</span>
            <span class="n">best_learning_rate</span> <span class="o">=</span> <span class="n">learning_rate</span>
            <span class="n">best_n_estimators</span> <span class="o">=</span> <span class="n">n_estimators</span>
            <span class="n">best_mae</span> <span class="o">=</span> <span class="n">mae</span>
</code></pre></div></div>

<p>The best parameters for the data set used in this study were <code class="language-plaintext highlighter-rouge">learning_rate = 0</code> and <code class="language-plaintext highlighter-rouge">n_estimators = 0</code> and we kept the early stopping rounds at a value of 5.</p>

<p>Using these parameters we perform one final fit of the model which we are going to use for prediction on the test set.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span> <span class="o">=</span> <span class="n">fit_xgboost_model</span><span class="p">(</span><span class="n">imputed_X_train</span><span class="p">,</span> <span class="n">label_y_train</span><span class="p">,</span> <span class="n">imputed_X_valid</span><span class="p">,</span> <span class="n">label_y_valid</span><span class="p">,</span> <span class="n">best_learning_rate</span><span class="p">,</span> <span class="n">best_n_estimators</span><span class="p">)</span>
</code></pre></div></div>

<h1 id="predicting-winloss">Predicting Win/Loss</h1>

<p>As we know the real outcome of the matches in the test set, we can compare the predictions</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">preds_test</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">imputed_X_test</span><span class="p">)</span>
</code></pre></div></div>

<p>with the actual results</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">output</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s">"gameid"</span><span class="p">:</span> <span class="n">imputed_X_test</span><span class="p">.</span><span class="n">index</span><span class="p">,</span> <span class="s">"team1_did_win"</span><span class="p">:</span> <span class="n">preds_test</span><span class="p">})</span>
<span class="n">output</span><span class="p">[</span><span class="s">"team1_did_win"</span><span class="p">]</span> <span class="o">=</span> <span class="n">output</span><span class="p">[</span><span class="s">"team1_did_win"</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mf">0.5</span>
<span class="n">output</span> <span class="o">=</span> <span class="n">output</span><span class="p">.</span><span class="n">set_index</span><span class="p">(</span><span class="s">"gameid"</span><span class="p">)</span>
</code></pre></div></div>

<p>and calculate the accuracy of our predictions</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">check</span> <span class="o">=</span> <span class="n">output</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">lsuffix</span><span class="o">=</span><span class="s">"_pred"</span><span class="p">,</span> <span class="n">rsuffix</span><span class="o">=</span><span class="s">"_test"</span><span class="p">)</span>
<span class="n">check</span><span class="p">[</span><span class="s">"team1_did_win_test"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">check</span><span class="p">[</span><span class="s">"team1_did_win_test"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"Win"</span><span class="p">)</span>

<span class="n">check</span><span class="p">[</span><span class="s">"equal"</span><span class="p">]</span> <span class="o">=</span> <span class="n">check</span><span class="p">[</span><span class="s">"team1_did_win_test"</span><span class="p">]</span> <span class="o">==</span> <span class="n">check</span><span class="p">[</span><span class="s">"team1_did_win_pred"</span><span class="p">]</span>
<span class="n">test_pred_accuracy_percent</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">check</span><span class="p">[</span><span class="n">check</span><span class="p">[</span><span class="s">"equal"</span><span class="p">]</span> <span class="o">==</span> <span class="bp">True</span><span class="p">])</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">check</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span>
<span class="k">print</span><span class="p">(</span><span class="s">"The accuracy on the test set is "</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">test_pred_accuracy_percent</span><span class="p">)</span> <span class="o">+</span> <span class="s">" %"</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The accuracy on the test set is 70.67495737639153 %
</code></pre></div></div>

<p>As can be seen, we are able to reach an accuracy of 70 % with that simple modeling approach by just taking into account data from the first 10 minutes of the match. This is quite remarkable as it means we are able to correctly predict the outcome of a match after the first 10 minutes more than 2 out of 3 times.</p>

<h1 id="summary-and-discussion">Summary and discussion</h1>

<p>We used the Riot Games API to fetch match data for approx. 50,000 matches for game version 10.13 5v5 Solo Queue on Summoner’s Rift. From the fetched match data we extracted features for the first 10 minutes of the match. Using these features we were able to train a model which can predict the winner of the game to an accuracy of 70%, by just looking at the first 10 minutes of that match.</p>

<h1 id="appendix">Appendix</h1>

<p>We will take a look at additional metrics which are part of the match data we fetched. We are especially interested in the win rate based on firsts in the match.</p>

<p>Towers are important defensive structures in the game and losing one opens up the possibilities for the other team. As can be seen in the next figure, it can indeed be relevant to lose the first tower.</p>

<p><img src="/assets/img/output_33_1.png" alt="" />
<em>Figure 1: First Tower Win vs Fail for different regions</em></p>

<p>Now it can of course be also interpreted that the result of all what happened before in the match tilted the match into one teams favor making it easier for them to take out a tower. Nevertheless, it can be shown that the win chance and the first tower kill is highly correlated.</p>

<p>In addition, Baron is a very important objective in the game. Not only does it provide a large boost for the team taking it, but it usually also indicates that the match is already going in the favor of that team. In the Win/Fail rate for first Baron kill there is a clear tendency to Win for the team taking the Baron and it is nearly impossible to turn a match if the opposite team takes a Baron.</p>

<p><img src="/assets/img/output_34_1.png" alt="" />
<em>Figure 2: First Baron Win vs Fail for different regions</em></p>

<p>Note: Here the Win/Fail rates do not sum up to 100%, as there are games were neither team takes the Baron. In an extended analysis one should only take matches were the Baron has been taken into consideration for that plot. Nevertheless, it is clear from these numbers, that it is quite hard to turn a game if the opposing team was able to secure the first Baron of the match.</p>

<p>The dragon is an earlier objective and usually not that important in the outcome of a match. Nevertheless, also here it can be seen that getting the first dragon indicates that that team is on a good way to end the match victorious:</p>

<p><img src="/assets/img/output_36_1.png" alt="" />
<em>Figure 3: First Dragon Win vs Fail for different regions</em></p>

<p>The first blood of the match (who killed the a champion of the enemy team first) can happen quite early in a match and from all the investigated metrics this one is the one which does not indicate if a team will win or not that clearly. It seems to be still an indication what team is going to win, however:</p>

<p><img src="/assets/img/output_35_1.png" alt="" />
<em>Figure 4: First Blood Win vs Fail for different regions</em></p>

<p>The last analysis we are going to perform is the average game length. Using the data of all the matches we fetched for game version 10.13 played on Summoner’s Rift and in solo 5v5 games we find that, independent of the region, the game duration is approximately close to 30 minutes. We also find the interesting phenomenon that shortly after it is possible to surrender a match, more matches end, which indicates that teams already consider it lost after just 15 minutes into the match. But as we are able to predict the outcome of a game with just 10 minutes of data, those players may indeed be right that they consider the match lost already after just 15 minutes into the match.</p>

<p><img src="/assets/img/output_37_1.png" alt="" />
<em>Figure 5: Distribution of game length for various regions</em></p>

<h1 id="references">References</h1>

<p>[1] Chen, Tianqi, and Carlos Guestrin. “XGBoost.” Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (2016), <a href="https://arxiv.org/abs/1603.02754">https://arxiv.org/abs/1603.02754</a>.<br />
[2] <a href="https://matplotlib.org/">https://matplotlib.org/</a><br />
[3] <a href="https://numpy.org/">https://numpy.org/</a><br />
[4] <a href="https://pandas.pydata.org/">https://pandas.pydata.org/</a><br />
[5] <a href="https://pymongo.readthedocs.io/en/stable/">https://pymongo.readthedocs.io/en/stable/</a><br />
[6] <a href="https://seaborn.pydata.org/">https://seaborn.pydata.org/</a><br />
[7] <a href="https://scikit-learn.org/stable/">https://scikit-learn.org/stable/</a><br />
[8] <a href="https://xgboost.readthedocs.io/en/latest/">https://xgboost.readthedocs.io/en/latest/</a></p>]]></content><author><name></name></author><category term="Machine" /><category term="Learning" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Lili’s Quest | Week 5</title><link href="https://torlenor.org/lili's/quest/2020/06/27/lilis_quest_week_5.html" rel="alternate" type="text/html" title="Lili’s Quest | Week 5" /><published>2020-06-27T09:45:00+00:00</published><updated>2020-06-27T09:45:00+00:00</updated><id>https://torlenor.org/lili&apos;s/quest/2020/06/27/lilis_quest_week_5</id><content type="html" xml:base="https://torlenor.org/lili&apos;s/quest/2020/06/27/lilis_quest_week_5.html"><![CDATA[<p>This week was heavily driven by refactoring. A lot more entity properties got moved into components, which only hold data. Currently the references to these components are still part of the entity, so for now the entity is still more than just an ID as one would usually have in a strict entity-component-system sense.</p>

<p>During that refactoring also the parsing of the entity definitions (JSON files) got much easier. Utilizing Go’s awesome marshalling/unmarshalling interface the parse functions got shortened and types like MutationEffect now know themselves how to unmarshal from a JSON string into an actually MutationEffect.</p>

<p>In terms of game play I thought of some Mutations I want to add in a first iteration and which functionalities the game has to have for them. E.g., it would be nice to be able to dig through a wall, but for that I need to have destructible walls. In a similar way I want to have force fields, there I need to construct walls!</p>

<p>Except of that I added a lot of TODOs. The next step will be to prioritize them. As game play should be my focus, I will probably start implementing the functionalities needed for the mutations first. UI will then be next on the list.</p>]]></content><author><name></name></author><category term="Lili&apos;s" /><category term="quest" /><summary type="html"><![CDATA[This week was heavily driven by refactoring. A lot more entity properties got moved into components, which only hold data. Currently the references to these components are still part of the entity, so for now the entity is still more than just an ID as one would usually have in a strict entity-component-system sense.]]></summary></entry><entry><title type="html">Lili’s Quest | Week 4</title><link href="https://torlenor.org/lili's/quest/2020/06/21/lilis_quest_week_4.html" rel="alternate" type="text/html" title="Lili’s Quest | Week 4" /><published>2020-06-21T06:25:00+00:00</published><updated>2020-06-21T06:25:00+00:00</updated><id>https://torlenor.org/lili&apos;s/quest/2020/06/21/lilis_quest_week_4</id><content type="html" xml:base="https://torlenor.org/lili&apos;s/quest/2020/06/21/lilis_quest_week_4.html"><![CDATA[<p>During this week I worked on refactoring the input handling, to make it easier adding new keyboard shortcuts. This also has the advantage, that I can disentangle the SDL events from the actual input handling in the program. Later on I have to think about a system on how to have different input key bindings/behaviors depending on the state of the game (main menu, options menu, inventory modal open, for example), but this is something I have to think of when I actually have additional game states.</p>

<p>In addition, I was not happy with the rendering. I wanted to have the ability to render directly onto a grid and therefore I added a console. For now there is only MatrixConsole which can work with square or rectangle fonts (e.g., 12x12 or 6x12) that form a grid. In the same manner as with libtcod you can then put chars onto those grids and customize their foreground and background color. The game map is using this now, which makes rendering much simpler. Due to that refactoring/rewrite I also moved the rendering of the entities into the game map, which makes more sense to me than in the actual game logic.</p>

<p>A simple main menu is now implemented. It can only start the game or quit the application and has placeholders for Options and Load Game. It uses the same MatrixConsole as the map, just with a different font texture. Ideally I can use a Text Console later on, but for now it is good enough to get the logic of the menus set up.</p>

<p>Oh, and here is a new GIF:</p>

<p><img src="https://i.imgur.com/P0M4eYA.gif" alt="" /></p>]]></content><author><name></name></author><category term="Lili&apos;s" /><category term="quest" /><summary type="html"><![CDATA[During this week I worked on refactoring the input handling, to make it easier adding new keyboard shortcuts. This also has the advantage, that I can disentangle the SDL events from the actual input handling in the program. Later on I have to think about a system on how to have different input key bindings/behaviors depending on the state of the game (main menu, options menu, inventory modal open, for example), but this is something I have to think of when I actually have additional game states.]]></summary></entry></feed>