<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged pygame</title>
<link>https://coderlegion.com/tag/pygame</link>
<description>Powered by Question2Answer</description>
<item>
<title>Actor Models in Pygame: Lessons in Serialization and State Isolation</title>
<link>https://coderlegion.com/9117/actor-models-in-pygame-lessons-in-serialization-and-state-isolation</link>
<description>&lt;p&gt;Our attempt to build a prototype &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/recreating-the-matrix-rain-with-pygame-manual-fades-and-the-transparency-of-code-ab24820ea4bd&quot;&gt;recreating the matrix rain effect&lt;/a&gt; last week forced us to revise the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events-1945&quot;&gt;mini Pygame framework&lt;/a&gt;. The implementation of the idea yielded a mixed result that I previously overlooked. No, it wasn’t due to my take on the Actor model, in case you’re wondering. What could the problem be then?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*M9CSjtSo3h1WF3k9RPI8Ug.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Cute doge on an adventure. Image courtesy of Microsoft Copilot.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/recreating-the-matrix-rain-with-pygame-manual-fades-and-the-transparency-of-code-ab24820ea4bd&quot;&gt;Recreating the Matrix Rain with Pygame: Manual Fades and the Transparency of Code.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events-1945&quot;&gt;The Pygame Framework I Didn't Plan: Building Tic-Tac-Toe with Asyncio and Events&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-vision-searching-for-order-in-the-matrix&quot;&gt;The Vision: Searching for Order in the Matrix&lt;/h4&gt;
&lt;p&gt;So what is this Actor model thing, you may wonder. Having originated from our experiment with Pygame through building a tic-tac-toe game, the framework was a “side-product” of the project. Extracting the framework out through refactoring was done in the hope of making it portable and applicable for other projects. It worked, as shown in the previous article, but it wasn’t perfect. We identified some problems too, namely inefficiency in the management of application state and the event dispatch lookups.&lt;/p&gt;
&lt;p&gt;I can already hear criticism from afar saying the latter is just a design failure, but since we are fixing the data structure, why not just fix both in one go?&lt;/p&gt;
&lt;p&gt;Jokes aside, let’s start by looking into the Actor model. Skipping a formal definition of the term, we let the code be a reference to the discussion. For the scope of this article, the model is merely something that looks like this sample code returned by Gemini.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class UpdateState:
    &quot;&quot;&quot;Message to change state (Write)&quot;&quot;&quot;
    key: str
    value: Any

@dataclass
class GetState:
    &quot;&quot;&quot;Message to request state (Read)&quot;&quot;&quot;
    key: str
    reply_to: asyncio.Future # Used to send the result back

async def producer(inbox: asyncio.Queue):
    &quot;&quot;&quot;Simulates a system event updating state (Write)&quot;&quot;&quot;
    await asyncio.sleep(1)
    print(&quot;[Producer] Sending update: score = 100&quot;)
    await inbox.put(UpdateState(&quot;score&quot;, 100))

