{"id":4047,"date":"2026-11-01T17:36:25","date_gmt":"2026-11-01T17:36:25","guid":{"rendered":"https:\/\/redfrontdoor.org\/blog\/?p=4047"},"modified":"2026-11-01T17:36:25","modified_gmt":"2026-11-01T17:36:25","slug":"but-can-it-run-doom","status":"publish","type":"post","link":"https:\/\/redfrontdoor.org\/blog\/?p=4047","title":{"rendered":"&#8220;But can it run Doom?&#8221;"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Some time ago now, a colleague and I were running a coding workshop for young people, using Pytch. We were talking with the club&#8217;s facilitator about Pytch, and he asked the age-old question: <em>But can it run Doom?<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The obvious answer was &#8220;no, sorry, this is just a 2D system&#8221;, but the question nagged at me, and eventually I put some time into thinking properly about whether Pytch could do <em>something<\/em> which was <em>a bit like<\/em> Doom.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It could!<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter\"><video height=\"360\" style=\"aspect-ratio: 480 \/ 360;\" width=\"480\" controls src=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/pytch-doom-walking-around.mp4\"><\/video><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Background<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/pytch.org\/\">Pytch<\/a><\/strong> is a system for teaching people Python coding, providing an environment which is very close to MIT&#8217;s Scratch. The graphical output consists of two-dimensional &#8220;sprites&#8221;, which can wear &#8220;costumes&#8221; of the coder&#8217;s choosing and appear at a chosen location, size, and rotation. This is the only way graphics can be generated.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"529\" src=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/pytch-ide-shooting-stars-1024x529.png\" alt=\"\" class=\"wp-image-4050\" style=\"aspect-ratio:1.9358323430917144;width:558px;height:auto\" srcset=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/pytch-ide-shooting-stars-1024x529.png 1024w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/pytch-ide-shooting-stars-300x155.png 300w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/pytch-ide-shooting-stars-767x396.png 767w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/pytch-ide-shooting-stars-1536x793.png 1536w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/pytch-ide-shooting-stars.png 1920w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Screenshot of the Pytch IDE<\/figcaption><\/figure>\n<\/div>\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"600\" height=\"450\" src=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/doom-screenshot.jpg\" alt=\"\" class=\"wp-image-4052\" style=\"aspect-ratio:1.333348397884937;width:456px;height:auto\" srcset=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/doom-screenshot.jpg 600w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/doom-screenshot-300x225.jpg 300w\" sizes=\"auto, (max-width: 600px) 100vw, 600px\" \/><figcaption class=\"wp-element-caption\">Screenshot of the original DOOM<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">Many people will be familiar with <strong>Doom<\/strong>. It was a PC game released in 1993, with 3D first-person views which were extremely impressive for the hardware of that era. In the years since then, Doom has been made to run on various surprising systems:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/doomwiki.org\/wiki\/NDoom\">A graphical calculator<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/boingboing.net\/2024\/02\/27\/doom-on-husqvarna-lawnmowers.html\">A lawnmower<\/a>, at least temporarily.<\/li>\n\n\n\n<li><a href=\"https:\/\/simonwillison.net\/2025\/Feb\/27\/typescript-types-can-run-doom\/\">TypeScript&#8217;s type system<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/cedardb.com\/blog\/sqldoom\/\">A SQL database<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Can we add Pytch to the list?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Research and reference<\/h2>\n\n\n\n<figure class=\"wp-block-image size-full is-resized has-custom-css wp-custom-css-351f93ff\"><img loading=\"lazy\" decoding=\"async\" width=\"480\" height=\"600\" src=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/gebb-doom-cover.png\" alt=\"\" class=\"wp-image-4053\" style=\"width:214px;height:auto\" srcset=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/gebb-doom-cover.png 480w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/gebb-doom-cover-240x300.png 240w\" sizes=\"auto, (max-width: 480px) 100vw, 480px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">I had, long ago, done some reading on Doom&#8217;s binary space partition (BSP) mechanism, and how it enabled its rendering efficiency. I needed to refresh my memory on how this worked. These resources were very useful in doing so:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <a href=\"https:\/\/fabiensanglard.net\/gebbdoom\/\">Game Engine Black Book: DOOM<\/a> by Fabien Sanglard.<\/li>\n\n\n\n<li>The <a href=\"https:\/\/github.com\/id-Software\/DOOM\">Linux DOOM<\/a> source code.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Cutting down the scope<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The full game of Doom has multiple levels, monsters, weapons, sound effects, and much more. To have a realistic chance of declaring success, I had to cut the scope right down. Combining the above research with some quick experiments, and with knowledge of what would be possible in Pytch, I came up with the following.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The player should be able to move round a simplified level from the original shareware Doom, with its first-person view. Walls should be rendered as Doom does, but height information can be discarded, and floor graphics, steps, windows, and similar, need not be implemented.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I took the &#8220;Episode 1, Mission 3&#8221; map:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"960\" height=\"699\" src=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-original.png\" alt=\"\" class=\"wp-image-4054\" srcset=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-original.png 960w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-original-300x218.png 300w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-original-768x559.png 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">and reduced it just to its boundary:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"960\" height=\"699\" src=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-one-sided-only.png\" alt=\"\" class=\"wp-image-4055\" srcset=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-one-sided-only.png 960w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-one-sided-only-300x218.png 300w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/e1m3-one-sided-only-768x559.png 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This threw away a lot of detail, but the hope was that it would still be recognisably Doom-like.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Implementation of rendering<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One approach would be to have one sprite-clone per pixel of a 320\u00d7240 screen, changing their colours every frame. But quick tests showed that it was completely infeasible to make this work at a reasonable frame rate.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, to render the player&#8217;s view, the Pytch program has 320 clones of an &#8220;texture slice&#8221; sprite. That sprite has one costume for every pixel-column of every texture image used in the level (3,056 in total). Every frame, the code computes what slice of what texture at what scale should be shown in that display column, and the sprite switches to that costume and sets its size as needed. There is also some vertical adjustment to get the &#8220;bobbing point-of-view&#8221; effect.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pytch cannot read files, so the required data from the WAD file is converted into hard-coded Python lists and included in the program.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The overall Python code, suitable for running under Pytch, is a mixture of hand-written Python code for the algorithms and the generated code for the data describing the level. A top-level script pulls everything together to make a lump of Python which Pytch can run. It comes to around 2,000 lines, around 45% code and 55% data.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Details<\/h3>\n\n\n<div class=\"wp-block-image wp-custom-css-3cae7c75\">\n<figure class=\"aligncenter size-full is-resized has-custom-css\"><img loading=\"lazy\" decoding=\"async\" width=\"960\" height=\"960\" src=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/allowzone-detail.png\" alt=\"\" class=\"wp-image-4056\" style=\"width:197px;height:auto\" srcset=\"https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/allowzone-detail.png 960w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/allowzone-detail-150x150.png 150w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/allowzone-detail-300x300.png 300w, https:\/\/redfrontdoor.org\/blog\/wp-content\/uploads\/2026\/11\/allowzone-detail-768x768.png 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">To prevent the player walking out of the level, I shrank the boundary wall by a small amount to get the region where the player is allowed to be:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The code then makes sure the player didn&#8217;t cross that smaller polygon&#8217;s boundary, using a very similar idea to the original Doom&#8217;s &#8220;blockmap&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the original Doom, there is careful use of, among other tricks, fixed-width integer arithmetic and look-up tables for trigonometric functions. It also has many other clever optimisations. I instead just used standard Python floating-point arithemetic, the usual <code>math.sin()<\/code> and friends, standard Python lists, and so on. This turned out to be quick enough, and to almost always work fine, although there is the occasional glitch (e.g., at around 04:05 in the above video), where I suspect some rounding didn&#8217;t quite come out right. Investigating and fixing this could be future work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Computers are quite fast these days, even when running Python transpiled to Javascript (via Skulpt) and running in the browser. One change which made a big difference to rendering speed was getting the column sprites to switch costume by index rather than by costume name, thereby saving something like 28 million Python string comparisons (converted to Javascript) per second.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Part-way through development, Chrome had an update which broke Pytch-Doom. The problem seems to be that recent Chrome versions impose a limit on the number of images a page can load. Firefox works fine.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Development workflow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I did some prototyping in Jupyter notebooks, which made it easier to try out test cases and explore the WAD data. I then moved the pieces of code across to standalone Python files.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These tools and libraries were very useful:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <a href=\"https:\/\/eureka-editor.sourceforge.net\/\">Eureka WAD editor<\/a><\/li>\n\n\n\n<li>The <a href=\"https:\/\/github.com\/devinacker\/omgifol\">omgifol Python library<\/a><\/li>\n\n\n\n<li>The <a href=\"https:\/\/github.com\/shapely\/shapely\">shapely Python library<\/a> for calculations involving polygons<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Results<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In its limited scope, it works! It runs at a respectable frame-rate of 60fps on my desktop machine and looks like the video at the top of this post.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Thoughts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There isn&#8217;t really anything new here, but it was fun to do all the same. The task was mostly the engineering work of porting Doom&#8217;s algorithms and C code to Python, and representing the required data in code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I think this <em>just about<\/em> counts as &#8220;running something Doom-like&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As an unexpected bonus, the &#8220;fade to black in the distance&#8221; effect of the original Doom came for free. When rendering a one-pixel wide image at a scale of less than one, the browser anti-aliases by making the image strip partially transparent, allowing some of the dark background to show through.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It was good to be reminded of the very clever algorithms and processor-tuned engineering used by the original Doom.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As you can see in the video, I am not very good at this kind of game. My poor sense of direction doesn&#8217;t help.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Source code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All the code is available in this GitHub repository:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/github.com\/bennorth\/pytch-doom\">Pytch Doom git repository<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Look in the <code>build<\/code> directory for details of how to reconstruct the Pytch project within <a href=\"https:\/\/pytch.org\/\">the Pytch webapp<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Statement on AI usage<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I did not use AI while exploring and writing the original code. Where would be the fun in that? But in preparing this post, I did use Claude to extract some of the standalone scripts in the <code>build\/<\/code> directory from the various hacky notebooks and scripts that I had written while developing the ideas. I also used Claude to hack in a quick extension to Pytch to allow frame-perfect recording of the rendering, and to hack up a script to produce an MP4 file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Image credits<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Created by me for this blog post, except:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Doom screenshot: <a href=\"https:\/\/www.computerhistory.org\/revolution\/computer-games\/16\/202\/855\">Computer History Museum<\/a><\/li>\n\n\n\n<li>Game Engine Black Book cover: <a href=\"https:\/\/fabiensanglard.net\/gebbdoom\/\">Fabien Sanglard<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Some time ago now, a colleague and I were running a coding workshop for young people, using Pytch. We were talking with the club&#8217;s facilitator about Pytch, and he asked the age-old question: But can it run Doom? The obvious answer was &#8220;no, sorry, this is just a 2D system&#8221;, but the question nagged at me, and eventually I put some time into thinking properly about whether Pytch could do something which was a bit like Doom. It could! Background Pytch is a system for teaching people Python coding, providing an environment which is very close to MIT&#8217;s Scratch. The graphical output consists of two-dimensional &#8220;sprites&#8221;, which can wear &#8220;costumes&#8221; of the coder&#8217;s choosing and appear at a chosen location,<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4047","post","type-post","status-publish","format-standard","hentry","category-uncategorized","comments-off"],"_links":{"self":[{"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4047","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4047"}],"version-history":[{"count":8,"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4047\/revisions"}],"predecessor-version":[{"id":4062,"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4047\/revisions\/4062"}],"wp:attachment":[{"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4047"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4047"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/redfrontdoor.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4047"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}