async def consumer(inbox: asyncio.Queue):
    &quot;&quot;&quot;Simulates a UI element needing to read state (Read)&quot;&quot;&quot;
    await asyncio.sleep(2)
    print(&quot;[Consumer] I need the 'score'...&quot;)
    
    # Create a 'return envelope'
    future = asyncio.get_event_loop().create_future()
    await inbox.put(GetState(&quot;score&quot;, future))
    
    # Wait for the Actor to fill the envelope
    result = await future
    print(f&quot;[Consumer] Received score: {result}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A loop is running in the background reading from a queue (denoted by the inbox argument in the coroutine functions) expecting one of the two dataclasses above, each representing a type of request. When it is an UpdateState, the value is changed. Otherwise, the value is returned via the asyncio.Future. Some disassembly was done to the model to keep it simple, though essentially we now can restrict access to the state only through the mailbox (or inbox in the snippet).&lt;/p&gt;
&lt;p&gt;On the other hand, fixing the event dispatch problem merely involves a slight redesign of the application state. Ideally, we should aim for a near &lt;strong&gt;O(1)&lt;/strong&gt; performance, let’s just use a hashmap for the purpose, preferably a frozendict(). Roughly speaking, we want the application state that stores events handlers as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass(frozen=True)
class Application:
    ...
    events: frozendict[tuple[Element, int], EventHandler]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are storing the handlers into a frozendict to stick with functional principles as much as possible. Immutability is the key to writing predictable and idempotent functions.&lt;/p&gt;
&lt;p&gt;Another advantage of adopting the Actor model is it opens up a possibility of clean separation from other components. When there is a possibility of overdoing things, you may already know by now I am always there. My attempt to overengineer for the sake of learning eventually caused a major rewrite, though fortunately the implementation still centers around what we have discussed so far. Sit tight, as we are diving deep into an adventure full of misjudgments.&lt;/p&gt;
&lt;h4 id=&quot;trekking-inward-drafting-the-actor-blueprint&quot;&gt;&lt;strong&gt;Trekking Inward: Drafting the Actor Blueprint&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Doesn’t sound like an adventure so far?&lt;/p&gt;
&lt;p&gt;Think of a request trekking into an application via a queue. The previous section would be like a roughly sketched overview map. Now is the time to join a relatively steady stream of requests. Looking at our map, we see some landmarks: a scheduler, a mailbox (basically a queue), some processing logic, and lastly a mechanism to reply. Implementing this in the context of our little Pygame framework is the goal we are trying to achieve here. There is some flexibility I wish to implement, specifically regarding how the request affects the application state.&lt;/p&gt;
&lt;p&gt;Let’s start by requests.&lt;/p&gt;
&lt;p&gt;An actor only responds to requests, and the outcome would be different depending on the types. Requests are then passed into an actor through a mailbox, in our case, a queue. Everything seems perfectly fine, until you see a small and almost illegible fine print in the map — Pygame is generally not thread safe, i.e. we run all Pygame code within the same thread. That means we need to be really aware when passing Pygame objects through queues, for instance, passing them through a multiprocessing Queue will end up a crash due to serialization failure.&lt;/p&gt;
&lt;p&gt;Each request represents an action we want to do with the data, either a retrieval or a mutation. We start by defining a mutation request, the SetRequest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class Operation:
    op: Callable
    args: tuple = field(default_factory=tuple)
    kwargs: dict = field(default_factory=dict)

@dataclass
class SetRequest[Data]:
    operation: Operation

    async def __call__ (self, data: Data) -&amp;gt; Data:
        return await self.operation.op(
            data, *self.operation.args, **self.operation.kwargs
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of consolidating all the application state changes within one huge monolithic match-case statement, SetRequest delegates the mutation logic definition to the caller. Unfortunately, this also means the caller needs to be aware of the state’s internal structure. Despite that downside, the delegation should promote the caller to do a small, localized change to the state that holds multiple pieces of data.&lt;/p&gt;
&lt;p&gt;On the other hand, there are cases where we want the current state. Retrieving multiple slivers of current state one at a time to complete a task can be rather inefficient as our requests could be stuck queuing up when traffic is busy, and that makes me think. The reason we are making the queries is to get things done; what if we bundle the processing logic over, like we did with SetRequest too? Rather than repeating the “fill in the blanks” exercise once for each sentence, we are doing the whole essay all at once. In short, bundling the retrieval requests into one execution request results in smaller queues. For that goal, we implement ExecuteRequest as such:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class ExecuteReply:
    tag: UUID
    payload: Any

@dataclass
class ExecuteReplyTo:
    tag: UUID
    mailbox: Queue[ExecuteReply]

@dataclass
class ExecuteRequest[Data]:
    operation: Operation
    reply_to: ExecuteReplyTo | None = None

    async def __call__ (self, data: Data) -&amp;gt; None:
        result = asyncio.create_task(
            self.operation.op(data, *(self.operation.args), **(self.operation.kwargs))
        )

        if self.reply_to:
            asyncio.create_task(
                asyncio.to_thread(
                    self.reply_to.mailbox.put,
                    ExecuteReply(self.reply_to.tag, await result),
                )
            )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In most cases, we do not expect the caller to care when the request is completed. What’s wrong with having an asyncio.Future like we saw earlier? The short answer is no, but I would like to defer this until we are discussing the overengineering part. Yet, if the caller cares about the reply, the request requires a recipient mailbox, and an identifier UUID for the recipient.&lt;/p&gt;
&lt;p&gt;A scheduler is then built to bind everything together. It takes two arguments, namely the application state that it manages, and a mailbox that acts as the single point of entry for the state. Refer to the snippet below for a simplified implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def state_scheduler[Data](state: [Data], mailbox: Queue[SetRequest[Data] | ExecuteRequest[Data]]):
    result = state
    while True:
        try:
            match request := await asyncio.to_thread(mailbox.get, timeout=0.001):
                case SetRequest():
                    result = await request(result)

                case ExecuteRequest():
                    asyncio.create_task(request(result))

        except Empty:
            pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some may find the snippet resembles a reduce function that loops infinitely. On each iteration, the result should only get replaced if a SetRequest is received. Imagine if the state isn’t immutable, even the execute request would have a chance of breaking the integrity of the object. Both SetRequest and ExecuteRequest are also expected to &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-versatility-of-call-a-python-developers-secret-weapon-4akd&quot;&gt;implement the&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-versatility-of-call-a-python-developers-secret-weapon-4akd&quot;&gt;__call__ dunder&lt;/a&gt; as shown in respective snippet. Remember the metaphor on bundling the “fill in the blanks” exercise into a callable when we discussed ExecuteRequest? It gets scheduled for execution here.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-versatility-of-call-a-python-developers-secret-weapon-4akd&quot;&gt;The Versatility of __call__: A Python Developer's Secret Weapon&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-bumpy-ride-overengineering-the-parallel-dream&quot;&gt;&lt;strong&gt;The Bumpy Ride: Overengineering the Parallel Dream&lt;/strong&gt;&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;“If it’s worth doing, it’s worth overdoing”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I didn’t watch a lot of MythBusters growing up, but this motto has a special place in my heart. Hang on tight, this is also where our relaxing boat ride gets a little bumpy. The Actor model offers a possibility to cleanly separate the state away from the rest of the application. Seems like a good opportunity to further split the application to multiple processes right? The application state actor, display update loop, as well as the event dispatch loop will all run independently, right?&lt;/p&gt;
&lt;p&gt;Let’s overengineer this project, because more processes in parallel means faster execution, right?&lt;/p&gt;
&lt;p&gt;Running each component in a separate process is done through the usual ProcessPoolExecutor setup. Further discussion on the topic can be found in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/concurrency-vs-parallelism-achieving-scalability-with-processpoolexecutor-1n7n&quot;&gt;AsyncIO article series&lt;/a&gt; and hence we are skipping the details here. Why dedicate a section for the topic then?&lt;/p&gt;
&lt;p&gt;Remember we deferred the discussion of having an asyncio.Future for replies? Turns out AsyncIO has the same constraint as Pygame — the objects should stay within the thread. As we know, objects get serialized when they are sent through a cross-process queue. Recreating an asyncio.Future in another process will raise an exception, causing the execution to fail.&lt;/p&gt;
&lt;p&gt;For the same reason, we are also avoiding sending Pygame objects through cross-process queues.&lt;/p&gt;
&lt;p&gt;You might notice that ExecuteReplyTo takes a tag UUID and mailbox. According to the dataclass definition we know mailbox is another cross-process queue, but what does the tag identifier do?&lt;/p&gt;
&lt;p&gt;It is a key to our registry of asyncio.Future that lives only in the caller’s process, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def inbox_consume(
    inbox: Queue[ExecuteReply], futures: dict[UUID, asyncio.Future]
) -&amp;gt; None:
    with suppress(Empty):
        reply = await asyncio.to_thread(inbox.get_nowait)

        if futures[reply.tag]:
            futures[reply.tag].set_result(reply.payload)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each process maintains its own registry and a cross-process queue. A loop is set up to run inbox_consume repeatedly such that the corresponding asyncio.Future objects get notified as soon as the reply arrives.&lt;/p&gt;
&lt;p&gt;As an example, this is how it works in action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def some_callable(state) -&amp;gt; bool:
    ...

    return True

async def caller(...):
    done_future = uuid4()
    futures[done_future] = asyncio.Future()
    game_in.put(
        ExecuteRequest(
            Operation(some_callable),
            ExecuteReplyTo(done_future, futures_mailbox),
        ),
    )
    await futures[done_future]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s a brief explanation to the snippet above:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;All callables defined for SetRequest and ExecuteRequest receive the current state as the first argument&lt;/li&gt;
&lt;li&gt;We register a new asyncio.Future to a UUID tag before firing the ExecuteRequest.&lt;/li&gt;
&lt;li&gt;The tag and the future_mailbox to update the registry of asyncio.Future are sent as a ExecuteReplyTo object.&lt;/li&gt;
&lt;li&gt;The bundled some_callable runs in the process where the state lives.&lt;/li&gt;
&lt;li&gt;Back in the caller’s process, since the ExecuteRequest expects a return value, we perform a blocking await at the corresponding entry in the registry of asyncio.Future.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Some Pygame objects are stored within the application state object, so it runs in the process that calls the Pygame API. While it seems like we managed to split the application into three separate processes, if we are delegating all requests back to the process that manages Pygame anyway, is the added complexity worth it?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*psFje8oVqYwFTul4IDmNuA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Silently screaming at the low FPS&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Grab onto something quick, as we are heading towards a waterfall.&lt;/p&gt;
&lt;h4 id=&quot;the-waterfall-a-30-fps-reality-check&quot;&gt;&lt;strong&gt;The Waterfall: A 30 FPS Reality Check&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Though there were hiccups, the code eventually ran. For a while I was quite proud of myself for getting everything assembled properly. I still remember the realization came about an hour later when I was staring at the code on pushing visual changes to the application window. That was the fall off a cliff moment. So what went wrong? The separation into multiple processes was not such a good idea eh?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*hiD-KescU1f619nU&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Aditya Chinchure on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Pygame.time.Clock has a method called .tick(). It is meant to be called once per frame. If a framerate is specified, it would block until time is up to prevent premature frame update. That practically means the callable sent by the process managing the display is going to be blocking the process hosting the scheduler for the Actor (which also holds the application state containing Pygame objects). Considering this callable also gets sent repeatedly in an infinite loop, it means the Actor process is constantly blocking, leaving little room for concurrency.&lt;/p&gt;
&lt;p&gt;Another bottleneck that I underestimated is how the serialization works through the cross-process queue could potentially slow things down. If I was falling when the realization of the blocking operation happened, recognizing this bottleneck is when my boat and I plunged into the water down the cliff. The struggle to get back to the surface almost became measurable, when I started to compare the frame per second figures.&lt;/p&gt;
&lt;p&gt;Instead of hitting the 60fps target, we are only seeing frame rate in the 30s. It is obvious that the added complexity that involves cross-process message passing is the culprit. Measuring is going to be hard, but I would imagine the constant conversion between an actual project and serialized form would contribute to the delay.&lt;/p&gt;
&lt;p&gt;We are now stuck in the middle of nowhere; the expedition failed.&lt;/p&gt;
&lt;p&gt;What’s next?&lt;/p&gt;
&lt;h4 id=&quot;the-principled-pivot-painting-with-experience&quot;&gt;&lt;strong&gt;The Principled Pivot: Painting with Experience&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;We failed the mission to cleanly separate the components into independent processes. Despite that, I am still happy throughout the adventure. The expedition pushed me far enough from my comfort zone and all the insights gained were valuable. Failing is never a problem, as long as we are learning something through it.&lt;/p&gt;
&lt;p&gt;Like many fellow engineers, I do have a fair share of failures when I write code. Every new project I work on is not unlike an expedition into the unknown. While the outcome is important, we need to be aware of every step we take too. If experience is a painting, the trail of foot steps is exactly how lines are formed in an image.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*HxQ7bnnIcm3opyK5&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Andrey Novik on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So what do we learn in this in-progress painting of our Pygame experiment?&lt;/p&gt;
&lt;p&gt;Looking at the bright side, I really liked how the application state is now managed through the Actor model. After removing the multi-process setup, certain parts would be greatly simplified. Overall, it is much more manageable than my poorly implemented model. Another revision is definitely happening and hopefully the fps count recovers.&lt;/p&gt;
&lt;p&gt;Lastly, thanks for another year of support, and I shall write again soon. Bonne Année.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Finally, I’d like to acknowledge the editorial and research support provided by Gemini in producing this write-up. While the architecture and the hard-learned lessons are mine, Gemini acted as a second set of eyes — helping me audit the sentence-to-sentence flow, cross-referencing patterns like the Active Object and the “Ask” pattern, and ensuring that my “expedition” through the code was as clear to you as it was bumpy for me. It’s been a collaborative effort to ensure that even my failures are presented with the technical rigor they deserve.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/9117/actor-models-in-pygame-lessons-in-serialization-and-state-isolation</guid>
<pubDate>Wed, 31 Dec 2025 03:53:15 +0000</pubDate>
</item>
<item>
<title>Recreating the Matrix Rain with Pygame: Manual Fades and the Transparency of Code.</title>
<link>https://coderlegion.com/8641/recreating-the-matrix-rain-with-pygame-manual-fades-and-the-transparency-of-code</link>
<description>&lt;p&gt;Earlier in the year, we &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/my-pygame-evolution-embracing-asyncio-and-immutability-for-scalable-design-g55&quot;&gt;explored Pygame&lt;/a&gt; by writing a &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events-1945&quot;&gt;simple tic-tac-toe game&lt;/a&gt;. The exercise unexpectedly yielded a reusable mini framework. Despite being far from “production-ready”, it is still sufficiently good for smaller-scale experiments. One of the possible projects mentioned towards the end of the article was a remake of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/code-rains-revelation-embracing-existence-before-perfection-60f5c641963a&quot;&gt;video intro with the Matrix code rain effect&lt;/a&gt;. Even though I phrased it as a joke at the time, we are diving into it in this article.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*2JV3T2MwZDMlCxNhB8g9AQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;A cute cartoon generated by my editor Gemini&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/my-pygame-evolution-embracing-asyncio-and-immutability-for-scalable-design-g55&quot;&gt;My Pygame Evolution: Embracing Asyncio and Immutability for Scalable Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events-1945&quot;&gt;The Pygame Framework I Didn't Plan: Building Tic-Tac-Toe with Asyncio and Events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/code-rains-revelation-embracing-existence-before-perfection-60f5c641963a&quot;&gt;Code Rain's Revelation: Embracing Existence Before Perfection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;returning-to-the-source-a-baby-steps-approach&quot;&gt;&lt;strong&gt;Returning to the Source: A Baby-Steps Approach&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;We are taking a baby-steps approach in this remake project, as I am rather tied up with my full-time work commitments. In this installment, we are only focusing on the fade-out animation and will continue working towards a full recreation in the future. The fade-out effect in the animation was made possible through the use of jQuery’s .fadeOut() method, which fades an element out in a specified duration.&lt;/p&gt;
&lt;p&gt;How do we do it in jQuery, one may wonder? The snippet below shows how it is being done in the context of a web application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- html --&amp;gt;
&amp;lt;p id=&quot;target&quot;&amp;gt;Lorem ipsum dolor sit amet&amp;lt;/p&amp;gt;

&amp;lt;script&amp;gt;
  // jQuery
  $(&quot;#target&quot;).fadeOut()
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;deconstructing-the-magic-recreating-fadeout-&quot;&gt;&lt;strong&gt;Deconstructing the Magic: Recreating .fadeOut()&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Now that we know how it’s done with jQuery, the logical next step is to start defining the problem. Revisions to the framework may be required if there’s a need to support the specific effect. Actual implementation comes a bit later, after drafting a proper plan. Read on to follow along.&lt;/p&gt;
&lt;p&gt;What is the problem we are solving today?&lt;/p&gt;
&lt;p&gt;We want to make it possible to render a character on the screen, and repeatedly re-render the character with a different alpha value until it reaches 0. Each character is expected to be rendered over a pygame.Rect and we are assuming there are no overlaps in our application. Additionally, the alpha value decreases linearly over the specified duration, which should keep the implementation simple.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*XnZ_hkd3mEvEEszZBpWAJw.gif&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;a screen recording of the desired outcome&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, we figure out what is needed from the framework. In order to trigger a re-render at a consistent interval, we need to post an event in our rendering loop. Let’s call it FRAME_NEXT, and add it to the enum of SystemEvent alongside the INIT event we saw in the tic-tac-toe project.&lt;/p&gt;
&lt;p&gt;That yields an updated display_update function, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def display_update(
    screen_update: asyncio.Queue, clock: pygame.time.Clock
) -&amp;gt; None:
    clock.tick(60)

    updates = []

    with suppress(asyncio.queues.QueueEmpty):
        while element := screen_update.get_nowait():
            updates.append(element.position)

    pygame.display.update(updates)

    # Trigger new FRAME_NEXT event
    pygame.event.post(pygame.event.Event(SystemEvent.FRAME_NEXT.value))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Printing a character to the screen is similar to how we draw a circle or a cross in the tic-tac-toe project. First, we draw a pygame.Rect, then we draw the rendered text surface object over it. Start by rendering the character, and we get the corresponding text_surface and the rect enclosing it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pygame.freetype

font = pygame.freetype.Font(&quot;/path/to/font.ttf&quot;, font_size)

text_surface, rect = font.render('a', (red, green, blue, alpha))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The self-explanatory font_size, red, green, blue, alpha are all integers, and the latter four are in the range of [0, 255].&lt;/p&gt;
&lt;p&gt;Given both the text_surface and enclosing rect, we can print them to the screen at any desired location as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# optionally we can move the rect
rect.topleft = (x, y)

# first draw the backing rect
pygame.draw.rect(application.screen, (0, 0, 0), rect)

# secondly draw the rendered character
application.screen.blit(text_surface, rect)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can finally assemble two functions, one to create an initial print, and another to update the region with the same character but with a different alpha value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass(frozen=True)
class CharElem(Element):
    value: str = &quot; &quot;
    font: pygame.freetype.Font | None = None
    color: tuple[int, int, int] = (0, 0, 0)
    alpha: int = 0

async def char_create(
    application: Application,
    value: str,
    size: int,
    color: tuple[int, int, int],
    alpha: int,
    position: tuple[int, int],
) -&amp;gt; CharElem:
    assert len(value) == 1 and isinstance(value, str)

    font = pygame.freetype.Font(&quot;/path/to/font.ttf&quot;, size)

    surface, rect = font.render(value, (255, 255, 255, alpha))

    rect.topleft = position

    pygame.draw.rect(application.screen, (0, 0, 0), rect)
    application.screen.blit(surface, rect)

    return CharElem(position=rect, value=value, font=font, color=color, alpha=alpha)

async def char_update(application: Application, elem: CharElem, alpha: int) -&amp;gt; CharElem:
    assert elem.position
    assert elem.font

    surface, _ = elem.font.render(elem.value, elem.color + (alpha,))

    pygame.draw.rect(application.screen, (0, 0, 0), elem.position)
    application.screen.blit(surface, elem.position)

    return replace(elem, alpha=alpha)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Updating a drawn character returned by char_create is done through a handler listening to the FRAME_NEXT event we defined earlier. An extension to the usual element event handler is hence needed to hold two extra pieces of data: the duration and start time. Fortunately, this is trivial with the use of __call__ &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-versatility-of-call-a-python-developers-secret-weapon-4akd&quot;&gt;dunder&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class FadeOutHandler:
    duration: int
    start_time: int = field(default_factory=lambda: monotonic_ns() // 1_000_000)

    async def __call__ (
        self,
        event: pygame.event.Event,
        target: CharElem,
        application: Application,
        logger: BoundLogger,
        **detail: Any,
    ) -&amp;gt; None:
        current = monotonic_ns() // 1_000_000
        elapsed = current - self.start_time
        remaining = 1.0 - (elapsed / self.duration)
        alpha = int(max(255 * remaining, 0))

        elem = await char_update(application, target, alpha)

        asyncio.create_task(screen_update(application, elem))

        if alpha &amp;gt; 0:
            asyncio.create_task(
                application.delta_data.put(
                    DeltaUpdate(ApplicationDataField.ELEMENTS, target, elem)
                )
            )
        else:
            asyncio.create_task(
                application.delta_data.put(
                    DeltaDelete(ApplicationDataField.ELEMENTS, target)
                )
            )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some points worth noting regarding the snippet:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;As with the .fadeOut(duration) offered by jQuery, we define the duration in milliseconds&lt;/li&gt;
&lt;li&gt;We remain somewhat stateless by storing just the start_time, and we calculate the alpha value at any time by comparing it to the percentage of remaining time.&lt;/li&gt;
&lt;li&gt;After alpha reaches 0, remember to remove the element reference from the application&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-versatility-of-call-a-python-developers-secret-weapon-4akd&quot;&gt;The Versatility of __call__: A Python Developer's Secret Weapon&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Registering the event can then be done through a helper function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def fade_out(application: Application, elem: CharElem, duration: int) -&amp;gt; CharElem:
    return await add_event_listener(
        elem,
        SystemEvent.FRAME_NEXT.value,
        FadeOutHandler(duration),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Are we done yet?&lt;/p&gt;
&lt;p&gt;One last piece of the puzzle is the click event handler, where a character is shown and eventually fades out into oblivion wherever the user clicks on the screen.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_click(
    event: pygame.event.Event, target: Application, logger: BoundLogger
) -&amp;gt; None:
    elem = await fade_out(
        target,
        await char_create(target, &quot;a&quot;, 20, (255, 255, 0), 255, event.pos),
        1000,
    )

    asyncio.create_task(screen_update(target, elem))
    asyncio.create_task(
        target.delta_data.put(DeltaAdd(ApplicationDataField.ELEMENTS, elem))
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are almost done, though I am skipping the boilerplate bootstrapping code as it is rather similar to the tic-tac-toe application. Basically, the code sets up the window, paints the background colour, and registers the click event. For those interested, do check the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/kitfucoda-intro2&quot;&gt;GitHub repository&lt;/a&gt; for the complete application.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/kitfucoda-intro2&quot;&gt;GitHub - Jeffrey04/kitfucoda-intro2&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-cracks-in-the-framework-scalability-and-loops&quot;&gt;&lt;strong&gt;The Cracks in the Framework: Scalability and Loops&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Some expansion to the framework was already done when I was building the tic-tac-toe game earlier. Apart from some small syntactic changes made to the API, I was pleasantly surprised to find the overall framework design was left untouched. That’s good news, no?&lt;/p&gt;
&lt;p&gt;A revision to the DeltaOperation was done previously to consolidate all changes to the Application object into one queue. It was a sensible improvement to the original implementation, but I still don’t quite like the complexity of the queue consumption. Sooner or later scalability will become an issue we want to address when the need to draw hundreds of elements at a time arises.&lt;/p&gt;
&lt;p&gt;Furthermore, another pain point occurs in the event dispatching loop. Ideally, we want a quick lookup that is close to O(1). Unfortunately, we are looping through elements followed by events on every cycle. Performance is definitely taking a hit as we are running everything concurrently and we are wasting so much time looping needlessly in lookup operations.&lt;/p&gt;
&lt;p&gt;What’s next then? I suppose you may wonder where we are headed, if you are still following thus far.&lt;/p&gt;
&lt;p&gt;Apparently the refactoring that was done to the DeltaOperation somewhat resembles the Actor model. Further research and experiments are needed to see how it fits the design. From what I know so far, the model offers a cleaner separation of data storage and retrieval logic from the rest of the application. Hopefully that possibility of clean separation allows some independence. This can be helpful as it enables us to further split different parts of the application to run in parallel. This would be challenging, given how Pygame is not even thread-safe.&lt;/p&gt;
&lt;p&gt;That means the next installment should focus on the optimization instead of continuing the momentum of building the next feature — the chain of characters raining down the screen.&lt;/p&gt;
&lt;h4 id=&quot;visibility-implementation-and-identity&quot;&gt;&lt;strong&gt;Visibility, Implementation, and Identity&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Just to recap, we achieved the goal of building an almost equivalent to jQuery’s .fadeOut() effect that works with our mini Pygame framework. An exact copy is never the goal, considering the rendering technique is different and we are synchronizing the change with the frame rate. Implementing the gradual decrease of alpha value really does take some effort in designing and planning. This is truly an interesting learning journey and I hope you benefit from following along.&lt;/p&gt;
&lt;p&gt;It really does make you appreciate all the work done to provide the functionality through a simple method call.&lt;/p&gt;
&lt;p&gt;The reimplementation in this low-level setting leads to a personal reflection related to recent news that deeply saddens me. There seem to be ongoing raid operations targeting men-only wellness centres. In one of those operations, a video was leaked across social media where the arrested patrons could be seen unclothed. A list with enough identifying information sans the name was also circulated.&lt;/p&gt;
&lt;p&gt;Every line of code written in the low-level setting is very explicit and corresponds to a visible consequence. This kind of transparency prompted a revelation where, as a gay man, it is crucial to increase the visibility of members of the sexual minority. There are a lot of us in society working earnestly and also actively taking part in our civil duties. It is important to recognize that everyone in society is multi-faceted. As long as no harm is caused, one’s private life should remain private. Judging a person solely on visiting a wellness centre is simply unfair. Even the authorities fail to bring the patrons to court due to lack of evidence in one of the raids. Unfortunately, some still faced repercussions at work, especially the public servants.&lt;/p&gt;
&lt;p&gt;Hopefully you gain insights while reading the articles published here on KitFu Coda, which is my ultimate goal as a fellow engineer. My sexuality indeed doesn’t matter to the content, but it is the best way I could think of to voice my support and solidarity with those who are still suffering the consequences to this day.&lt;/p&gt;
&lt;p&gt;Lastly, thank you for reading, and I shall write again soon.&lt;/p&gt;
&lt;p&gt;This article was written with editorial assistance from Gemini to refine the language and structure. Please note that while the AI helped catch errors, all project ideas, code algorithms, and personal perspectives are my own original work.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8641/recreating-the-matrix-rain-with-pygame-manual-fades-and-the-transparency-of-code</guid>
<pubDate>Sat, 20 Dec 2025 01:54:00 +0000</pubDate>
</item>
<item>
<title>My Pygame Evolution: Embracing Asyncio and Immutability for Scalable Design</title>
<link>https://coderlegion.com/3469/my-pygame-evolution-embracing-asyncio-and-immutability-for-scalable-design</link>
<description>&lt;p&gt;Maintaining a blog is a lot of work, especially when it comes to finding new ideas. Over the weekend, I explored &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pygame.org/news&quot;&gt;Pygame&lt;/a&gt; by building a simple game. I was very humbled by the experience throughout the build. Aha, this exploration turned out to be a worthy idea for a new article! Read on to relive the struggles involved while building a graphical game via Pygame together.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*cdjhvr9v3dw_IwtLW6GqXw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;A cute illustration on the topic, by Microsoft Copilot&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-tic-tac-toe-catalyst-unveiling-pygame-s-low-level-nature&quot;&gt;The Tic-Tac-Toe Catalyst: Unveiling Pygame’s Low-Level Nature&lt;/h4&gt;
&lt;p&gt;Over the weekend, in the EngineersMY chatroom, a friend shared his work in a recent game jam held in his workplace. He built the project with &lt;a rel=&quot;nofollow&quot; href=&quot;https://p5js.org/&quot;&gt;p5.js&lt;/a&gt;, piqued my interest. Eventually I decided to proceed with Pygame, and that decision led me to hours of exploration and perhaps the expense of a few hair follicles. Despite that, the lower-level library nudged me to build a custom architectural design that can potentially evolve into a proper framework.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*eNY9cVdgE87-J49_&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Ryan Snaadt on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Discussions about my friend’s project reminded me of a less than ideal job application that happened around late last year. The offered package was unattractive, and I was expected to work on graphical animation alongside full-stack development. My submitted video processing pipeline work received satisfactory feedback, though the hiring manager came back with another assignment. He requested a new task: to build a graphical application with p5.js, and it was also when I formally ended the application. That mention of p5.js was the first time in a formal setting.&lt;/p&gt;
&lt;p&gt;Interestingly, I was still given a job offer despite all that, which I ultimately rejected.&lt;/p&gt;
&lt;p&gt;Fast forward to the weekend, I consulted with my AI assistant and started my research about &lt;a rel=&quot;nofollow&quot; href=&quot;https://processing.org/&quot;&gt;Processing&lt;/a&gt;, a project where p5.js was based on. Apparently there are ports for Python, though the official processing.py only works with Jython. All other alternatives either relied on the Java package or required special libraries. Consequently, I stumbled upon Pygame after a series of aimless searches.&lt;/p&gt;
&lt;p&gt;Unlike Processing, Pygame felt like a lower level library for building graphical applications. Partially inspired by the discussion on the game jam, but mostly out of boredom, I decided to give it a try. The realization of how low-level Pygame is came when I was figuring out the specification and scope of my over-ambitious initial plan. At the time, my main loop was just barely running.&lt;/p&gt;
&lt;p&gt;As a project to explore graphical application building, I eventually decided to limit the scope further to start small. &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Tic-tac-toe&quot;&gt;Tic-tac-toe&lt;/a&gt; seemed like a good idea, and I should be able to build a reasonably functional version over a weekend. How wrong I was, as I am still doing some last minute changes as of writing this article…&lt;/p&gt;
&lt;h4 id=&quot;building-a-reactive-core-event-driven-design-in-pygame&quot;&gt;Building a Reactive Core: Event-Driven Design in Pygame&lt;/h4&gt;
&lt;p&gt;Pygame is synchronous, and most of the objects and operations are not thread-safe. Due to the interactive nature of graphical applications, they often require a carefully planned strategy to achieve concurrency. AsyncIO is an obvious candidate for the task, though getting it to work with Pygame, requires some thought. Much of the implementation takes a cue from Javascript, especially in the event dispatching department. Immutability of certain components is also crucial, though mainly to keep me sane.&lt;/p&gt;
&lt;p&gt;Let’s start with the main loop, essentially we want something like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def run() -&amp;gt; None:
    pygame.init()

    continue_to_run = True
    clock = pygame.time.Clock()

    pygame.display.set_mode((300, 300))

    while continue_to_run:
        clock.tick(60)

        continue_to_run = event_process(pygame.event.get())

        pygame.display.flip()

    pygame.quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A potential problem I see with this arrangement is that, both handle_events and pygame.display.flip() are inevitably blocking. If any of them stalled for a prolonged period, we will miss the target frame rate. Ideally, we want something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def run():
    pygame.init()

    exit_event = asyncio.Event()

    pygame.display.set_mode((300, 300))

    asyncio.create_task(display_update())
    asyncio.create_task(event_process(exit_event))

    await exit_event.wait()

    pygame.quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*Q7gEPaYVeZW316KJ&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Acton Crawford on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A window popped up, and my event handling work seemed functional. Yet, sometimes when the application exits I get a SIGSEGV segmentation fault when exiting the program. After some research on the error, I found out that Pygame is not thread-safe, hence I should really avoid asyncio.to_thread. This means the following code, delegating the blocking display.flip() call to another thread, is invalid:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def display_update():
    clock = pygame.time.Clock()

    while True:
        clock.tick(60)

        await asyncio.to_thread(pygame.display.flip)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that discovery, I settled with the original main loop convention for a while, and only moved the event dispatching work to a coroutine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def run() -&amp;gt; None:
    pygame.init()

    clock = pygame.time.Clock()
    exit_event = asyncio.Event()

    pygame.display.set_mode((300, 300))

    while not exit_event.is_set():
        clock.tick(60)

        asyncio.create_task(event_process(pygame.event.get(), exit_event))

        pygame.display.flip()

    pygame.quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, due to the synchronous blocking nature of Pygame, the event_process tasks do not have a chance to run, though scheduled successfully. As my coding companion, the chatbot suggested adding asyncio.sleep(0) into the loop. According to &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3/library/asyncio-task.html#sleeping&quot;&gt;the documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Setting the delay to 0 provides an optimized path to allow other tasks to run. This can be used by long-running functions to avoid blocking the event loop for the full duration of the function call.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Adding the statement to the end of the loop allows the application to finally respond to events.&lt;/p&gt;
&lt;p&gt;Previously, I mentioned I performed a last-minute update while writing this article. With this knowledge, we can apply the asyncio.sleep(0) fix to both display_update and event_process, enabling AsyncIO to run the scheduled tasks. In turn, this allows the refactoring of the huge main loop shown in the first snippet into the second snippet.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*S7jVmr9jJnjbRz-x&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Scott Rodgerson on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Before diving into the event dispatching part of the design, let’s talk about how I manage shared data. Certain components need to be shared across different parts of the application. Graphical applications are extremely interactive, hence I would need to respond to them concurrently. In this case, interactivity poses a new challenge — keeping track of changes of these shared objects.&lt;/p&gt;
&lt;p&gt;Much of the design came from my experience with development work in JavaScript. Within a JavaScript application, it is possible to dispatch and subscribe to events almost everywhere, and changes can also be made to a page whenever we see fit. Keeping a reference while staying aware of these changes is a nightmare, and it is extremely unfriendly to my hair follicle.&lt;/p&gt;
&lt;p&gt;Grouping them together into one dataclass, which serves as a registry is usually my first step. In this case, I prefer them to stay immutable, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass(frozen=True)
class Application:
    screen: pygame.Surface = pygame.Surface((0, 0))
    elements: tuple[Element, ...] = tuple()
    state: GameState = GameState.INIT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The three objects consist of a pygame.Surface object for the main application window, an element tuple holding all the interactive widgets in the surface, and an Enum holding the current game state. Events are dispatched to be executed concurrently, so each execution is completely isolated from one another. Therefore, we need to synchronize the changes made to these shared objects to avoid race conditions.&lt;/p&gt;
&lt;p&gt;Aiming to maintain the simplicity inspired by functional programs, I started by marking the dataclass as frozen, as shown in the snippet. Changes to the elements and state objects are deferred to two queues I introduced to the registry dataclass.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass(frozen=True)
class Application:
    ...

    state_update: asyncio.Queue[GameState] = asyncio.Queue()
    element_delta: asyncio.Queue[DeltaOperation] = asyncio.Queue()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Making the dataclass immutable would require a regeneration of application registry for every batch of events.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def events_process(application: Application):
    while True:
        application = await application_refresh(application)

        await events_dispatch(
            application,
            pygame.event.get(),
        )
        
        await asyncio.sleep(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whenever an event handler makes a change to the game state or manipulates the widgets on screen, the changes are pushed to the queue instead. Then the queue is consumed, to update the corresponding object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from dataclasses import replace

async def application_refresh(application: Application) -&amp;gt; Application:
    with suppress(asyncio.queues.QueueEmpty):
        elements = application.elements

        while delta := application.element_delta.get_nowait():
            match delta:
                case ElementAdd():
                    elements += (delta.item,)

                case ElementUpdate():
                    elements = tuple(
                        delta.new if element == delta.item else element
                        for element in elements
                    )

                case ElementDelete():
                    elements = tuple(
                        element for element in elements if not element == delta.item
                    )

    with suppress(asyncio.queues.QueueEmpty):
        state = application.state

        while state_new := application.state_update.get_nowait():
            state = state_new

    return replace(
        application,
        elements=elements,
        state=state
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wrapping the widget element with an ElementAdd, an ElementUpdate and an ElementDelete is just a way to provide metadata to indicate the manipulation type. On the other hand, the replace function, provided by the dataclasses module, is just a helpful shorthand in this case. Alternatively, we can recreate the Application object as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    return Application(
        application.screen,
        elements,
        state,
        application.state_update,
        application.element_delta
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Moving on to the event dispatching part.&lt;/p&gt;
&lt;p&gt;Due to my inexperience, I didn’t spent too much time drafting a design or a plan when I worked on the game. Still, as I went deeper I realized I really needed to start structuring the event handling code. Keeping that in mind, I continued with the build until I managed to get a bare minimum version running.&lt;/p&gt;
&lt;p&gt;Drawing inspiration from JavaScript, I started my refactoring by revising the event handler registration. Directly inspired by the document.addEventListener method, my interpretation of it in this setting took this form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def add_event_listener(target: Eventable, kind: int, handler: Callable[..., None]) -&amp;gt; Eventable:
    event = Event(kind, handler)
    result = target

    match target:
        case Application():
            await target.event_delta.put(DeltaAdd(event))

        case Element():
            result = replace(target, events=target.events + (event,))

        case _:
            raise Exception(&quot;Unhandled event registration&quot;)

    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Besides the widgets in the application window, the application itself can also subscribe to an event. For that, I added a tuple of events as well as an event_delta queue to the Application dataclass. This function always returns either the application registry, or the recreated element. Publishing the recreated element to the element_delta queue is handled by the caller.&lt;/p&gt;
&lt;p&gt;Unlike the display_update function, there is no clock.tick() function to regulate the timing of execution in the event_process function shown previously. After finishing scheduling the current batch, we want to immediately start fetching and processing a new batch of events as soon as possible. The current dispatching function is rather straightforward, as it just schedules the registered handler by cycling through the events tuple in the application registry as well as each of the elements.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def events_dispatch(
    application: Application,
    events: Sequence[pygame.event.Event]
) -&amp;gt; None:
    for event in events:
        match event.type:
            case pygame.QUIT:
                application.exit_event.set()

            case pygame.MOUSEBUTTONDOWN:
                for element in application.elements:
                    for ev in element.events:
                        if not ev.kind == event.type:
                            continue

                        if element.position.collidepoint(mouse_x, mouse_y):
                            asyncio.create_task(
                                ev.handler(ev, element, application)
                            )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basically these is all the code mainly for handling the orchestration work; it contains no game logic. Like how I was working on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/bigmeow_bot&quot;&gt;BigMeow&lt;/a&gt;, a structure emerges when I work more on the project. When I work on a personal project, I also tend to avoid writing object-oriented code as much as possible, and I am glad how well the current revision turns out.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*dQK9UzM3Ixvv0AoZ&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by BoliviaInteligente on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Being a complete newbie to Pygame, Gemini proved helpful yet again. When I talked about the project with a friend, he jokingly asked if I vibe-coded it. However, the whole experience was not unlike &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-quest-continues-porting-the-word-game-with-asyncssh-111n&quot;&gt;how I ported the LLM word game to AsyncSSH&lt;/a&gt;, where Gemini assisted by helping me to navigate the documentation. Granted, the code example is usually good enough, but it helps to know how and why things are written in a certain way.&lt;/p&gt;
&lt;h4 id=&quot;lessons-learned-and-horizons-gained-my-pygame-journey-continues&quot;&gt;Lessons Learned and Horizons Gained: My Pygame Journey Continues&lt;/h4&gt;
&lt;p&gt;Refactoring work started right after I was more or less done with handling mouse click and triggered a change to game state. As the code gets cleaned up, it occurs to me that extracting the game logic into a separate module becomes trivial. To my surprise, after getting Gemini to review the code, I was told the separation would yield a mini framework for similar projects.&lt;/p&gt;
&lt;p&gt;Not knowing where I could get feedback, I turned to Gemini to review my code, and we chatted about how this project could be extended. That follow-up discussion was a very enlightening session, apart from the revelation of an emerging framework. Presumably, the Application dataclass resembles an Entity-Component-System. This was definitely a concept worth adding to my learning backlog.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*edohuBuN2z2BWAyp&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Glenn Carstens-Peters on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If time permits, I would like to work on the separation and complete the game. Curiosity is definitely the biggest motivation behind the effort, rather than publishing the potentially bad framework. New system events like &lt;code&gt;window.onload&lt;/code&gt; and &lt;code&gt;window.onclose&lt;/code&gt; equivalents may also be implemented to better simulate my web development workflow. Follow &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/pygame-ttt&quot;&gt;the project on GitHub &lt;/a&gt; if you are interested.&lt;/p&gt;
&lt;p&gt;Unfortunately, I will not have as much free time for a while. On the flip-side, I no longer have to entertain needy and exploitative hiring managers too. With my incoming new job offer, I will adjust the publishing frequency for the coming weeks. I would also like to take this opportunity to thank everyone offering encouragement and help throughout this trying period. &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/beyond-the-code-a-lunar-new-year-reflection-on-career-recovery-and-new-beginnings-l77&quot;&gt;Spring does come, after winter&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;To ensure this post is as clear and readable as possible, I leveraged Gemini as my editorial assistant. Gemini also provided complementary information to Pygame’s official documentation, helping to clarify complex concepts. However, please note that the voice and all code presented within this article are entirely my own. If you found this article helpful or insightful, consider following me on &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;Medium&lt;/a&gt; and subscribing for more content like this.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/3469/my-pygame-evolution-embracing-asyncio-and-immutability-for-scalable-design</guid>
<pubDate>Tue, 24 Jun 2025 19:00:07 +0000</pubDate>
</item>
</channel>
</rss>